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