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