Commit Graph

137 Commits

Author SHA1 Message Date
3dtours 4df6ba7ccc fix(ui): expand develop sidebar width and fix ruler track and chip label wrapping in desktop STUDIO 2026-10-16 12:29:36 +07:00
3dtours 7a3b81b6ea feat(ui): compact Color Wheels & Match panel with MiniSlider rulers and 4-column color bands 2026-10-16 08:56:49 +07:00
3dtours 2dac868274 fix(heal): feed crisp photo context to inpaint model and cache patch across zoom levels 2026-10-16 07:58:38 +07:00
3dtours 64e6ff2608 fix(heal): restore inpaint-web MI-GAN neural model to resolve grey blob bug 2026-10-16 07:36:25 +07:00
3dtours 49c5c9c5b9 fix(heal): the mask owns the whole spot, and the fill is the diffusion, not the model 2026-10-15 23:24:01 +07:00
3dtours d17ae406e0 fix(studio): the roll stands while the next frame opens
A click on the next frame of the roll emptied the stage before the bytes
were read — `clearPreview` for the picture, and `loadFile` dropping the
catalogue's frame id for the strip — and both the roll and the frame's own
info line lived inside the branch that drew the picture. The whole strip
went with it and was drawn again on the way back: every tile torn down and
read a second time, three loads and three fresh images, for a click that
changed one frame.

The info line and the roll stand outside that branch, so the roll survives
the click, and a caller opening a frame of the catalogue's own roll says so
and keeps the id `loadFile` otherwise clears. The info line now sits above
the roll instead of under it.

probe-strip-click.mjs, desktop and phone: 0 tiles torn down, 0 loads,
the frame clicked is the one lit, the info line above the roll.
2026-10-15 20:41:36 +07:00
3dtours 9607f9594e fix(studio): the zoomed stage keeps the frame's own pixels
The preview copy behind the stage stopped at PREVIEW_HARD_MAX (3200) whatever
the photo was. A 5729px frame opened from Immich, zoomed past 3200 device
pixels, was painted from a 3200px copy stretched over the screen — soft enough
for a reader to call it the thumbnail having opened.

The ceiling is now the photo's own longest edge. `resizedJpeg` never grows a
photo, so at that size it hands the original bytes straight back, and the copy
still only grows when the stage says it needs it: at fitting size the 1600px
one stands and the edit keeps its old cost.

Measured on the operator's own Immich asset (5729x3820, retina 2x, opened on
the stage and zoomed 12 notches in): the stage held 3200x2134 before and
5729x3820 after.
2026-10-15 18:55:41 +07:00
3dtours b2e2ebb266 fix(studio): the strip asks for the frame in view, and lands where it is put
The rail glided to the frame in hand. A glide across a roll of six hundred
fires a scroll event a frame, and every one of them asked the thumbnail
cache for another window: the tiles of the frames being looked at queued
behind two screens of margin, so the strip sat blank exactly where it was
being read while it drew frames nobody had reached. The rail now lands
where it is put, one window is asked for, and the frames in view are the
first asked for a tile.

The strip also keeps its height: `overflow-x: auto` makes the other axis a
scroller too, so a strip squeezed by the column hid its own frames rather
than spilling them. The photo above gives way instead.
2026-10-15 17:52:37 +07:00
3dtours 065c9af1c1 feat(studio): a strip of the folder's own frames under the photo
The studio is where a frame is graded, and the frame next to the one being
graded is the one the reader almost always wants next — a roll is shot in
order and looked through in order. Until now the only way to it was LIBRARY:
the stage had no way back to the folder the frame came from.

The strip stands between the photo and the row that names the file, on the
phone at the same place. It holds the folder the open frame was filed in —
the frames beside it, not every frame the folder was scanned for, which is
what the row's own `dir` is read for — in a file browser's order rather than
the catalogue's newest-shutter-first, because a roll off a camera is the
order it is looked through in. The frame on the stage is the lit one and the
one the rail is scrolled to, wherever the rail had been left; a click is the
same opening the catalogue's wall makes, so the frame arrives with the look
it was last left at.

