Skip to content

Review fixes: type-check gate, data-loss guards, ML worker hardening, input-handling bugs#108

Merged
dompm merged 8 commits into
mainfrom
claude/focused-rubin-pmcknj
Jun 10, 2026
Merged

Review fixes: type-check gate, data-loss guards, ML worker hardening, input-handling bugs#108
dompm merged 8 commits into
mainfrom
claude/focused-rubin-pmcknj

Conversation

@dompm

@dompm dompm commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Batch of fixes from the codebase review, prioritized for impact and safety to land without manual app testing.

What's in

#85 — build type-check was a no-op

"build": "tsc && vite build" ran plain tsc against the solution-style tsconfig ("files": []), which checks nothing and exits 0. Now tsc -b, with all 16 surfaced errors fixed — three of them were live bugs:

  • "Replace pattern" passed a File to updatePatternImage(url, w, h), corrupting the project (and autosaving the corruption); both upload and drag-drop now share one loader
  • The lamp profile dialog referenced a nonexistent updateProjectReferenceError on scale-unit change; now uses updatePatternScale
  • SheetPanel's forceTool was an undefined name → ReferenceError when switching to an uncalibrated sheet with the measure tool active

Plus: 11 props App passed to SheetPanel that it never declared, CANVAS.fg, the hoisted-function null-narrowing issue in Lamp3DPreview, the STEPS record type, unused locals, vite-env.d.ts, and a CI workflow running the build on PRs/pushes to main.

