Commit Graph

195 Commits

Author SHA1 Message Date
3dtours ea7973cb1e fix(heal): continuous anisotropic isophote inpaint on mobile to eliminate reload and blockiness 2026-10-16 10:47:17 +07:00
3dtours 8952d1f282 fix(heal): run neural inpaint via WebGPU and dedicated worker to prevent crash and blocky patch 2026-10-16 10:37:21 +07:00
3dtours bdea7d3395 fix(heal): exemplar-based texture synthesis inpaint on mobile to prevent WebKit crash and grey blob 2026-10-16 09:58:07 +07:00
3dtours ce31e498d9 fix(heal): enable MI-GAN neural inpaint on mobile with bounded memory to prevent grey blob 2026-10-16 09:39:15 +07:00
3dtours de20fb4440 fix(heal): handle mobile/iOS with fast local Poisson inpainting to prevent WebKit OOM crash 2026-10-16 09:03:56 +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 5ff46116ce fix(heal): read the rim for the fill, take the grid from the frame; VIGNETTING both ways; EV in the wide panel
HEAL came back a stain larger and softer than the stain it was asked to
remove, and the model's half of it answered in a colour that appears nowhere
near the repair. Three causes, all inside `engine/inpaint.ts`:

- The fallback's reading ring was the hole dilated by two pixels whatever the
  brush, so the boundary of the fill was the dirt's own penumbra and the fill
  came back as the dirt. The pad is now proportional to the hole's own radius
  (`rimPad`), the fill is an inverse-square average of the clean ring relaxed
  into one field by 36 Jacobi rounds, and the hole's grain is read against a
  2px box of the clean pixels and taken from the pixel it mirrors through its
  own middle. The texture half before this was a no-op — `detail = rgba[p] -
  fR[idx]` with `fR[idx]` the same pixel's own colour, so every donor handed
  over zero and the hole was the smooth field alone (15.6% of the ground's
  detail). Measured on a stain of radius 10 with a halo to 16 under a 12px
  brush (`probe-fallback.mjs`): the repair landed 22.3 levels below the ground
  it sits in, and comes back 0.06 off it.

- The plate the model reads under the mask was the mean of every pixel of the
  crop, so the network met a hard step at the mask's own edge and answered in
  that colour. It is now the rim's own light spread inward (`cropPlate`), and
  the mask is downsampled by "any hole pixel in this cell" rather than by a
  corner sample. Step across the mask's edge: 69.3 levels before, 9.5 after.

- The same repair came back different every time the reader zoomed out of it:
  the crop was sized from the stage's own pixels on the zoom ladder
  (`PREVIEW_QUANTUM`), so each zoom handed the model a different grid, and
  every grid invents its own pixels. The crop's box is now its share of the
  FRAME (`INPAINT_REF_LONG`, `exportEngine` 6e), so one spot is one grid at
  every zoom and at the export's own size.

The patch is deliberately not baked into the photo: a recipe stays reusable on
other frames and the repair stays a pass. ponytail: the pixels handed to the
model are still read from whatever the stage rendered at, so the answer is the
same repair to within the resampling of its input; a patch cache keyed on the
frame's identity and the spots is the rung that makes it identical, and the one
to take if a zoom still shows a repair move.

VIGNETTING now runs both ways (-10..10): down darkens the four corners, up
lifts the lens' own falloff out of them with a white ink — the same curve and
the same weight. EV was defined and live on the phone but absent from the wide
panel's own Light row, which is `DevelopPanels.tsx` and nothing else.

