Commit Graph

411 Commits

Author SHA1 Message Date
3dtours df44200890 feat(heal): compute true outer union boundary for brush strokes and expand zoom viewport with free panning 2026-10-14 18:42:58 +07:00
3dtours c384d421d6 feat(heal): group brush stroke circles into unified patches with contour outline, bulk drag, and bulk delete 2026-10-14 18:07:18 +07:00
3dtours 4b5e55bc50 fix(heal): eliminate boundary seams by windowing inpaint clusters to 1:1 scale and smoothing feather transition 2026-10-14 17:35:27 +07:00
3dtours 8df0a03414 fix(heal): eliminate border seams with smoothstep feather, 95% solid mask, and matched photo grain texture 2026-10-14 16:45:15 +07:00
3dtours f6acf3ee6c fix(heal): eliminate grey wash artifacts by restoring opaque patches and progressive fillSurface cropping 2026-10-14 16:29:17 +07:00
3dtours 1fe31c35a4 fix(heal): resolve adjacent objects texture leak and overwriting by clustering and progressive masking 2026-10-14 16:15:00 +07:00
3dtours a24f000ead style(mobile): uppercase button names in mobile hamburger menu and export button 2026-10-14 15:58:49 +07:00
3dtours 91d7d8d471 feat(studio): move UNDO and REDO icons to mobile topbar outside hamburger menu 2026-10-14 15:43:01 +07:00
3dtours dfa75f3356 fix(studio): resolve iOS HEAL auto-reload and grey blob with fast harmonic inpaint 2026-10-14 15:17:57 +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 172df9871b fix(studio): theme-sync LIGHT tab, fix iOS HEAL crash, and align docker container names to recipescam 2026-10-14 12:06:03 +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 8fee6693bf fix(studio): an iPhone keeps the repair off WebGPU, off the worker pool and at 384 pixels, the three things its page did not survive 2026-10-14 09:50:51 +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 4ff5eca71a fix(studio): the repair model runs in a worker where the phone has no device, so the page keeps answering fingers 2026-10-14 00:18:35 +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 ad85f0bd0a fix(studio): a touch anywhere on the ruler takes the number, on every engine 2026-10-13 23:06:27 +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 6e810b66fc feat(studio): two fingers on the stage are the phone's own zoom, for aiming FIX and HEAL 2026-10-13 19:20:30 +07:00
3dtours 976ec2cd37 fix(library): a server's frames stand at the shape the server stated, not at a guess 2026-10-13 18:50:47 +07:00
3dtours 139b5fb42b feat(library): the ADD CLOUD door says where the key is made and which boxes to tick 2026-10-13 15:51:06 +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 6f2c9cc780 fix(library): draw one tile for a frame a catalogue holds under both spellings 2026-10-13 15:02:22 +07:00
3dtours a3171dd40e fix(library): one row per frame of a drive root, however a build spelled its id 2026-10-13 14:57:49 +07:00
3dtours 9e2e6a6727 feat(library): close the mobile preview on the frame the reader swiped to, selected in the thumb grid 2026-10-13 12:54:45 +07:00
3dtours 04461afbdf fix(library): fold the picked drive root to the root it is, so a folder row draws its own frames and the subfolder toggle holds 2026-10-13 12:32:29 +07:00
3dtours 6d3ee728b4 feat(library): mint a server frame's tile once and keep it, always show the whole branch under the open node, and draw the year tick as a grey rule 2026-10-13 11:50:01 +07:00
3dtours 1ca8ac6031 feat(library): lay the ALL THUMBS wall out in justified rows per day, and stop clipping the frames at the pane's edges 2026-10-13 11:10:57 +07:00
3dtours 822833c1ef feat(library): cut the wall by month and day, and stand a ruler timeline on its right edge 2026-10-13 10:37:57 +07:00
3dtours ddbde7d426 feat(library): read the wall's time by the year and the month, and mint only the frames near the eye 2026-10-13 09:54:47 +07:00
3dtours 62f244f3c2 fix(library): draw the wall whole and read its time down the right edge 2026-10-13 09:38:05 +07:00
3dtours fcded40c65 fix(library): measure the wall's shelf in two steps so the scroll reaches its end 2026-10-13 09:15:59 +07:00
3dtours ada0c3d93e feat(library): the all-thumbs wall is cut into the days its frames were shot 2026-10-13 08:31:50 +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 897335b701 fix(studio): the refusal Immich gives is the one the upload dialog says 2026-10-12 22:38:30 +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 fa52ffc424 feat(library): the single view walks the shelf from the picture itself, by arrow and by finger
SINGLE is where a roll is read one frame at a time, and the step from one frame
to the next was a trip to the strip below the picture and back. The stage now
carries the pair itself, at the edges the eye already reads as "there is more
this way", dimmed at the ends of the roll rather than taken away so the pair does
not jump about. The press stops at the arrow: the zoom, the pan and the open
under it are the picture's.

The phone's full-screen view has no room for a pair, so the same step is the
reader's own finger. A drag that travelled sideways is a step the way the list is
read; one that wandered up and down, and a tap in place, are left alone.

Both walk the list on screen — filters and sort included — so what is drawn after
the step is what stood beside it.

Verified against a local preview with a seeded shelf: the arrows appear, the left
one is dimmed on the newest frame and the right one on the oldest, and a left
swipe on the phone moves to the next frame and back.
2026-10-12 21:55:58 +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 6d437591a0 fix(studio): a repair wears its band only while the pointer is on it, so the photo is not left under a wash of the tool's own 2026-10-12 20:39:54 +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 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 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 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 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 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