Address Gadfly review on #9: memoize shapes, fades, unique ids, robustness
Build image / build-and-push (push) Successful in 24s
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
This commit is contained in:
+20
-9
@@ -68,6 +68,16 @@ export function clampScale(scale: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, scale))
|
||||
}
|
||||
|
||||
/** Linear interpolation. */
|
||||
export function lerp(a: number, b: number, t: number): number {
|
||||
return a + (b - a) * t
|
||||
}
|
||||
|
||||
/** Cubic ease-in-out for viewport tweens. */
|
||||
export function easeInOutCubic(t: number): number {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-scale the viewport while keeping the world point currently under
|
||||
* `screenPoint` stationary — the math behind wheel-zoom-to-cursor and pinch. The
|
||||
@@ -90,28 +100,29 @@ export function zoomViewportAt(
|
||||
}
|
||||
|
||||
/**
|
||||
* The viewport that frames `rect` centered in a viewport of `viewport` pixels,
|
||||
* The viewport that frames `rect` centered in a canvas of `canvasSize` pixels,
|
||||
* with `padding` px of margin on every side. Scale is clamped to [minScale,
|
||||
* maxScale] so a tiny/huge rect still lands in a sane zoom.
|
||||
* maxScale] so a tiny/huge rect still lands in a sane zoom. Rect dimensions are
|
||||
* taken by magnitude, so a degenerate (0/negative) size can't corrupt the math.
|
||||
*/
|
||||
export function zoomToFitRect(
|
||||
rect: Rect,
|
||||
viewport: Size,
|
||||
canvasSize: Size,
|
||||
padding: number,
|
||||
minScale: number,
|
||||
maxScale: number,
|
||||
): Viewport {
|
||||
const availW = Math.max(1, viewport.w - padding * 2)
|
||||
const availH = Math.max(1, viewport.h - padding * 2)
|
||||
const rectW = Math.max(rect.w, 1e-6)
|
||||
const rectH = Math.max(rect.h, 1e-6)
|
||||
const availW = Math.max(1, canvasSize.w - padding * 2)
|
||||
const availH = Math.max(1, canvasSize.h - padding * 2)
|
||||
const rectW = Math.max(Math.abs(rect.w), 1e-6)
|
||||
const rectH = Math.max(Math.abs(rect.h), 1e-6)
|
||||
const scale = clampScale(Math.min(availW / rectW, availH / rectH), minScale, maxScale)
|
||||
|
||||
const centerX = rect.x + rect.w / 2
|
||||
const centerY = rect.y + rect.h / 2
|
||||
return {
|
||||
scale,
|
||||
tx: viewport.w / 2 - centerX * scale,
|
||||
ty: viewport.h / 2 - centerY * scale,
|
||||
tx: canvasSize.w / 2 - centerX * scale,
|
||||
ty: canvasSize.h / 2 - centerY * scale,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user