The tiles are the shelf's own: `makeThumb` off the same store, and the URL
cache the wall keeps is lent to the strip through the hook that fills it.

  docker/frontend/src/ui/Filmstrip.tsx      the strip
  docker/frontend/src/ui/ImageStage.tsx     one optional slot for it
  docker/frontend/src/App.tsx               the folder, read off the catalogue
  docker/frontend/src/Library.tsx           the tile hook, exported
  docker/frontend/src/styles/app.css        the strip and its frames
2026-10-15 17:16:59 +07:00
3dtours bfa207f8b2 fix(develop): call the tonal rows what the reference calls them (HIGHLIGHTS, SHADOWS, WHITES, BLACKS) 2026-10-15 00:55:11 +07:00
3dtours 70ed61b8de fix(studio): stroke clustering for HEAL, mobile WebGL inpaint stability, and reorganize LIGHT panels with HSL 2026-10-14 12:57:31 +07:00
3dtours d52d4c4281 feat(studio): redesign LIGHT tab to Lumetri style and fix mobile inpaint freeze with worker proxy and patch cache 2026-10-14 11:01:52 +07:00
3dtours 5aa6a5ba23 fix(studio): a repair lands under the brush with no pill over it, and a browser with no device is not asked for one 2026-10-14 08:59:13 +07:00
3dtours 836858c36c fix(studio): the repair keeps to a worker wherever it falls back, so a device that cannot run the graph cannot freeze the page 2026-10-14 00:52:58 +07:00
3dtours e31074be4a feat(studio): a phone's header folds into one button, the open ruler no longer rides the photo 2026-10-13 23:35:44 +07:00
3dtours 4eb74c3779 fix(studio): a drag on the photo holds the render until the finger lifts 2026-10-13 19:56:46 +07:00
3dtours b03896e398 feat(studio): a finger across the photo is the open ruler's own track 2026-10-13 19:42:08 +07:00
3dtours 4888bfda77 fix(studio): file a catalogue frame's look and its tile ten seconds after the stage goes still 2026-10-13 15:41:59 +07:00
3dtours b37ec97352 fix(studio): a dust spot is a place on the photo, so the angle carries it too 2026-10-13 07:08:28 +07:00
3dtours 1d7510366a fix(studio): a dust spot is a place on the photo, so a quarter turn carries it with the frame 2026-10-13 06:38:43 +07:00
3dtours bf5eeac14f fix(studio): the repair belongs to the frame, so a look loaded over it leaves it standing 2026-10-12 22:26:51 +07:00
3dtours 550e688b3c revert(studio): the repair is drawn once again, and the model's own answer is the only one the stage shows
The two rounds were meant to answer the stroke sooner, but a preview cannot be
half a repair: the blurred fill is what a machine without the runtime gets, and
with it on the stage a spot of dirt came back smeared rather than gone — worst of
all on a run of strokes, where each new one cancels the model's round that was
still on its way and leaves the blur standing longest. The repair that works is
the one that waits for the model, and the wait is worth it.

The worker stays available but is switched off with the rest: this reverts the
runtime's proxy along with the second round, so the stage is exactly what it was
before, lag and all.

Reverts 5489114.
2026-10-12 22:05:45 +07:00
3dtours 54891148a9 fix(studio): the repair runs off the page's own thread, and a stroke is answered twice so the hand is never held
The graph is seconds of solid arithmetic however large the crop is, and it ran
where the pointer lives: the brush lagged behind the hand for exactly as long as
the model took. The runtime's wasm provider now works in a worker of its own, so
the page keeps drawing while the repair is thought through.

The wait itself stays — it is the model's, not the page's — so a preview paints
the blurred fill first and lets the model's version of the same mask replace it.
Filing and export still wait for the model: the faster answer is for the eye
waiting on the brush and for nothing else.

Measured on the same photo and stroke, worst main-thread gap 3703 ms before and
487 ms after; previews land at 1.0 s and 5.3 s, and the model's render is the one
left on the stage.
2026-10-12 21:43:00 +07:00
3dtours 15fb4422dc feat(studio): HEAL invents what was under the brush with MI-GAN, and keeps the blurred fill for the machines that cannot run it
The dust brush filled its hole with a blur of the hole's own rim — the light
beside it, carried in. Right for a speck on a wall, and visibly thin over
anything with structure: a stroke across a shirt came back as the average of a
shirt. The fill is now a learned one.