#86 — silent data-loss paths in OPFS persistence

  • Save errors propagate again, so the existing "Couldn't save / Retry" UI is reachable (it was dead code)
  • A failed load is no longer treated as "new user" — startup can't overwrite a real project with an empty one; unreadable projects restart under <name> (recovered) so the original file is preserved
  • Rename writes the new file before deleting the old one (was: delete immediately, write 500 ms later)
  • Pending debounced saves flush on pagehide; navigator.storage.persist() requested once
  • Importing a project clears undo/redo (undo could restore and persist the previous project's state over its file)
  • Export uses a Blob URL instead of a data: URI that hit URL length limits on image-heavy projects

#88 / #89 / #90 — ML worker hardening

  • ORT's wasm binary is now a same-origin Vite asset instead of a runtime jsdelivr fetch (which was a hard dependency for every user, including WebGPU ones) — verified the built worker bundle contains zero CDN references; removed the dead @huggingface/transformers dependency
  • SAM backend rejects all in-flight requests and resets the singleton when the worker crashes (pieces no longer spin forever); nesting worker posts ERROR instead of swallowing exceptions and reporting COMPLETE, and the packing promise rejects + terminates the worker on load failure (Pack spinner can't stick)
  • A failed model-cache write no longer re-downloads the ~305 MB encoder already in memory, and the network fallback checks res.ok instead of feeding an HTML error page to InferenceSession.create

#91 — Smart Pack now enforces the cutting gap between pieces

Pieces conflict when they intersect, contain each other, or any edge pair comes closer than gapPx (point-to-segment distance over edge pairs). Numerically verified: 30×30 squares at gapPx=8 land ≥ 8 px apart; gapPx=0 still packs flush. (Oversized-piece skipping and the rotation reset remain open in #91.)

#92 — smooth lamp mode no longer breaks on contracting tiers

For tiers whose radius decreases (e.g. the tulip profile), the unrolled sector has L_top > L_bot, but the forward mapping assumed the opposite: the d-range check rejected the whole tier (pieces vanished from 3D) and the v denominator was clamped positive (profile edits destructively flattened drawn polygons onto an arc via reflowLampPoints). The inverse mapping was already sign-correct, so only the forward direction changed. Verified numerically: round-trip reflow error < 1e-12 across a v/theta sweep on an 800→400 contracting profile; zero-size tiers no longer emit NaN into the mesh. (The cross-tier theta mirroring in the 3D preview is left for visual verification.)

#94 (partial) / #95 — input handling

  • Marquee selection on the glass sheet no longer self-deselects (Konva's synthesized post-pointerup click is suppressed after a marquee commit; the click handler is kept since it's the only tap-to-deselect path on touch)
  • Cmd/Ctrl/Alt chords no longer trigger single-letter tool shortcuts (Cmd+C → crop, Cmd+S → refine-remove, etc.); the pen tool's Cmd+Z vertex-pop is preserved
  • Delete/Backspace from a focused <select> no longer deletes the selected piece

#97 / #98 / #105 — smaller fixes

  • Segmentation results are clipped against the pieces as they are when the worker resolves, not the render-time snapshot (two quick boxes no longer produce overlapping pieces); replaced debugMask bitmaps are close()d
  • Welcome tutorial waits for the OPFS load instead of inspecting the empty placeholder project
  • Sheet IDs use crypto.randomUUID() instead of Date.now()

Verification

tsc -b clean and pnpm build green after every commit. The packing-gap and lamp-geometry math were verified numerically against the bundled modules (details in the commit messages). Not manually tested in-app — flagging the riskiest spots for a quick once-over: the rename flow, marquee select on the sheet panel, a cold-cache segmentation run (wasm now served same-origin), and lamp smooth mode with the tulip profile.

Closes #85, closes #86, closes #88, closes #90, closes #95, closes #105

https://claude.ai/code/session_018ug1kMLE8aSdT8k4prFPYV

claude added 5 commits June 10, 2026 11:36
The build script ran plain `tsc` against a solution-style tsconfig
("files": [] + references), which type-checks nothing and exits 0, so
real type errors shipped unchecked. Switch to `tsc -b`, fix everything
it surfaces, and add a CI workflow so it stays green:

- App.tsx: "Replace pattern" passed a File to updatePatternImage(url,
  w, h), corrupting the project (and persisting the corruption via
  autosave); route both upload and drag-drop through a shared
  loadPatternImageFile helper
- App.tsx: lamp profile dialog referenced a nonexistent updateProject —
  ReferenceError on pattern-scale unit change; use updatePatternScale
  (same path as the measure tool)
- App.tsx: drop 11 props passed to SheetPanel that it never declared
  or read
- SheetPanel: `forceTool` was an undefined name — ReferenceError when
  switching to an uncalibrated sheet with the measure tool active;
  align with the handleToolChange path which checks !sheet.scale only
- SheetPanel: CANVAS.fg doesn't exist; use var(--text-soft) for the
  pack-popover label
- Lamp3DPreview: convert hoisted `function resize()` to a const arrow
  so the container null-narrowing applies
- Tutorial: type STEPS as Record<AnchoredStepId, StepConfig> instead of
  a Record keyed by the full StepId union it never covered
- Remove unused locals/imports; add vite-env.d.ts so the ?worker import
  in packing.ts resolves; gitignore *.tsbuildinfo
- Add GitHub Actions workflow running pnpm build (tsc -b + vite) on
  pushes to main and PRs

Closes #85

https://claude.ai/code/session_018ug1kMLE8aSdT8k4prFPYV
- saveToOPFS no longer swallows write errors; failures now propagate to
  persist() so the existing "Couldn't save / Retry" status UI actually
  fires (it was unreachable before)
- loadProjectFromOPFS returns null only for genuine NotFoundError and
  throws on anything else (corrupt JSON, transient OPFS errors), so
  startup no longer mistakes a broken-but-present project for a first
  visit and overwrites it with an empty one; on load failure the app
  starts under "<name> (recovered)" so autosave can never clobber the
  original file
- Rename now writes the project under the new name and deletes the old
  file only after the write succeeds, instead of deleting immediately
  and leaving a 500 ms window with no copy on disk
- Flush the pending debounced save on pagehide so edits made within the
  debounce window survive closing the tab
- Request navigator.storage.persist() once on startup (OPFS is
  best-effort storage and silently evictable otherwise)
- Importing a project now clears the undo/redo stacks; undoing after an
  import could previously restore the prior project's state and persist
  it over that project's file
- Export uses a Blob object URL instead of a data: URI, which hit
  browser URL limits on projects with embedded images
- switchProject/deleteProject/createNewProject handle the now-throwing
  save/load calls explicitly (stay on current project on a failed
  switch; surface save failures via saveStatus)

Closes #86

https://claude.ai/code/session_018ug1kMLE8aSdT8k4prFPYV
- SheetPanel: Konva synthesizes a click after every pointerup with no
  movement threshold, so committing a marquee selection immediately ran
  handleStageClick's background-deselect and wiped the selection in the
  same gesture. Suppress the synthesized click right after a marquee
  commit. The handler itself stays: on touch devices (where pointerdown
  pans instead of marqueeing) it is the only tap-to-deselect path.
- Both canvas panels' single-letter tool shortcuts now ignore
  Cmd/Ctrl/Alt chords: Cmd+C no longer switches to crop, Cmd+V to
  select, and Cmd+S no longer toggles refine-remove. The pen tool's
  Cmd+Z vertex-pop is hoisted above the guard and unchanged.
- App's Delete/Backspace handler now ignores events from <select>
  elements, matching the panels' own guards; pressing Delete with the
  piece popover's sheet dropdown focused deleted the selected piece.

Fixes #95, partially addresses #94 (panel scoping and the listener-
ordering race still need the shared shortcut hook).

https://claude.ai/code/session_018ug1kMLE8aSdT8k4prFPYV
… model cache retry

- Serve onnxruntime-web's wasm binary as a same-origin Vite asset
  instead of fetching it from jsdelivr at runtime. The WebGPU EP runs
  on top of the wasm runtime, so every user needed the CDN to be
  reachable for segmentation to initialize at all, and the hardcoded
  version string in the CDN URL could silently drift from the installed
  package. (#88)
- Remove @huggingface/transformers: zero imports anywhere in src, and
  it bundles its own copy of onnxruntime-web that would conflict with
  ours if ever actually imported. (#88)
- SAM backend: reject all in-flight requests when the worker crashes
  (GPU device lost, wasm OOM) instead of leaving their promises — and
  the pieces marked pending — hanging forever, and reset the singleton
  so the next call spawns a fresh worker. Handle messageerror too. (#90)
- Nesting worker: post ERROR instead of logging the exception and
  reporting COMPLETE as if packing succeeded; packing promise now
  rejects on worker error/load failure and always terminates the
  worker, so the Pack spinner can't spin forever. (#90)
- Model cache: a failed OPFS cache write no longer triggers a full
  re-download of the ~305 MB encoder that is already in memory, and the
  network fallback now checks res.ok instead of handing an HTML error
  page to InferenceSession.create as model bytes. (#89)

https://claude.ai/code/session_018ug1kMLE8aSdT8k4prFPYV
…ions

- The welcome-tutorial decision now waits for the OPFS project load
  (gated on isLoaded) instead of inspecting the empty placeholder
  project at mount; returning users with saved work but cleared
  localStorage were dumped into the tutorial on top of their project
  (#105)
- handleAddPiece/handleUpdatePrompt clip segmentation results against
  the pieces as they are when the worker resolves (via an
  always-current ref) rather than the render-time snapshot; two boxes
  drawn in quick succession were each clipped without the other's
  final polygon, producing overlapping pieces (#97)
- debugMask ImageBitmaps are closed when replaced instead of waiting
  for GC; repeated refine clicks accumulated tens of MB of pinned
  bitmap memory (#97)
- Sheet IDs use crypto.randomUUID() like piece IDs instead of
  Date.now(), which could collide within a millisecond and break
  piece->sheet references (#98)

https://claude.ai/code/session_018ug1kMLE8aSdT8k4prFPYV
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 10, 2026

Copy link
Copy Markdown

Deploying vitrai with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9b6fbf4
Status: ✅  Deploy successful!
Preview URL: https://18c57876.vitrai.pages.dev
Branch Preview URL: https://claude-focused-rubin-pmcknj.vitrai.pages.dev

View logs

claude added 3 commits June 10, 2026 12:32
For a tier whose radius decreases downward (Rt > Rb), the unrolled
sector has L_top > L_bot, but three code paths assumed the opposite:

- patternToSurface rejected every point in the tier because the d-range
  check used [L_top, L_bot] as [lo, hi]; pieces in contracting tiers
  vanished from the 3D preview entirely
- The v parameter clamped its denominator positive with
  Math.max(1e-6, L_bot - L_top); for contracting tiers numerator and
  denominator are both legitimately negative, so v collapsed to 0 —
  and since reflowLampPoints round-trips every drawn polygon through
  patternToSurfaceRobust on profile edits, committing a profile change
  destructively flattened those polygons onto the tier's top arc
- Same clamp in Lamp3DPreview's per-vertex mapping

The inverse mapping (surfaceToPatternRobust) already handled both signs
correctly, so only the forward direction needed fixing. Also guard the
cylinder branches against 0/0 NaN from zero-size tiers, which passes
straight through Math.min/max into the mesh position buffer.

Verified numerically (esbuild-bundled module, node): contracting
profile 800->400 now maps v=0.3 exactly where the inverse places it,
round-trip reflow error < 1e-12 across a v/theta sweep, degenerate
zero-height tier produces no NaN.

The cross-tier horizontal mirroring of contracting tiers in the 3D
preview (theta01 direction) is intentionally left untouched — it needs
visual verification. See #92.

https://claude.ai/code/session_018ug1kMLE8aSdT8k4prFPYV
findBestPlacement only applied gapPx against the bin walls; the
inter-piece test was a raw intersection check, so pieces were packed
flush against each other — leaving no clearance for the glass cutter,
which is the entire purpose of the gap (defaultCuttingGapPx computes a
2 mm physical allowance).

Pieces now conflict when they intersect, one contains the other, or any
edge pair comes closer than gapPx (point-to-segment distance over edge
pairs; both directions covers the min distance between non-crossing
segments). The bounding-box prefilter is inflated by gapPx so the exact
check sees near-misses, which also fixes exactly-touching edges
slipping through segmentsIntersect's strict inequalities.

Verified numerically: packing 30x30 squares into a 100x100 bin with
gapPx=8 yields >= 8px separation between all pairs (10px after grid
quantization); gapPx=0 still packs flush with no overlap.

Addresses the headline bug of #91 (silently-skipped oversized pieces
and the rotation reset are still open there).

https://claude.ai/code/session_018ug1kMLE8aSdT8k4prFPYV
…urprise phases

First-run flow was: init downloads the decoder (~25 MB), progress hits
100% ("downloaded!"), bar hides — then the first encode kicks off the
~309 MB encoder download and the bar restarts near zero, which reads as
"it's downloading the same thing again" (observed during the tutorial).

- init() now registers the encoder files in the progress accounting up
  front (first run only — skipped when they're already in the OPFS
  cache), so the reported fraction covers the full first-run download
  and never claims completion after just the decoder
- After the decoder is ready, the encoder download is warmed in the
  background instead of waiting for the first encode; buffers are
  released to the OPFS cache when no encode has claimed them, so blank
  projects don't pin ~300 MB of worker memory
- Encoder fetches are deduplicated behind a shared promise so the
  warm-up and a concurrent first encode can't download the 309 MB
  twice; failed warm-ups remove their progress entries (no stalled bar)
  and retry transparently on the first segment

https://claude.ai/code/session_018ug1kMLE8aSdT8k4prFPYV
@dompm dompm merged commit 48b2fc9 into main Jun 10, 2026
2 checks passed
@dompm dompm deleted the claude/focused-rubin-pmcknj branch June 10, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment