Commit Graph

441 Commits

Author SHA1 Message Date
3dtours cb198ed9fe chore(docker): weigh the NAS bundle that carries the heal's own corner 2026-10-12 20:24:09 +07:00
3dtours 2d9d7ef644 fix(studio): the heal's fill is drawn at the surface's own corner, not at the frame's, so a repair no longer wears the colour of the frame's far side 2026-10-12 20:23:32 +07:00
3dtours cf6b7f5b9f chore(docker): weigh the NAS bundle that carries the wall's own step 2026-10-12 19:57:50 +07:00
3dtours 73d1c22c30 fix(library): the wall takes a step off a single row, and a year of four frames stops stranding the shelf 2026-10-12 19:57:18 +07:00
3dtours c88d43a158 chore(docker): weigh the NAS bundle that carries the cloud reading's last word 2026-10-12 18:43:39 +07:00
3dtours dd9310b344 fix(library): a reading of the cloud says when it has finished, and a window is measured off the shelf that is up
The background walk behind a cloud node read its pages after the answer the
screen had already been given, and no one told the screen when the last page
landed: the note went on saying a reading was running for the rest of the
visit. The walk now gives its own last word, with the count it read.

The strip and the wall windows were measured off the node the effect was set
up on. The stage is drawn while a node is open, so a visit that loses its
nodes and gets them back draws a new strip and a new wall, and a window
measured off the old node — a box of nothing, a scroll always nought — never
moved again for the rest of the visit. Both now read the element that is up.
2026-10-12 18:43:13 +07:00
3dtours 9cbaed42b1 chore(docker): weigh the NAS bundle that carries the cascade heal brush
Same two images, rebuilt on the cascade heal and the Immich re-read; the checksum is the only thing the repo keeps.
2026-10-12 18:13:34 +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 b4bb600cca chore(docker): weigh the NAS bundle that carries the one-mask heal brush 2026-10-12 16:16:01 +07:00
3dtours 5ac2ffb916 fix(studio): a heal stroke is one mask, and the fill comes from its rim
The dust brush had two faults and the second was hidden behind the first.

A stroke was a row of independent repairs. Every circle the brush laid down
found a patch of its own somewhere else in the frame and pasted it, so a stroke
of twenty spots was twenty pastes with twenty rims to match, and what showed on
the photo was the run of circles itself. The mask was their union on paper but
not on the picture.

And the mask was not a stroke either. The brush only ever dropped a spot where
the pointer's own sample fell, and a fast drag reports a sample every few
centimetres of screen — several brush radii apart. Circles that far apart do not
touch, so their union was the row of dots the tool was complained about. The
segment the pointer travelled is now walked at the brush's own spacing and a spot
is laid at every step, whatever speed the stroke was drawn at.

With the mask one shape, the patch had nothing left to justify it. The fill is
now a push-pull pyramid over the whole mask: the painted pixels are cut out of
the frame as a hole, the remaining pixels are halved and halved again until a
level is small enough that no hole is left in it, and each level is drawn back
over the one below it. What comes up out of that is the photo where the photo
was, and the light of the mask's own rim carried inward where it was not —
nothing is copied from anywhere, and a repair matches the grade, the blur and
the grain around it because those are the pixels it was made from.

Non-destructive as before: the recipe still holds the circles, and the preview
and the full-size export run the two passes in engine/exportEngine.ts, the
shaders and the mask stops in shared/utils/heal.ts. A recipe written by the old
brush still loads; the patch coordinates it stored are simply not read any more.
2026-10-12 16:15:18 +07:00
3dtours 3ac58826d5 chore(docker): weigh the NAS bundle that carries the cloud dialog 2026-10-12 14:57:44 +07:00
3dtours adcc6eb67b feat(library): a cloud account is changed where the cloud accounts are listed
The dialog knew two shapes: an add form with the saved links above it, and an
album-only view that hid the form. So a saved server could be retick'd but never
renamed, and the duplicate-address warning was only readable from the add side.