engine/inpaint.ts runs MI-GAN (andraniksargsyan/migan, the same
`migan_pipeline_v2.onnx` inpaint-web ships) through onnxruntime-web on the
visitor's own machine: 27MB of weights in public/models, WebGPU where the graph
fits the provider, the processor otherwise, one warm-up pass so the first repair
is not the one that pays for the kernels. It is handed the masked crop and
nothing else — the model's mask polarity is 255 KEEP / 0 HOLE, the opposite of
the obvious guess, and the other way round repaints the frame and leaves the
hole (measured).

Step 6e of the renderer cuts the crop at the resolution the model will actually
run at (512 on its longest side — the graph is fully convolutional and would
take a whole frame, and ten seconds over one), builds the mask from the brush's
own circles, and draws the answer into the fill surface at its own place in the
box. The composite, the feathered mask and the untouched frame around it are
unchanged, so nothing outside the mask moves — and when the model cannot be had
the blurred fill is still built first and still stands. The preview gets this
for free: renderPhoto was already async and the preview was already debounced.

Node probe: the model + polarity + read/write path run; heal-check.mjs keeps
measuring the fallback and now asserts the wiring and the polarity. Browser:
engine/inpaint.ts in a real Chromium returns a patch (hole moves, kept pixels
within 0.02 of a level), and the built app renders a 42-spot stroke in 4.7s on
single-threaded wasm with no fallback warning — 1.15s and the warning when the
model is taken away.
2026-10-12 21:07:09 +07:00
3dtours 00063a7f7c fix(studio): the heal fill is a cascade of blurs, the brush is put down with its tab, and Immich is re-read on the way back
HEAL painted a grey blob over the photo it was meant to repair, and the
cause was one draw Skia gets wrong on this build: a snapshot used as a
SCALED source comes back with the alpha multiplied into its rgb a second
time, so a partly transparent rim — which is what the mask's feather is —
was read as a dark wash. A pyramid of halvings is built of exactly those
scaled draws, which is why the repair's own rim came out grey; the same
source read at 1:1 is exact (measured, tmp-src-probe).

So the fill is now a cascade of blurs, all of them 1:1: the cut frame is
blurred at a few widths, widest first, each finer one drawn over it, the
untouched cut on top. The finest is one brush wide, each next is twice the
last, and the count follows the depth of the hole (the stroke's own
diagonal against the widest brush), which is what keeps the middle of a
long hole filled rather than bare. Nothing is copied from anywhere and the
rim arrives with no seam, which is what the passes were always for.

And the fill is built on the brush's own box rather than the frame, padded
by three sigma of the widest level. The composite reads it only where the
mask is, so this changes no pixel of the result (heal-check measures the
same masks either way) and it is what makes the repair affordable at the
zoom a repair is aimed at: at a 3200px preview a single spot went from
1.7s of frozen main thread to 1.2s, and the heal's own share of that from
0.8s to 0.3s. What is left is the pipeline itself, which is a separate
matter.

The FIX brush outlived its tab. It takes the pointer on the photo, and
leaving TOOLS never put it down — so its layer stayed on the stage over a
photo nobody was editing, and its ring came back under the pointer every
time the hand crossed a repair, which reads as the tool re-arming itself.
It now goes down with the tab, with another chip or another strip, and on
a press outside its own two chips, the same way the mask tool does.

And the Immich button was missing for a visitor who had just connected a
server: the studio and the library are two screens of one page, both
mounted, so coming back is a route change and not a mount — the studio's
own effect, which reads the server list once a session, never ran again.
It is read again on a popstate that lands on /app.

