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.
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).
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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):{tx, ty, scale}(scale = px per cm);zoomToFitRect(rect, viewportSize, padding)→ target{tx, ty, scale};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) viaObjectShape.web/src/editor/useViewport.tswith@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), animatedzoomToFit.touch-action: noneon the svg so the browser doesn't fight the gestures.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
scaleeasily readable by markers.Key files & patterns
DESIGN.md§ Geometry (center-based positions, two nesting levels).DESIGN.md§ Frontend layout.Dependencies
Blocked by #2.
Acceptance criteria
npx vitest rungreen on geometry (round-trips, rotation, zoomToFit math, unit formatting).zoomToFitanimates to frame a given bed.