One shape now. The list is always drawn, every row carries SỬA beside
REMOVE THE LINK, and SỬA puts that server's name and address in the fields below
— prefill, edit, save. A blank key is the one that server already answers by, so
a rename does not ask for it again, and the backend's single PUT probes before it
stores either way. The desktop bar's button says TÀI KHOẢN CLOUD once there is an
account to manage, and THÊM IMMICH while there is not.
2026-10-12 14:57:00 +07:00
3dtours a00c9992d7 chore(docker): weigh the NAS bundle that carries the phone header 2026-10-12 14:15:16 +07:00
3dtours 274bda1d4c feat(studio): a phone header keeps the pinwheel and drops the word 2026-10-12 14:14:46 +07:00
3dtours cdb39d2e84 chore(docker): weigh the NAS bundle that carries the clear-all button 2026-10-12 12:54:18 +07:00
3dtours 1f0fb0374e feat(library): a clear-all button puts every filter back at once 2026-10-12 12:53:48 +07:00
3dtours fbaf24ec32 chore(docker): weigh the NAS bundle that carries the maximised-window fix 2026-10-12 11:55:32 +07:00
3dtours 491ae9c199 feat(library): a maximised window leaves the phone frame 2026-10-12 11:03:27 +07:00
3dtours 73b7398583 feat(library): let the shelf be read by month and by day 2026-10-12 10:22:22 +07:00
3dtours 934a3aa056 fix(studio): let the session photo outlive a reload again 2026-10-12 09:50:18 +07:00
3dtours 2f6681851e fix(immich): prune a read node against every frame the reading met 2026-10-12 09:30:26 +07:00
3dtours ebfd1df176 fix(library): put a cloud frame back before opening the studio 2026-10-12 08:56:10 +07:00
3dtours e559452337 chore(git): the measurement frames are read from sample/, not carried at the root 2026-10-12 07:59:36 +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 807c271b6b chore(git): the NAS image bundle is built where it is loaded, not kept in the history 2026-10-12 07:28:03 +07:00
3dtours 0665796e37 improve: Immich cloud filter and optimize 2026-10-11 20:18:04 +07:00
3dtours f37a093a23 chore(docker): rebuild the NAS image bundle on 9517b53 2026-10-11 12:23:39 +07:00
3dtours 9517b53901 feat(library): RESET SETTING puts a frame back to the tile it came in with 2026-10-11 12:00:22 +07:00
3dtours 02ee21678f fix(library): a folder that is not connected is not a file that was moved 2026-10-11 12:00:18 +07:00
3dtours 1848f057fc fix(library): read the catalogue the disk holds, not the version this build asks for 2026-10-11 11:59:38 +07:00
3dtours 3f778a859e fix(library): split a stored path on the separator it was written with 2026-10-11 11:59:38 +07:00
3dtours 6527a9c12d fix(raw): read the frame's own white balance off its embedded preview
The Fujifilm RAF this was reported on opened with magenta through every white
in the frame. The develop itself was the cast, not the tone curve fitted over
it: run flat — white balance, matrix, knee, encode, no curve at all — the
frame's bright neutral blocks came back 232,196,201 against the 223,232,236
the camera's own preview has there. The matrix is not it either; every row of
`rgb_cam` sums to one, so a neutral triple opens neutral. What was wrong is
the balance it was given.

Neither balance LibRaw hands over is the one the frame was rendered with.
`pre_mul` on that frame is R/G 2.296, B/G 1.307; `cam_mul` is 1.795, 1.626;
the balance the preview's own bright neutral blocks imply is 1.588, 1.266 —
and that one is readable: average the sensor triple behind each block the
camera left bright and near neutral, and the ratio that equalises those
blocks is the frame's white.

So the develop reads it there, before the first draw, from the same preview
map the tone curve is fitted on, and keeps LibRaw's tags only as the fallback
a frame with nothing neutral and bright to read (a clipped or dark frame)
falls back to. `cam_mul` is tried before `pre_mul` on that path: as-shot
before daylight, measured 6.73 -> 5.27 on this frame where the reverse gave
6.66.

