Picture a panorama too large for one texture being navigated

A 22 927 × 8966 Lightroom panorama of a glacier, opened in develop:
the wheel in from fit to the peaks, a slow pan along the ridge at that
zoom, back out, then 1:1 with a double-click and fit with another. The
scene borrows the file from outside the demo library, copies it into a
panorama folder, restarts so the scan finds it, and deletes it and the
sidecar the app writes beside it before restarting again; it is
registered last so no other scene sees it.

The film is fine ice and rock in every frame and was 22 MB at the
usual 960 px and ten frames a second, so a scene can now name its own
GIF size (GIF_SIZE, read by record.sh through `scenes.py gif`); this
one is 640 px at eight, 8.7 MB.
This commit is contained in:
2026-09-27 19:43:09 -04:00
parent 5e863fa718
commit 757133d2a8
6 changed files with 111 additions and 1 deletions
+6
View File
@@ -47,6 +47,12 @@ manual does not show; it reads two files and runs in CI. `--changed`
re-records a scene when one of its sources, or its own code, has changed
since the commit that last touched its pictures.
GIFs are made at 960 px and ten frames a second. A scene whose film does
not compress at that size says otherwise in `GIF_SIZE`, which `record.sh`
reads through `scenes.py gif NAME`. `giant_pano` borrows a 500 MB panorama
from outside the library (`DR_GIANT_DNG`, by default
`/var/tmp/dr-dng/IMG_4181-Pano.dng`) and removes it again, so it runs last.
Two things the scripts know that are not obvious: a Slint `TouchArea` wants
a held press, not xdotool's `click`; and the app is driven on Xvfb rather
than the desktop because under XWayland at scale 2 a pointer warp lands at
+2 -1
View File
@@ -150,7 +150,8 @@ python3 "$scenes" record "$media" "${@:-all}"
# GIFs for the page; the MP4s are working files and are not kept.
for mp4 in "$media"/*.mp4; do
[ -e "$mp4" ] || continue
"$here/gif.sh" "$mp4" "${mp4%.mp4}.gif" 960 10
read -r width fps < <(python3 "$scenes" gif "$(basename "${mp4%.mp4}")")
"$here/gif.sh" "$mp4" "${mp4%.mp4}.gif" "$width" "$fps"
rm "$mp4"
done
find "$media" -name '*.png' -newer "$stamp" -exec optipng -quiet -o2 {} + || true
+97
View File
@@ -9,6 +9,8 @@
scenes.py changed the scenes whose sources changed since
their pictures were last committed
scenes.py list every scene, its pictures and sources
scenes.py gif NAME the width and frame rate NAME.gif is
made at (960 10 unless a scene says)
Each scene drives the running app through `drive.py` and leaves the
pictures it names in OUT_DIR: PNGs, and MP4s that `record.sh` turns into
@@ -1527,6 +1529,99 @@ def duplicates():
pause(3)
# --- a photograph larger than one texture -------------------------------------
# Last on purpose: it adds a file to the library for its own length only, and
# no scene after it should find that file there.
GIANT_DNG = os.environ.get('DR_GIANT_DNG', '/var/tmp/dr-dng/IMG_4181-Pano.dng')
GIANT = 'IMG_4181-Pano' # a 22927 × 8966 Lightroom panorama of a glacier at dusk
# A GIF that is fine rock and ice in every frame does not compress; at the
# usual 960 px and ten frames a second this one was 22 MB, and 11 at 720.
GIF_SIZE = {'giant-pano': (640, 8)}
def restart_on_library():
dr.stop()
dr.launch([])
dr.wait_ready()
pause(5)
at_library() # it reopens on the photograph it closed on
pause(3)
def remove_giant(folder, made_folder):
for f in os.listdir(folder) if os.path.isdir(folder) else []:
if f.startswith(f'{GIANT}.'):
os.remove(os.path.join(folder, f))
if made_folder and os.path.isdir(folder) and not os.listdir(folder):
os.rmdir(folder)
@scene(media=['giant-pano.gif'],
sources=DEVELOP_SRC + ['ui/dr-ui/src/develop/session.rs', 'ui/dr-ui/src/develop/render.rs',
'core/dr-gpu/src/demosaic.rs', 'core/dr-pipeline/src/tiles.rs'])
def giant_pano():
"""A linear DNG too large for one GPU texture, opened and navigated: in
from fit on the peaks at its left as far as the wheel goes (sixteen
times fit, short of 1:1 on a frame this wide), a slow pan along the
ridge, back out, then 1:1 on the peaks with a double-click and fit with
another. The file is not in the demo library (it is 500 MB): it is
copied into a `panorama` folder for this scene, the app restarted so the
scan catalogues it, and the copy and the sidecar the app writes beside
it deleted, and the app restarted again, afterwards. DR_GIANT_DNG names it."""
folder = os.path.join(DEMO_LIBRARY, 'panorama')
copy = os.path.join(folder, f'{GIANT}.dng')
made_folder = not os.path.isdir(folder)
try:
os.makedirs(folder, exist_ok=True)
shutil.copy2(GIANT_DNG, copy)
restart_on_library()
at_library()
grid_top()
for _ in range(60): # until the scan has catalogued it
if dr.present(f'{GIANT}@ListItem'):
break
pause(2)
dr.click(*cell(GIANT))
dr.wait_for(f'{GIANT}.dng', 120)
wait_canvas(300)
pause(8) # the reduced copy, drawn
a = dr.photo(0.14, 0.22) # the snow peaks at the left
rec('giant-pano')
pause(1.5)
for _ in range(10): # the wheel's limit, 16 times fit
wheel(-1, *a)
pause(0.7)
pause(1.5)
cx, cy = mid(dr.rect('id:canvas-image'))
for _ in range(3): # along the ridge, rightwards
dr.drag(cx + 350, cy, cx - 350, cy, 90)
pause(1.0)
for _ in range(10):
wheel(1, cx, cy)
pause(0.5)
pause(1.5)
# 1:1 is past the wheel's limit on a frame this wide: a double-click
# reaches it, on the peaks the film started from, and another fits.
b = dr.photo(0.2, 0.2)
dr.move(*b)
pause(0.3)
dr.x('click', '--repeat', 2, '--delay', 80, 1)
pause(3.0)
dr.x('click', '--repeat', 2, '--delay', 80, 1)
pause(2.0)
cut()
finally:
# Back to the grid first, or the next start reopens the photograph
# it closed on and writes its sidecar again.
at_library()
dr.stop()
remove_giant(folder, made_folder)
restart_on_library()
remove_giant(folder, made_folder)
# --- running them -----------------------------------------------------------
def order(names):
"""`names` with their prerequisites, each once, prerequisites first."""
@@ -1677,6 +1772,8 @@ def main():
elif cmd == 'changed':
for name, why in changed():
print(f'{name}\t{why}')
elif cmd == 'gif':
print(*GIF_SIZE.get(rest[0], (960, 10)))
elif cmd == 'list':
for s in SCENES.values():
after = f' (after {", ".join(s.after)})' if s.after else ''