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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
- 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>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
- 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>
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>