Measured end to end through the real develop (LibRaw worker + CanvasKit,
64x64 block grid against the file's own preview), mean dE00 / mean dRGB:

  HAGIANG-20170202-FUJIFILM-418.RAF   6.73 -> 4.89   +5.8,+0.9,+0.5 -> 0.7,-0.5,-0.7
  _DSF3087.RAF                        7.52 -> 6.78   +1.6,+0.1, 0.0 -> 0.5,-0.2,-0.7
  DSCF1701.RAF                        6.86 -> 6.54     ~0           -> -1.5,-1.7,-0.9
  _DSF7273.RAF                        5.13 -> 4.66   +2.4,+1.0,+1.5 -> 1.8,+0.9,+1.5
  _3279791.ORF                        5.81 -> 5.93     ~0           -> -0.4,-0.2,-0.5
  P1010256.RW2                        9.60 -> 9.56   +2.5,+0.6,+0.7 -> 1.6,+0.4,-0.1

The four Fujifilm frames lose the cast; the Olympus and Lumix frames hold.
The two that open as their preview (Nikon NEF, DNG) still do. The bright
neutral fifth of both Fujifilm frames now differs from the preview by the
same figure on all three channels — a difference in brightness, not in
colour, which is the tone curve's business and not this one's.

The read is on the frame, not on the brand or its tags, so the bodies with
no sample here (Sony, Canon, Ricoh, Leica) take the same path.

scripts/raw-develop-check.mjs had gone stale against the module and is back
in step with it — the highlight gate's threshold, the gain's divisor, the
preview-WB read and the tag order are pinned, and previewWhiteBalance now
carries a case that hands it a plane and a map and reads the balance back.
2026-10-10 23:38:57 +07:00
3dtours 9c0c71a053 fix(raw): fix FUJIFILM RAF & RAW color cast by prioritizing pre_mul WB multipliers 2026-10-10 22:22:36 +07:00
locpham 2a8aba2ad6 chore(docker): rebuild the NAS image bundle on 1a48d76 2026-10-10 22:00:30 +07:00
locpham 1a48d76105 fix(studio): the head of a list carries no mark of its own 2026-10-10 21:56:47 +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 3961f6eda3 chore(docker): rebuild the NAS image bundle on b35f67f 2026-10-10 21:25:47 +07:00
3dtours b35f67f491 fix(library): a catalogue is read without the file handles it was keeping
A row of `photos` kept the frame's own FileSystemFileHandle, and reading the
catalogue handed them all to the page at once. Measured on a catalogue of
504290 frames, 458103 of them with a handle, opening the library left the JS
heap at 133MB while the browser process held 5.7GB and the renderer 2.1GB:
handles are not the page's heap, so nothing ever asked the collector to run,
and the rescan's own reload of the catalogue every thirty seconds piled one
such reading on the next until the tab was out of memory.

No row keeps a handle now. The catalogue is read a few thousand rows at a time
— the four and a half gigabytes of live handles came from asking for all of
them at once — and a frame's file is asked of its folder's handle when it is
drawn or opened, `fileHandleFor`. `reconnectPhotosForFolders` no longer writes
handles back into the frames, which is what put them there.

504290 rows read in chunks peak at 1.4GB and settle at 1.1GB, against 8.0GB in
one read before.
2026-10-10 21:15:03 +07:00
3dtours 2849eb5977 chore(docker): rebuild the NAS image bundle on b76de03 2026-10-10 20:37:20 +07:00
3dtours b76de03eee Revert "perf(library): a drive is walked with its position written every five seconds"
This reverts commit ced749d72c.
2026-10-10 20:36:31 +07:00
3dtours 3b01478762 chore(docker): rebuild the NAS image bundle on ced749d 2026-10-10 20:26:57 +07:00
3dtours ced749d72c perf(library): a drive is walked with its position written every five seconds 2026-10-10 20:25:53 +07:00
locpham 3a4cfb7671 chore(docker): rebuild the NAS image bundle on 27e68c0 2026-10-10 20:03:05 +07:00
locpham 27e68c0036 fix(library): a drive is read again from the catalogue it is filed under 2026-10-10 20:02:27 +07:00
locpham e5f6df16f7 chore(docker): rebuild the NAS image bundle on b3b9ed0 2026-10-10 19:11:44 +07:00
3dtours b3b9ed0193 fix(library): a reading that never finished no longer says nought
`saveWalkedDirs` filled in a nought beside every folder a reading had listed but
never opened. A reading stopped by a closed tab, and one kept to a branch, list
the rest of the roll before they have been into any of it — 484 folders of one
real roll read empty over tens of thousands of frames — and the record is folded
back into itself, so the noughts stayed.

Only a reading that met the whole roll may say nought now (`WalkedDirs.full`), and
the screen drops a nought from a record that does not say so. An old record has no
word to give, so its rows fall back on the catalogue instead.

feat(library): search Immich from the filter row

One box, one query, one node: the backend reads what is typed as a tag name when
the key can see one and as a file name otherwise, and the answer is filed under the
node itself. A share link searches its one album in place. The box is not a filter —
it leaves the shelf, which is what the md's search permission is for.

feat(ui): a shorter status line, and a wall that keeps its own row height

The status line is one short 12px row at the foot of the library. A wall with fewer
frames than there are columns no longer stretches its one row — and the cards' own
borders with it — down to the stage.
2026-10-10 18:27:49 +07:00
3dtours edd7b8497b chore(docker): rebuild the NAS image bundle on 1059c87 2026-10-10 18:08:07 +07:00
3dtours 1059c87c87 chore(docker): rebuild the NAS image bundle on 7299f1d 2026-10-10 18:00:41 +07:00
3dtours 7299f1dbde fix(app): the hand-over is announced, so the window that held the pen lets go 2026-10-10 17:58:48 +07:00