Checks: node scripts/heal-check.mjs (the cascade and the box, to the
letter), npx tsc --noEmit, and the studio driven in a browser: the button
appears on the way back from the library, the brush goes down with TOOLS
and re-arms with HEAL, and a stroke over a photo lays one band with no grey
in it.
2026-10-12 18:12:45 +07:00
3dtours 97fd181cfa feat: the camera's own colour, a reading that keeps its place, and cloud links in Settings
- RAW opens at the colour the camera wrote into it: the develop carries the
  difference against the file's own preview (previewResidual) instead of the
  per-channel cubic fit, dE00 6.04 -> 0.54; previewMatch.ts keeps only the grid.
- Immich reading: keyset position kept across visits, one reading per node,
  a page in flight retried, timeline buckets for the years on offer.
- The wall draws a server frame at the address the proxy serves it (no blob
  minted for it), and the 80-thumbnail pre-cache on open is gone.
- Settings lists the cloud links, flags two that answer at the same address,
  and forgets one.
- Custom frames imported from a file (engine/customFrame.ts, IndexedDB).
- Upload an edited frame from Studio to Immich Cloud.
2026-10-12 07:59:24 +07:00
3dtours 0665796e37 improve: Immich cloud filter and optimize 2026-10-11 20:18:04 +07:00
locpham 00b5897ec3 feat(studio): every name in the four lists carries the mark of what it names 2026-10-10 21:54:41 +07:00
locpham 2329ea779f feat(studio): MY PHOTOS, FAVORITED, PHOTO STYLE and RECIPES read as a list of names, and their column is the visitor's 2026-10-10 21:47:34 +07:00
3dtours 7b3086b1db feat(library): a shelf that fills in as it reads, and one that carries the studio's look and frame back with it
- tiles land one at a time. The screenful a picked folder asks for was built all
  at once, and a HEIC or a RAW in it is a decode of its own, so the strip stood
  empty until every one of them was done: 2293ms on a roll of eighteen, with the
  batch arriving in one go (0/18 then 18/18 in a single step). Tiles are built in
  two lanes now and each one goes up the moment it is drawn — first picture at
  36ms, then 1, 2, 4, 6, 8, 9, 11, 12, 14, 15, 16, 17, 18 of 18, done at 2496ms.
  The threading is handed back between tiles (`scheduler.yield`), so a click
  still answers while the strip fills: no long tasks either side, worst frame gap
  169ms -> 164ms, a view toggle 49ms -> 41ms. The tiles are the same tiles; the
  wall's is drawn from the same counter.
- the studio's look reaches the shelf. A frame graded in the studio writes the
  tile the shelf draws, at neutral geometry — no turn, crop, watermark or frame,
  since the shelf turns a tile itself — and every screen holding the tile is told
  the moment it is written, so the row repaints without a reload.
- the shelf comes back on the frame the studio was holding. The frame a visit
  opened is kept with the visit, and coming back from the studio raises it: the
  strip centres it and the wall brings its card to the middle, even when the
  frame sits in another folder of the same tree. A shelf that opened on whatever
  it was left standing on sent the reader looking for the frame they had just
  been working on.
- the frame's name stands under the picture, in the studio's own meta bar.
2026-10-09 21:52:38 +07:00
3dtours c2e27b8e9d feat(studio): edits follow the photo — reopening it from LIBRARY restores the whole look
Settings changed in the studio were remembered per photo, but only as a
film recipe, so everything outside that recipe (straighten, flips, crop,
watermark, geotag, frame, the picked sim) was gone as soon as the photo
was reopened from the library in a later session.

Keep the whole StudioState instead and restore it when a library photo is
opened; rows written by older builds — a flat recipe — are still readable,
so photos edited before this change open with their look intact.

SAVE PHOTO is untouched: that remains the step that publishes a photo to
the landing film strip.
2026-10-09 21:19:06 +07:00
3dtours 9ad61be26a fix(library): one session for the whole tab, and a shelf that does not take the machine
The library is the accounts' shelf, but the tab kept two copies of that fact: the
screen read `/me` once as it mounted and then stopped listening, so a visitor who
signed in at the studio came back to the panel asking for the login they had just
given. One record, read by the studio, the landing page and the shelf alike, is
now the only copy — a sign-in anywhere is a sign-in everywhere.

