Canvas foundation: SVG viewport, pan/zoom/pinch, geometry lib #9

Closed
opened 2026-07-18 18:16:54 +00:00 by steve · 0 comments
Owner

Part of epic #20 · Design: DESIGN.md

Context

The heart of pansy is a landscaping-style 2D editor: an SVG canvas showing a garden at real-world scale, with smooth pan/zoom on desktop and touch. This issue builds that foundation against mock data — the viewport, the gesture handling, and the pure geometry library — so #11 (field editor) and #15 (plops) only add interactions on top. This is the technically riskiest part of the project; front-load the gesture work and verify on a real phone.

Scope

  • web/src/lib/geometry.ts (pure functions + vitest tests):
    • world↔screen transforms for viewport {tx, ty, scale} (scale = px per cm);
    • object-local↔garden-world transforms (object center + rotation_deg; plops live in the local frame);
    • zoomToFitRect(rect, viewportSize, padding) → target {tx, ty, scale};
    • unit formatting: cm→"1.2 m" / cm→"4 ft 0 in", ft/in string→cm.
  • web/src/editor/GardenCanvas.tsx: full-viewport <svg>, garden boundary rect, single <g transform="translate(tx,ty) scale(s)"> world group, light background grid (fades by zoom), renders a hardcoded mock garden (a few beds, a circular container, a path) via ObjectShape.
  • web/src/editor/useViewport.ts with @use-gesture/react: wheel zoom to the cursor, drag-pan on empty space, two-finger pinch zoom+pan on touch, scale clamped (~0.05–20 px/cm), animated zoomToFit. touch-action: none on the svg so the browser doesn't fight the gestures.
  • Zustand store (web/src/editor/store.ts): viewport + selection + focusedObjectId slots (selection logic itself comes in #11).
  • ObjectShape: renders rect/circle kinds with rotation, name label, kind-based default colors.

Out of scope

  • Server data, palette, move/resize/rotate (#11). Plops (#15). Semantic zoom bands (#15) — but keep scale easily readable by markers.

Key files & patterns

  • Coordinate rules: DESIGN.md § Geometry (center-based positions, two nesting levels).
  • Component map: DESIGN.md § Frontend layout.

Dependencies

Blocked by #2.

Acceptance criteria

  • npx vitest run green on geometry (round-trips, rotation, zoomToFit math, unit formatting).
  • Demo route with mock data: 60fps-feeling pan/zoom with mouse wheel + drag on desktop; pinch + drag on an actual phone (or precise DevTools touch emulation); wheel zoom keeps the point under the cursor stationary.
  • zoomToFit animates to frame a given bed.
  • Page never scrolls/bounces while gesturing on the canvas.
Part of epic #20 · Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md) ## Context The heart of pansy is a landscaping-style 2D editor: an SVG canvas showing a garden at real-world scale, with smooth pan/zoom on desktop and touch. This issue builds that foundation against **mock data** — the viewport, the gesture handling, and the pure geometry library — so #11 (field editor) and #15 (plops) only add interactions on top. This is the technically riskiest part of the project; front-load the gesture work and verify on a real phone. ## Scope - [ ] `web/src/lib/geometry.ts` (pure functions + vitest tests): - world↔screen transforms for viewport `{tx, ty, scale}` (scale = px per cm); - object-local↔garden-world transforms (object center + rotation_deg; plops live in the local frame); - `zoomToFitRect(rect, viewportSize, padding)` → target `{tx, ty, scale}`; - unit formatting: cm→"1.2 m" / cm→"4 ft 0 in", ft/in string→cm. - [ ] `web/src/editor/GardenCanvas.tsx`: full-viewport `<svg>`, garden boundary rect, single `<g transform="translate(tx,ty) scale(s)">` world group, light background grid (fades by zoom), renders a hardcoded mock garden (a few beds, a circular container, a path) via `ObjectShape`. - [ ] `web/src/editor/useViewport.ts` with `@use-gesture/react`: wheel zoom **to the cursor**, drag-pan on empty space, two-finger pinch zoom+pan on touch, scale clamped (~0.05–20 px/cm), animated `zoomToFit`. `touch-action: none` on the svg so the browser doesn't fight the gestures. - [ ] Zustand store (`web/src/editor/store.ts`): viewport + selection + focusedObjectId slots (selection logic itself comes in #11). - [ ] `ObjectShape`: renders rect/circle kinds with rotation, name label, kind-based default colors. ## Out of scope - Server data, palette, move/resize/rotate (#11). Plops (#15). Semantic zoom bands (#15) — but keep `scale` easily readable by markers. ## Key files & patterns - Coordinate rules: `DESIGN.md` § Geometry (center-based positions, two nesting levels). - Component map: `DESIGN.md` § Frontend layout. ## Dependencies Blocked by #2. ## Acceptance criteria - `npx vitest run` green on geometry (round-trips, rotation, zoomToFit math, unit formatting). - Demo route with mock data: 60fps-feeling pan/zoom with mouse wheel + drag on desktop; pinch + drag on an actual phone (or precise DevTools touch emulation); wheel zoom keeps the point under the cursor stationary. - `zoomToFit` animates to frame a given bed. - Page never scrolls/bounces while gesturing on the canvas.
steve added the frontenddrawing labels 2026-07-18 18:16:54 +00:00
steve closed this issue 2026-07-19 00:18:28 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/pansy#9