Build image / build-and-push (push) Successful in 24s
Fixes from the PR #29 adversarial review (considered; not graded). Performance / correctness - ObjectShape is memo'd, so a pan/zoom (which only mutates the world <g> transform) no longer re-renders every object. - 'circle' objects render as an <ellipse> (rx/ry), honoring both dims — a circle when width == height — instead of ignoring heightCm. - The 1 m grid now actually fades in (opacity ramps as cells grow past the visibility threshold), matching its description. - Unique SVG pattern id (useId) so multiple canvases can't collide. - The canvas re-fits when the garden id changes (not just once), so #11 switching gardens reframes. Robustness - geometry.zoomToFitRect takes rect size by magnitude; wheel/pinch ignore non-finite deltas; ObjectShape clamps dimensions to >= 0 (no invalid SVG). Maintainability - Renamed the Size params from `viewport` to `canvasSize` (4 models); moved easeInOutCubic/lerp into geometry.ts; renamed toLocal → clientToCanvas; named constants for the label/corner factors; DEFAULT_FILL const; EditorGarden.unitPref imports UnitPref; the Fit control uses the shared Button. focusedObjectId/plantable are intentionally reserved for #11/#15. tsc + 17 vitest tests green; re-verified in a browser (ellipses render, unique pattern id, wheel zoom + Fit). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi