48 Commits

Author SHA1 Message Date
Samuel Prevost
5e2071dc7f Serena MCP
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-05-01 00:44:39 +02:00
Samuel Prevost
3cfd1c3101 chore(chrome): logo flush left, stepper to the right at lg
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Header is now a flex row with the Skwik mark + tagline pinned left and
the stepper + theme toggle on the right at lg+. Below lg the row stacks
— logo on top, stepper underneath — and the theme toggle moves up next
to the logo so it stays reachable without the absolute-positioned
floating button in the footer. Footer is now byline + GitHub link only.
2026-05-01 00:41:36 +02:00
Samuel Prevost
8aaec477f6 chore(chrome): swap Gitea corner ribbon for a footer GitHub link
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
The corner ribbon ate the top-left of the viewport on desktop, plus
pulled in 60+ lines of CSS for a single decoration. Replace with a
small "Fork me on GitHub" link in the existing footer (12 px Octocat
glyph + label, separated from the byline by a middot). Drops the
`.github-fork-ribbon` CSS block and the now-empty header spacer
comment along with it.
2026-05-01 00:39:14 +02:00
Samuel Prevost
790f3e9147 fix(measurements): labels also dodge handle dots, not just other labels
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
The collision resolver only avoided overlapping label rects. Labels
could still land on handle dots — easiest to see on a horizontal line
where the endpoint dots sit at the same Y as the midpoint anchor,
making the pill cover the dot the user wants to grab.

Extended `resolveLabelPositions` to push labels past any handle dot
too. Each handle becomes a 20×20 phantom rect (centred on the dot,
sized to match the primary handle's outer reach + ring) that the
resolver dodges with a 4 px gap. The handle-gap is smaller than the
inter-label gap (8 px) since dots are smaller than pills — keeps the
result visually tight.

Per-anchor exception: the handle sitting at a label's own anchor
(ellipse / circle / angle anchors are at the primary handle disk) is
skipped, otherwise the resolver would chase the label arbitrarily far
from its own measurement. The per-type `offsetY` already places the
pill clear of that one specific handle.
2026-05-01 00:34:00 +02:00
Samuel Prevost
6dc5454d46 refactor(crop): tighten architecture + fix zoom-after-crop
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Architectural debt addressed in one pass:

- CropViewer drops its five local refs (rotationDeg, cropLeft/Top/Right
  /Bottom) in favour of computed views over `store.cropRotate`. Drag and
  rotation handlers write straight to the store via thin setters; the
  store is now the canonical source of truth. localStorage is flushed on
  commit boundaries (drag end, rotation change, unmount, Next), not on
  every pointermove.
- `ImagePreTransform` deleted from `src/types/index.ts`. CorrectedImage-
  Viewer now takes `crop?: { state: CropRotateState; srcW; srcH }` and
  derives the pixel-space affine internally via a memoised `computed`.
  Cuts the second reactive ref in MeasureViewer and removes the
  redundant "type that exists only to ferry derived numbers across a
  prop boundary."
- `crop-transform.ts` is now pure geometry. The DOM-bound
  `renderRotatedCropped` moved to a new `src/lib/crop-render.ts`. The
  pure module is now safe to import from a worker / test without a
  canvas mock.

Bug fix shipped in the same commit because it fell out of the same
analysis:

- Zoom (wheel + pinch) anchored on a measurement-space point returned
  by `screenToImg`, but `viewOffset = screen - bitmap * scale` expects a
  bitmap-space point. With identity crop the two were equal, so the bug
  was invisible until the user cropped — at that point each zoom step
  shifted the image far off-screen ("canvas goes blank/black"). Pan was
  fine because it never converted screen↔image. Fix: a tiny
  `screenToBitmap` helper used by `onWheel` and the pinch branch of
  `onTouchMove`. Measurement placement / drag still go through the
  full `screenToImg` so coords stay in the canonical frame.
2026-05-01 00:29:24 +02:00
Samuel Prevost
9f54bc62bd fix(crop): persist on unmount + handle image-load failure
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Two follow-up fixes flagged by the post-merge review:

- persist() on `onUnmounted` so a mid-drag navigation (Back button,
  hot-reload) doesn't lose the in-progress crop. The local refs
  already hold the latest coords; we just need to flush them through
  to the store + cache before the component goes away.
- Add `el.onerror` to the image load and a small fallback UI offering
  Back-to-Deskew. Without it, a corrupt deskew blob would leave the
  canvas permanently blank with no way to recover short of refresh.
2026-05-01 00:14:20 +02:00
Samuel Prevost
565baddfbf feat(crop): rotate + crop step between deskew and measure
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2026-05-01 00:12:11 +02:00
Samuel Prevost
415058d7d8 fix(datums): dot-decimal inputs, gate Next on validity
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Dimension inputs (width, height, length, diameter) were `type="number"`,
which let some browsers reinterpret comma-locale input ("21,5") as 21.5
under the hood. Switch to `type="text" inputmode="decimal"` and parse
strictly with `Number()` so only dot-decimal ("21.5") is treated as the
intended fraction; "21,5" produces NaN and is flagged.

A small per-field raw-input buffer remembers the literal string the user
typed so an invalid intermediate state ("21,") doesn't get rendered as
"NaN" on the next reactive pass. The buffer is dropped automatically
when the stored value diverges from `Number(buffered)` — i.e. when an
external mutation (preset button, rect-dim swap) changes the datum.

Visual: invalid fields get a red border + ring. Functional gate:
`canProceedToStep4` now also checks `Number.isFinite(...)` so a NaN
dimension prevents Next from being clickable, matching the user's
"prevent Next unless it's a number" requirement.
2026-05-01 00:00:29 +02:00
Samuel Prevost
23d3297434 fix(datums): tighten primary-flag handling
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
- setAxisRole: gate the "clear other datums' flags" loop on
  role !== null. Previously, calling setAxisRole(id, null) — e.g.
  clicking "None" on an unflagged line button — would silently strip
  the primary off whichever rect/ellipse legitimately held it. The
  docstring's "no-op if it wasn't set" promise now actually holds.
- setAxisRole: replace the bare else in the null branch with an
  explicit `else if (target.type === "ellipse")` so a future datum
  type added to the union can't quietly inherit isPrimary.
- pickPrimary: extract `flaggedPrimary(d)` so the user-flag check
  reads as "is this datum flagged?" rather than implying a check
  order across types. Adds a comment documenting that mutual
  exclusion is enforced by setAxisRole and that bypassing the store
  would fall back to array-order tie-breaking.
2026-04-30 23:52:25 +02:00
Samuel Prevost
c2f7bf0df2 feat(datums): make ellipses user-flaggable as the primary datum
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Previously only rectangles (`isAxisReference`) and lines (`axisRole`)
could be flagged as the gauge primary; ellipses fell through to the
auto-pick by type rank. Add `EllipseDatum.isPrimary` so the user can
pick a circle/ellipse as the primary datum and override the type-rank
heuristic. Mutual exclusion with the existing rect/line flags is
enforced through `setAxisRole`. Adds a "Primary reference" toggle in
the ellipse datum row and a "primary" badge next to the datum name so
the active primary is visible at a glance.
2026-04-30 23:39:26 +02:00
Samuel Prevost
590ba16596 feat(measurements): rect click-through, color-as-selection, fullscreen, label clamping
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
- Rectangles now follow the same body-click rule as circles: corner dots
  drag, body (rim + interior) selects when no tool is active and falls
  through to placement when one is. Lets the user draw measurements
  inside an existing rectangle.
- Selected shapes keep their own colour for the geometry stroke instead
  of swapping to white. Unselected shapes are dimmed to 0.45 alpha so
  the selected one still stands out, and the colour stays consistent
  with the label pill — easier to pair shape ↔ label.
- Label clearance: anchors that coincide with a handle (ellipse, circle,
  angle) now use a larger Y offset so the pill clears the handle disk.
  Inter-label gap bumped from 4 to 8 px.
- Export labels are clamped to the destination canvas after collision
  resolution so full-resolution exports never clip a pill off the edge.
- Fullscreen toggle in the toolbar (icon + text). Esc exits. Renders the
  viewer as a fixed-position overlay; the existing ResizeObserver picks
  up the new container and re-fits the image automatically.
2026-04-30 23:34:24 +02:00
Samuel Prevost
8c7f4078df License
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2026-04-29 12:54:24 +02:00
Samuel Prevost
cb576b603f docs: refresh README with current pipeline + accurate license
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
The "How it works" steps now match the five-stage Upload → EXIF →
Datums → Deskew → Measure flow (the old README still described a
four-step pipeline ending at "Run correction"). The algorithm section
is rewritten to describe what the solver actually does today: primary-
gauge selection, the alternating-minimization loop around findHomography,
the per-type correspondence builders (rectangle/line/ellipse),
confidence-as-replication weighting, oscillation detection, output
bounds clamp, and per-datum residual reporting.

Adds the three example tiles from the upload page, fixes the presets
list to mention circles, and corrects the license to match the GPLv3
LICENSE file already in the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:49:09 +02:00
Samuel Prevost
e56ee9611d fix(mobile): stack header, declutter stepper, and reflow chrome
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
The header now stacks on mobile: title row up top, stepper on its own
row below, and the theme toggle moves into the footer's right edge so
it isn't competing for space with the title or stepper. Stepper labels
drop the leading "1." prefix (saves ~8ch across five steps) and the
mobile container has overflow-x-auto so very narrow screens scroll
instead of breaking layout.

Clear-cache button on the upload card collapses to icon-only on mobile
so the centered "Load Source Image" title doesn't sit underneath it;
the confirm state still reads "Sure?" so the destructive prompt is
visible at a glance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:57:25 +02:00
Samuel Prevost
9032af426e feat(upload): recent-uploads gallery + per-image zoom restore
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Past uploads now persist to IndexedDB (originalBlob, exif, and — once
deskew has run — the corrected blob, diagnostics, and output scale).
The upload page renders a tile grid for any entry that completed a
deskew; clicking one rehydrates the store from the cached artefacts
and drops the user straight back into Measure with their datums and
measurements intact. The section is hidden entirely when there are no
completed entries, and each tile has a hover-only delete affordance.

Canvas zoom + pan are now per-image, persisted to localStorage with a
short debounce. Re-running the deskew at a different scale clears the
saved zoom (the stale offsets would point off-image at the new
dimensions).

Other tweaks bundled here:
  • Start Over collapses the previous "Start over | New Image" group
    into a single dashed/transparent button.
  • The Measure header gets md:pl-5 so the title clears the fork-me
    ribbon on desktop.
  • Example images on the landing page swap to the IMG_8324 set, with
    a third "Measured" tile spanning the combined width that shows the
    annotation-baked output.
  • Clearing the cache now also wipes the upload + zoom caches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:05:18 +02:00
Samuel Prevost
a5f4bf650c feat(measure): split toolbar into back · downloads · start-over zones
Back collapses to a leading arrow icon, downloads stay in the middle,
and "New Image" moves to the far right behind a labeled "Start over"
separator. The reset button now uses a dashed transparent outline so
it reads as a deliberate, low-frequency action and is harder to hit
while reaching for a download.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:54:00 +02:00
Samuel Prevost
ed4da082ce feat(upload): move clear-cache to top-right with two-step confirm
The cache button now sits in the corner of the upload card so it's out
of the way of the drop zone. First click swaps the label to "Are you
sure?", second click within 4s clears; otherwise the prompt reverts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:50:16 +02:00
Samuel Prevost
1118de74da feat(pipeline): split Deskew + Measure into separate steps
Step 4 (was "Result") now only runs the perspective correction and shows
diagnostics + a small preview at the standard column width. Step 5
("Measure") is a new full-bleed view dedicated to annotation, with the
download buttons promoted to the top of the page.

Re-running the deskew at a different output px/mm now rescales any
measurements already saved for the image (cached by file hash) so they
stay anchored to the same physical features instead of drifting.

Theme tweaks: card surfaces are now visibly distinct from the page
background in light mode, and dark mode is a touch lighter than the
previous near-black. The "Made by" footer is pinned to the bottom of
the viewport via fixed positioning, with corresponding pb on <main>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:46:19 +02:00
Samuel Prevost
b28ffe267b feat(measurements): smarter labels, 45° snap, and circle click-through
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
- Labels go through a collision-resolver before painting: greedy
  top-to-bottom placement pushes overlapping pills downward, and labels
  shifted significantly draw a dashed leader line back to their anchor
  in the measurement's color. Same two-pass order is used by the live
  overlay and the annotated exports.
- Unselected dark label pills now have a colored border matching the
  measurement, so a label can be paired to its geometry without relying
  on selection state.
- New "Snap 45°" toolbar checkbox: when on, line endpoints and angle
  arms snap their direction to multiples of 45° (relative to the fixed
  endpoint or angle vertex) during placement and during handle drag.
  Length is preserved.
- Circles: only the center and edge handles drag. Body / rim / label
  clicks select-only, and when a placement tool is active they fall
  through entirely so the user can draw a new measurement on top of an
  existing circle.
2026-04-25 11:58:42 +02:00
Samuel Prevost
9c47736799 feat(measurements): add circle tool, annotated exports, and per-image persistence
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Three combined additions to the corrected-image viewer:

- Circle measurement tool: 2-click placement (center + edge), dedicated
  hit-test, handle drag preserving radius when grabbing the center.
- Annotated PNG exports via two new buttons in the result page:
  "Download full + measurements" (source resolution, strokes scaled up
  to read at the same visual weight) and "Download view + measurements"
  (current pan/zoom). Both respect the existing scale-bar toggle; the
  view export's bar is sized for canvas-px/mm = image-px/mm × view scale.
- Per-image measurement persistence keyed by file hash, mirroring the
  datum cache. "Clear cache" in the upload step now wipes both.

Drawing helpers were refactored to take a RenderCtx (transform +
strokeMul + handle/decoration flags) so the same code paths handle live
overlay and offscreen export.
2026-04-25 11:07:28 +02:00
Samuel Prevost
93b05f554c feat(result): full-bleed corrected-image card
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
The corrected-image card was inscribed in the same max-w-4xl column as
the controls, which capped the canvas at ~896px even on wide displays.
Wrap just that one card in a full-bleed shell (relative left-1/2 -tx-1/2
w-screen) so it spans the viewport width while leaving the surrounding
controls/diagnostics/download in their narrower column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:27:44 +02:00
Samuel Prevost
f3411a14bd fix(measurements): rectangle stays a rectangle on corner drag
Dragging a corner used to move only that corner, turning the rect into
an arbitrary quad. Constrain to axis-aligned: dragged corner follows
the cursor, the diagonally-opposite corner stays put, the two adjacent
corners are recomputed from the cross of (dragged.x, opp.y) and
(opp.x, dragged.y) so the shape stays rectangular. Corner indices stay
stable (c0 is still the logical TL even if the box flips through
itself), so handle keys keep tracking the same corner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:23:45 +02:00
Samuel Prevost
bb8ea1929f fix(measurements): grabbing existing handles wins over placement tools
Mirror DatumCanvas/Konva: hit-test on every mousedown/touchstart, and
let an existing-measurement hit win over both stage pan and placement
tools. Empty-space presses fall through — to a placement (if a tool is
active) or to a pan.

Suppress the trailing click event when the press picked up a prior
measurement so the placement tool doesn't commit a spurious new point
right after a corner drag ends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:19:53 +02:00
Samuel Prevost
c6249aad5d fix(measurements): drag survives leaving the canvas
Drag was wired to canvas-level mousemove/mouseup, so any time the
cursor left the canvas (or a fast drag outpaced the canvas-bound event
firing) the drag died silently. Also onMouseLeave ended the drag, which
made off-edge nudges feel like they "didn't work".

Move the live mousemove/mouseup listeners to the window for the
duration of a press, leaving onMouseLeave to clear only the placement
preview. Window listeners are attached on the press and detached on
release (and on unmount, in case the user navigates away mid-drag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:13:09 +02:00
Samuel Prevost
f1d32d0fb2 fix(measurements): handles drag on the first pixel, like datum view
Handles previously had a 3 px screen-space dead zone before drag started
(the click-vs-drag heuristic). On a precision-positioning tool that
makes nudges feel mushy, and the datum editor — which uses Konva drag
— has no such threshold. Drop the threshold; selection still happens
on pointerDown so a pure click that doesn't move never enters
pointerMove and the position never drifts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:07:44 +02:00
Samuel Prevost
a499e97361 feat(measurements): add rectangle tool
Fourth measurement type alongside line/ellipse/angle. 2-click placement
(opposite corners → axis-aligned), corners normalised to TL/TR/BR/BL on
commit then free to drag individually so the user can make the quad
non-rectangular if they need to. Index ordering stays stable across
drags — same as the rect *datum* in step 3.

Hit test: 6 px tolerance on each of the 4 edges, plus an interior point-
in-polygon fill so a big rect can be grabbed anywhere. Handles still
win priority over the geometry fill so a corner grab always beats a
whole-rect grab.

Label at the centroid: `w × h mm · area mm²`. Width/height are means of
opposing edges so reshape doesn't make readings jump; area is computed
with the shoelace formula and survives non-rectangular drags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:04:27 +02:00
Samuel Prevost
923e969bdf chore(logo): replace squirrel SVG with the 📐 emoji
SkwikLogo renders 📐 in an inline span sized via the existing `size`
prop (font-size = round(size * 0.9)). Favicon becomes an SVG with a
single <text> element rendering the same emoji.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:04:00 +02:00
Samuel Prevost
fe61ba3cf2 fix(result): default auto-scale targets a 2000px output
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Previously the auto-scale was just the source-image px/mm of the chosen
reference datum, which produced enormous outputs for high-res photos.
Pick the output px/mm so the longer side of the warped image is roughly
2000 px; floor to int (the input is integer-only); clamp at 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:57:52 +02:00
Samuel Prevost
e94a814335 fix(measurements): row is a div, not a nested button
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
The side-panel measurement rows were <button> with a nested <button> for
delete, which HTML doesn't allow and Vite was warning about. Demote the
row to a focusable div with role=button + tabindex=0 + Enter/Space
handlers; keep the inner delete <button> with type=button. Same
keyboard a11y, no nesting violation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:44:10 +02:00
Samuel Prevost
497e71d63c feat(measurements): larger canvas, always-visible handles, fewer clicks
- Canvas fills the viewport (h-[calc(100vh-12rem)] on desktop, -14rem on
  mobile), matching the datum-editor precedent. The side panel tracks
  the same height so both read as equal-height siblings.
- Handles are now visible on every measurement, not just the selected
  one. Unselected: small (3px), low-alpha, faint white ring. Selected
  endpoints: 6.5px with a thick ring. Selected primary handles (ellipse
  center / angle vertex): 8px. The invisible grab radius is 14px so the
  tiny unselected dots are still easy to target.
- Selected handles keep their palette color (previously they went white
  along with the lines, so a selected handle disappeared on light
  backgrounds). Matches DatumCanvas's look.
- Hit-test priority is explicit: handles beat geometry, so a precision
  grab on an endpoint always wins over a line-body drag — including on
  an unselected measurement, which promotes to select-and-drag in a
  single gesture.
- Removed the on-canvas delete button next to the selected label. The
  side-panel row × and Delete/Backspace still work. HitResult.kind
  drops its "delete" variant; the matching draw + dispatch blocks and
  the dedicated hit region are gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 18:22:56 +02:00
Samuel Prevost
e07ee9d204 fix(solver): ellipse diagnostic used wrong H direction
residualForEllipse computed C = H^T·E·H, but H maps image → output, so
the correct output-space conic is C = H^{-T}·E·H^{-1}. The forward form
represents a geometrically meaningless conic and produced nonsensical
numbers in the per-datum table (a 210mm circle was reported as 2046mm).
The deskewed image itself was correct — only the diagnostic was wrong,
because these residuals are display-only and don't feed back into the
solver. Now invert H once and compute the conic in output space.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 18:22:42 +02:00
Samuel Prevost
da5be3851d feat: world-axis selector, 8-point circle, annotated measurement tool
Datum editor (step 3):
- Add world-axis role to rectangles (isAxisReference) and lines
  (axisRole: "x"|"y"). Exclusive via a new store action that clears
  any other axis flag on write. The solver's pickPrimary now honors an
  explicit user flag ahead of the type-priority fallback; line-primary
  correspondences target world +x or +y depending on the flag.
- Panel UI: checkbox on rect cards, three-way button row on line cards,
  and an axis badge in each card header.
- Ellipse datum switches to 8 user-placed points on the circle contour.
  New src/lib/ellipse-fit.ts does an algebraic LSQ conic fit (data-
  normalised, 5x5 Gaussian solve, f=-1 constraint) and returns the
  geometric center + perpendicular conjugate semi-axes, which we cache
  on the datum for the solver and renderer. Dragging any handle
  refits; an extra center handle translates all 8 points together.
  datum-cache migrates legacy 3-handle storage by synthesising 8
  samples from the old parametric form.
- ResultViewer auto-scale now floors to an int to match the integer-
  only scale input (step=1).

Measurement tool (step 4) — CorrectedImageViewer.vue:
- Three measurement tools: line (length), ellipse (semi-axes + area),
  angle (0-180 degrees between two rays).
- Persistent, multi-measurement state. Each has id, colorIndex, and
  type-specific geometry; colors cycle via the existing getDatumColor
  palette with a monotonic counter so deletion doesn't recolor.
- Selection model with hit-testing on handles, geometry, and labels.
  Selected draws on top in white; others render dashed with 0.8/0.5
  alpha so the active measurement pops.
- Dragging geometry or label moves the whole measurement; dragging a
  handle reshapes just that handle. 3px mouse threshold distinguishes
  click from drag.
- Side panel lists measurements with color chip, type, value, and a
  delete button; clicking selects on canvas. Delete/Backspace deletes
  the selected measurement. Escape cancels in-progress placement.
- Live placement preview + inline hint strip describes what the next
  click does. Pinch-zoom and single-finger pan still work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 18:10:22 +02:00
Samuel Prevost
b87f933b9e feat(solver): iterative homography solver with circle datums
Replace the two-pass closed-form deskew (getPerspectiveTransform +
per-axis scale corrections) with an alternating-minimisation loop around
cv.findHomography (internal Levenberg–Marquardt). Each outer iteration
recomputes per-datum point correspondences from the current H and the
datum's shape constraint, then findHomography refines H. Confidence
drives per-correspondence replication; primary gets a 3× gauge boost.

- Add EllipseDatum type (center + two conjugate semi-axis endpoints +
  known diameter) with 3-handle Konva rendering and coin presets.
- Generalise primary selection to any datum type. Priority rect >
  ellipse > line; within type, confidence then image size. Warm-start
  anchors: rect = 4 axis-aligned corners; ellipse = 4 conjugate-axis
  samples on a world circle; line = 2 endpoints + 2 synthetic
  perpendicular points (isotropic image-scale assumption).
- Direction-agnostic shape residuals: Procrustes-fit ideal (w × h) rect
  to projected corners; midpoint-preserving line rescale; radial-snap
  ellipse samples to a circle at projectPoint(H, center).
- Drop the "at least one rectangle" requirement. Any datum combination
  works; diagnostics widgets auto-pick a scale reference across types.
- Diagnostics: replace X/Y axis-correction cards with RMS residual +
  iteration count; per-datum table shows a residual breakdown column
  (edge %, perp Δ°, iso/skew/dia).
- Detect period-2 oscillation in the outer loop and warn to console.
- Relative convergence threshold so the affine and perspective entries
  of H are weighted comparably.
- Guard diagnostic diameter via geometric-mean-radius for non-circular
  conics; guard collinear-axes ellipses; fix Mat leak in
  solveHomography on the exception path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:42:40 +02:00
Samuel Prevost
a71c8c73ef feat(datums): broaden image upload, swap W/H, validate rect corners
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
- Accept any browser-supported image type (png, webp, gif, …) in addition
  to jpg and heic on the upload step.
- Add a swap button between the width and height inputs on rectangle
  datums so users can flip dimensions with one click.
- Block the Next action on the datum step when a rectangle has crossed
  corners (top below bottom or right left of left), and surface which
  datums need fixing in the tooltip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 20:39:37 +02:00
Samuel Prevost
27b23a61d9 docs: add comparison table to README
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Highlight what sets Skwik apart from existing perspective-correction
and measurement tools: client-side, multi-datum weighting, mm scale,
measurement tools, and scale bar export all in one.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-16 17:59:01 +02:00
Samuel Prevost
03d0f38476 fix(result): fix scale bar export, scale input UX, and auto-scale
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
- Fix scale bar checkbox: replace shadcn Checkbox (broken event
  propagation) with native input[type=checkbox] + v-model
- Scale input: use local string ref so user can type freely;
  red highlight when invalid, run button disabled until valid
- Auto-compute default scale to match input image dimensions,
  capped at 8192px output; cached scale takes priority

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:59:36 +02:00
Samuel Prevost
e72c4bc89b fix(canvas): fix mobile touch interactions and desktop panning
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
- Mobile: fix pinch-zoom jitter by disabling Konva stage drag during
  two-finger gestures so only our zoom handler controls position
- Mobile: fix dot dragging — defer pan start so Konva can claim touch
  for shape drag first (isDraggingShape flag)
- Desktop: enable stage drag for click-drag panning, disable during
  point drag; filter dragend by nodeType to prevent offset corruption
- Fallback file hash using name+size+lastModified when crypto.subtle
  is unavailable (HTTP contexts, some mobile browsers)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:51:29 +02:00
Samuel Prevost
f3d065e610 ci: add GitHub Actions workflow for GitHub Pages deployment
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
- pnpm 10 + Node 22 for fastest CI
- Frozen lockfile, upload-pages-artifact v3, deploy-pages v4
- Vite base path set to /skwik/ when building in GHA
- Concurrency group cancels in-progress deploys

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:28:36 +02:00
Samuel Prevost
bf20518083 chore: add .claude/ to gitignore
Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:25:59 +02:00
Samuel Prevost
9e3cf6fd67 chore: add compressed example before/after images
Resized to ~480px max dimension, JPEG q80 (~128KB total).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:24:14 +02:00
Samuel Prevost
23fecfb738 fix(nav): make step indicators clickable with real button elements
Badge Primitive (<span>) was swallowing clicks. Reachable steps now
render as <button> with hover effects, current/unreached as Badge.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:24:09 +02:00
Samuel Prevost
98c6fc9a35 feat(ui): squirrel logo, fork ribbon, clickable steps, and polish
- Squirrel engineer logo (SkwikLogo.vue) with hard hat and ruler
- Matching favicon with squirrel head silhouette
- Gitea fork ribbon (top-left, desktop only, Gitea green)
- Centered header with logo, title, and subtitle
- Footer: "Made by Samuel Prevost" with GitHub link
- Clickable step indicators for previously visited steps
- Smaller datum dots (6/4 base radius with visual cap)
- Engineering-tool styling: monospace for measurements, Geist Mono
  font, deeper dark mode colors, instrument-panel header
- EXIF viewer explains why focal length matters
- Upload page describes what Skwik does

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:19:44 +02:00
Samuel Prevost
11e8013b6a feat(cache): persist datums per file hash and user settings
- Add file-hash.ts: SHA-256 hash of uploaded files via Web Crypto API
- Add datum-cache.ts: localStorage save/load/clear for datums by hash
- Add settings-cache.ts: persist scalePxPerMm and includeScaleBar
- Restore datums from cache on re-upload of same file
- Discreet "Clear cache" button on upload page
- Store fileHash and cacheRestoreMessage in Pinia store
- Auto-save datums on every change via deep watcher
- Track maxStepReached for clickable step navigation

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:19:34 +02:00
Samuel Prevost
0cb9009eaa feat(result): add measurement tools, grid overlay, and scale bar export
- New CorrectedImageViewer component with dual-canvas (image + overlay)
- Point-to-point measurement tool: click two points, see mm distance
- Toggleable grid overlay with configurable spacing and major lines
- Scale bar export: appends measurement bar to downloaded PNG
- Progress bar with step labels during algorithm execution
- Estimated output size shown before running, blocks if > 512MB
- Actual output dimensions/filesize shown in diagnostics
- Filename changed to originalname-skwik.png
- Collapsible algorithm explanation with numbered steps
- Process New Image button to reset and start over
- Scale bar tooltip explaining the feature
- Add shadcn-vue Checkbox component

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:19:25 +02:00
Samuel Prevost
1bc1f46bb8 feat(deskew): add debug logging, progress callbacks, and WASM safety
- Add step-by-step console logging throughout the algorithm
- Add onProgress callback for UI progress bar integration
- Fix WASM OOM: clamp output dimensions with proper matrix scaling
  (previously clamped size but not the transform, causing cropping)
- Fix waitForOpenCV race condition: probe cv.Mat() instead of
  checking constructor existence
- Wrap all OpenCV mats in try/finally for guaranteed cleanup
- Raise MAX_OUTPUT_DIM to 12288 for more leniency

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:19:16 +02:00
Samuel Prevost
3e0284da4c Add README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:19:10 +02:00
Samuel Prevost
4069491c2f Implement real deskew algorithm and UI improvements
- Replace placeholder with OpenCV.js WASM perspective correction:
  pick highest-confidence rectangle, compute homography, fold
  weighted scale corrections from secondary datums, single warpPerspective
- All units now mm throughout (no cm conversion)
- Simplified datum creation: two buttons (+ Rectangle / + Line) with
  preset chips, auto-numbered labels (Line 1, Rectangle 2, etc.)
- Dimensions default to 0, user must input manually; Next button
  disabled until all datums have valid dimensions with tooltip hint
- Fix image preview (keep object URL alive), fix canvas disappearing
  on breakpoint switch (single instance + ResizeObserver re-fit)
- Mobile responsive: bottom sheet for datum panel, full-width canvas
- Spinner on result screen during processing
- Stricter ESLint config, updated Prettier to 4-space/no-semicolons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:15:53 +02:00
Samuel Prevost
2d56c5dada Initial commit: Skwik image deskew tool
Vue 3 + Vite + TypeScript (strict) app with shadcn-vue, Konva.js canvas,
and Pinia. 4-step wizard: upload JPG/HEIC, view EXIF, place datum
measurements (rectangles/lines with presets), run deskew (placeholder).
Dark mode, mobile-responsive with bottom sheet for datum panel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:53:00 +02:00