- auth.ts (new): the tab's one session record. `useAuth()` reads it through
  `useSyncExternalStore`, a `/me` already in flight is joined rather than
  repeated, and a call that fails leaves the record *unknown* rather than signed
  out, so a cold API never reads as a guest.
- TopBar: LIBRARY is a link for a member and a dead button for everyone else. An
  anchor wearing `aria-disabled` still navigates on a click, and the screen
  behind it would only send the guest home again — so a guest gets the shape of
  the button and nothing to press.
- Library: the gate is that shared record, not a copy taken at mount. A guest who
  opens `/library` straight is turned home with `replace`, so a Back press does
  not walk them into the same wall. The screen is mounted the whole time the tab
  is open, so the turn is for the address the visitor actually opened: a guest on
  the studio stays on the studio.
- engine/library.ts: the thumbs hand the thread back. Two lanes instead of four,
  a frame's worth of idle between them, and one job per frame — a second caller
  joins the decode already running instead of starting another.

Verified in Chrome (stubbed API, harness run against both the dev server and the
built bundle): a guest gets a dead LIBRARY button and stays on /app; signing in at
the studio turns it into the link and opens the shelf, with no second login and no
"log in to browse" note; the same after a sign-in on the landing page; /library
opened cold as a guest lands on / with Back going to /app; opened cold signed in,
the shelf is up. On a folder of 22 frames the open runs 2 decodes at a time (was
5) and repeats no frame (44 decodes, was 56 — 12 frames were read twice); the
tiles still come out 22 on the open, 24 by hand, 22 again after a reload.
2026-10-09 20:50:17 +07:00
3dtours 00f06ce1ba fix(studio): open a new photo with the GPS stamp off
A photo carrying EXIF GPS arrived with GPS WATERMARK already ON: adoptPhoto
set useGeotag to the presence of the position, so the file's own EXIF decided
to print a location. The stamp is the visitor's to ask for.

A new photo now opens with it OFF whatever the file carries; the position is
still read off the photo and is there the moment the chip is switched on by
hand (and the device-locate try for an EXIF-less frame is unchanged).

Verified in a real Chrome against the built bundle: a GPS photo opens OFF
with no hint, switching the chip ON reads back 'GPS read from the photo.'; a
no-GPS photo opens OFF and offers the manual entry.
2026-10-09 18:12:12 +07:00
3dtours 88d97432bd fix(library): stop calling a .tif a RAW, and stop the wall's render loop
Three things, all from one look at a folder of TIFF scans.

A `.tif` is no longer a RAW. Nothing here develops one, and `tif`/`tiff` sat in
`RAW_EXT` for no decoder's sake: opening a TIFF asked for PRO and its tile wore
a RAW badge, for a file the engine has nothing to hand back. Both extensions
are dropped, and a TIFF keeps its own route instead — the tile reads the file's
own pixels (`useThumbUrls`, via `tiffDevelop`) and so does the studio — so a
`.tif` still opens and still draws, it just is not a RAW any more.

The wall's render loop. `painted` was a fresh `.slice()` on every render, so
`useThumbUrls` saw a new list each time round and its effect ran again, and
again: about twenty thousand renders a second while the page sat idle, and the
file reads frozen at the first screenful, so a tile scrolled to never came up
(7 of 24 cards drawn). `painted` is memoised on `listed, wall.from, wall.to`
now — no date formats in eight idle seconds, 28 of 28 cards after a scroll.

Scrolling between the two modes. SINGLE → ALL THUMBS lands the wall on the
frame the strip was showing, centred in the window; ALL THUMBS → SINGLE swipes
the strip to the frame nearest the wall's middle, centred in the row. Each way
the frame the other mode was standing on is the one you come back to.
2026-10-09 16:12:18 +07:00
3dtours 981a482e1b fix(boot): bring the session and the catalogue back without waiting on /me
A reload drew nothing until the account round trip landed, which on a
faraway API is the whole of the wait: 8.8s to the photo on the studio's
stage and 8.2s to the library's first row, against 0.9s and 0.3s now
(measured with every /api answer held back 8s).