`npx tsc --noEmit`, `vite build`, `heal-check`, `studio-drag` and the other
suites pass. `studio-export-check` fails one assertion here on the code at HEAD
as well — a frame left behind files its waiting look 9s after the pick — so it
is not this change.
2026-10-15 22:37:05 +07:00
3dtours 1a718a74d9 fix(library): a roll picked at a drive root counts its frames under one spelling
The picker hands a whole drive over named `\`, which `normPath` folds to the
empty root, and a row of that roll was keyed `Roll` where the reading files its
counts, its walked folders and its frames under `/Roll` — one folder under two
names. Every number beside a subfolder of a picked drive read nought, and the
spinner of a reading stood on the roll's own row and nowhere it was reading.

The row key is the roll's own spelling again, and so is the path `relOf` hands
back for a row under it: a reading kept to one folder of a drive root asks for
`/Roll`, and `sub` counted from the old `Roll/` merged nothing. Both are the
spelling the check at the foot of `scripts/tree-scan-check.mjs` asks for, and it
is green again.

A bounded reading also measured itself against `\/Roll` spelled from the raw
name, which no id is filed under: `held` stood at nought and the line read the
catalogue's whole roll as the ground it was climbing from. It is built from
`normPath` of both, the way an id is.
2026-10-15 16:22:15 +07:00
3dtours fa0b2cb719 fix(heal): one drag is one repair, and the band at its rim is the model's
A press has to mean what it lands on. A drag is a run of spots and the run
has a spine, so a press within half a brush radius of that spine takes the
drag and dragging moves it, while a press in the outer half of the spots —
where the run is not, and a new repair is wanted — lays a stroke against it.
Before, a brush near a repaired one was read as a drag OF it: the second
stroke was folded into the first and the first moved under the user. Spots
carry the drag they came from (stroke), older recipes still answer to the
old radius, and the outline of a repaired run is drawn idle so it can be
taken hold of (app.css).

A repair already made is part of the photo it was made in. The renderer now
solves one drag at a time, in the order they were drawn, each against the
frame the drags before it have already left — so a stroke laid beside a
repaired one invents its pixels from a frame that no longer holds what the
first took away, and the repair the user has already accepted does not move
when they brush next to it. Measured (probe-seam.mjs, a second stroke laid
2.5 brush radii from the first, counted in levels over the first drag's own
circles): 0 at the worst pixel, 0 of 19189 pixels moved by more than two.

The ring the first repair left behind was the fill's and not the model's.
The model's pixels were laid through the mask's own ramp, so the outer
quarter of every spot — where the mask still weighs the fill in at up to
full strength — was the fill's, and the fill is the light of the WHOLE box
it was built in: a stroke a third of the frame long is filled by blurs up to
sixteen brush radii wide, so what it brings there is the box's average and
not the rim's. The model is the repair wherever it has one, so it now owns
the spot out to 0.92 of its radius (HEAL_PATCH_STOPS) and the fill only
meets the photo in the sliver the mask itself fades over. Measured with the
same probe: 0.76 levels of mean change over the repair where it was 1.86,
8.5 at the worst pixel where it was 14.2, and the edge strength at the rim
after the repair 0.92 where the photo's own edges around it are 0.5 (1.52
before) — the band that was 8 to 11 levels dark is gone.

matchToRim goes with them: the relaxation it ran over every crop measures
nothing the band above does not already do, and the probe's numbers are
identical with it removed.
2026-10-15 15:32:59 +07:00
3dtours cfdebab157 feat(library): read a cloud folder by its albums, and a frame by any of its paths
A folder on an Immich server is named by an album's id and a folder on this
machine by a path, and both are drawn in one column, so a preview asked for
by one spelling of an address has to be found by the others. A frame's id
now answers to the spellings of the same path — drive-letter case, either
separator, a leading slash — and the thumbnail cache holds enough of them
that a roll is not read twice.

The cloud catalogue is read by what the folder says it is. Kind 'share', or
a folder with no album ticked, is every album the key can see: one reading,
kept to its own cursor, and the frames no album names found by the
whole-library read behind it. A folder with albums ticked is those albums,
read album by album, and it is complete when the last of them is. The album
rows are written before the node's own number, so the column never says a
number and then grows rows under it.

The inference session runs without a proxy: the worker script a proxy loads
is fetched by the session's own origin, and a cross-origin page is served
by the page's.
2026-10-15 15:32:52 +07:00
3dtours ba181da86b fix(tone): read the ramp's base from the pixel's own neighbourhood, so highlights and shadows move a region and keep the detail in it 2026-10-15 00:55:09 +07:00
3dtours aa8f462058 feat(library): ask the origin to keep the tiles, so the visit after opens painted 2026-10-14 23:07:22 +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 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 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 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 897335b701 fix(studio): the refusal Immich gives is the one the upload dialog says 2026-10-12 22:38:30 +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 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 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 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 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
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
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 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 ced749d72c perf(library): a drive is walked with its position written every five seconds 2026-10-10 20:25:53 +07:00