The studio's restore effect returned early on `!authReady`. The gate is
only needed by the two files that ask a tier question — a RAW, which is
a PRO develop, and a catalogue frame, which walks `loadFile` — so it
keeps those and lets the session's own JPEG up on the first pass.
`restoredRef` stops the second pass, when /me lands, from decoding the
frame the first one already showed.

The library mounted nothing until /me answered. What the first paint
reads is now the answer the last visit got, the way the studio reads its
own session back: the frames are already on this disk and have no reason
to sit behind a round trip. An answer the API contradicts puts the note
back on screen the moment it arrives, and an API that never answers is
not written down as this account's standing.
2026-10-08 17:09:53 +07:00
3dtours bb4e574486 feat(studio): hand an export straight to a social network
EXPORT only ever wrote a file, and posting it meant leaving the studio, finding
the file and uploading it again. The dialog now offers FACEBOOK, INSTAGRAM,
REDDIT and X beside the sizes, and the render goes to the network instead of the
download folder.

What the browser can do decides how, since a page may not touch another origin's
composer: a phone's share sheet opens the target app with the picture already
attached, a desktop gets the picture on the clipboard and the site in a new tab
for one paste, and an origin with no clipboard API downloads the file beside the
open site. The line under the row says so before the click.
2026-10-08 14:38:22 +07:00
3dtours cbcfe353ab feat(studio): draw every slider as a line with a hollow node, and move the shapes into TOOLS
The default ring was a solid disc on a coloured bar, which read as a crude
block next to the rest of the rail; every range input is now a hairline
track with an outline node in the accent colour, and the temperature and
tint strips keep their gradients on the track itself.

CROP, ROTATE and WATERMARK reshape the frame rather than grade it, so they
belong with the tools that work on the whole image.
2026-10-08 11:28:14 +07:00
3dtours af97e53ab3 style(studio): place Color Chrome sub-buttons on 1 single row and format chip labels bold with theme accent colors 2026-10-07 17:59:50 +07:00
3dtours 4cdf828e9b fix(studio): ensure Color Chrome sub-chips are hidden when switching tabs 2026-10-07 17:41:00 +07:00
3dtours 18c09bf417 feat(studio): add right-click context menu (Solo mode, Collapse all, Expand all) for DevelopPanels, pin Color Chrome sub-chips, and fix library single view sorting & time range select 2026-10-07 17:35:20 +07:00
3dtours 2ab92a05fa feat(studio & library): sync Light & Color tab parameters (vibrance, highlight, shadow, white, black) to Gradient Mask & add multi-field sorting options 2026-10-07 12:36:55 +07:00
3dtours 15dab6d65a feat(library & studio): add EXIF info bar, portrait frame panning, and multi-photo selection with Panorama/HDR context menu 2026-10-07 10:44:59 +07:00
3dtours 2af000c1cb library: open a phone's HEIC, and lock the shelves to accounts
A HEIC is the one camera file no engine on this platform will paint:
Chromium answers createImageBitmap with InvalidStateError and its
WebCodecs take no hvc1 still, and unlike a RAW there is no JPEG preview
inside to lift. It is decoded in software instead, by libheif built to
wasm, in a worker of its own — so a folder of them reads without the tab
stopping for a second, and what leaves the worker is the same JPEG
developRaw hands the studio. Nothing downstream knows the difference.

Into the studio it goes on the same terms as every other frame: dropped,
picked, or opened from the catalogue, no tier asked and the engine's own
colour untouched. The frame's date, ISO and position come off the HEIC
itself, which is passed along as the bytes the stamps read.

The catalogue's tile does not pay the decode: a phone writes a small
HEVC copy of its frame as an item of its own (thmb, 320x240 on an
iPhone), reachable only by walking the container by hand, and it decodes
in milliseconds where the frame takes a second — from the same 256KB the
catalogue already reads for a header.

The shelves themselves now want an account: a guest gets the note and
the way in, and the scan never starts.
2026-10-06 20:59:40 +07:00
3dtours e2e0297f31 library: stand a frame up in the catalogue, wall, strip and studio
The library stage carried no way to correct a frame shot on its side, and
nothing kept the correction. A quarter turn now rides on the catalogue row
(`rot`, beside `star`, carried over by a rescan), the two chips under the
preview step it, and the tile, the strip thumbnail and the stage all read it
through one bake so a frame is never stood on its side in one place and
upright in another. The studio opens a frame at that angle and a turn made
there is filed straight back onto the row, so either end of the turn is
what the frame comes up at next time.
2026-10-06 17:22:56 +07:00
3dtours f7c509eff1 fix(raw): fix RAW preview matching, EXIF orientation alignment & studio initial state 2026-10-06 12:23:38 +07:00
3dtours b1ebb7467c fix(studio): let a session an older build filled heal on the next open
The previous frame stops a session being written, not read: the browser that
reported this still holds the olive develop under the ORF's name, and a reload
would paint it once more. A session photo named like a RAW is that develop and
nothing else — the RAW branch writes none — so the restore path now reads the
parked RAW first and only falls back to such a copy when there is nothing
parked to develop. An ordinary photo's name is left alone.

Measured in Chromium on the built bundle, with the session poisoned and the ORF
parked: a copy named POISON.ORF opens as the re-developed 1600x1197 frame
(mean 167.3,164.2,138) and a copy named POISON.jpg opens as itself.
2026-10-06 07:28:13 +07:00
3dtours 9aa9e67d33 fix(studio): stop the session from serving a frame an older build developed
A RAW was parked in OPFS whole and its develop kept in the session too, so a
reload painted the stored JPEG and never went back to the sensor. That makes a
session outlive the engine that filled it: 4c675a1 fixed the Olympus ORF's
colour — measured against the file's own preview, mean 167.0,163.9,137.7
against the preview's 167.0,164.8,138.6 — and a browser that had opened the ORF
before the fix kept painting the olive develop, which is what "the fix is
deployed and it still opens olive" was.

The RAW is in OPFS either way, so the RAW branch now clears the session slot
instead of filling it (forgetPhoto) and the reload develops the parked sensor
data again (App.tsx:767). Measured in Chromium against the deployed bundle:
after a drop the session holds no photo, and a reload re-develops the parked
RAW to the same 1600x1197 frame, mean 167.3,164.2,138. The cost is one develop
per reload; the comment names the build stamp that would lift it.
2026-10-06 07:23:21 +07:00
3dtours 91722861f2 fix(library): optimize image scanning, fast raw preview extraction and lazy thumbnail loading 2026-10-05 06:34:37 +07:00
3dtours 0f123a48dd feat(frame): add OLD FILM LANDSCAPE and OLD FILM PORTRAIT from old_film.png 2026-10-02 19:46:06 +07:00
3dtours 9b24ce7bbc web: the RECIPES chip hands the phone's bar to the list, not a second row over it — IMPORT .RECIPE stays the tab's own chip, and the path says TABS > PRESETS > RECIPES
On a phone the bar holds one level: the tab's chips, or the strip a chip of
theirs opened. The RECIPES list was the exception — it stood over the bar
with the bar still under it, so the level the finger had just asked for was
drawn twice, once as the list and once as its own row of chips.

The list now displaces the bar, beside the option groups and the watermark,
and wears the bar's slot and skin: that row IS the bar now.

IMPORT .RECIPE is not one of the looks the list holds, so it does not go
into the list — the way in belongs to the chip that opens it, not to the
content it fills. It stands in the tab's own row, one chip over from RECIPES
and at the same level, which is where it already stood on a wide screen. It
stands down with the bar while the list is open, as every chip of that row
does.

With the bar gone the tab's chip is gone too, so the path under the strip
grows the level: TABS > PRESETS > RECIPES, and PRESETS in it returns to the
tab's own chips. A wide screen is untouched — the bar never stands down
there; it simply gains the chip in its own row.

Checked on the running bundle (390x844 and 1280x900): the bar holds the
three chips in one row, the list takes the bar's slot under the ceiling
holding looks and nothing else, IMPORT is not among them, the path reads and
walks back, and a wide screen keeps its bar.
2026-10-01 14:53:09 +07:00