From 677dbf075675b5158c13dd7f5abb2c6c3905b68a Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Sun, 19 Jul 2026 03:06:42 -0400 Subject: [PATCH] Address Gadfly review: gofmt, memoize bed grid, unify clamp - gofmt domain.go struct alignment. - Memoize the bed grid line arrays so a high-frequency plop drag doesn't rebuild them each pointermove. - Route the non-snapping placement/move paths through snapLocalToBedGrid with step 0 (pure clamp), removing the duplicated inline clamps in GardenCanvas.onPlace and PlopOverlay (drop the dead clampLocal helper). - Factor snapLocalToBedGrid's per-axis snap-and-clamp into one local fn. - Correct the gardenFromInput doc: grid size is defaulted from 0 on update. - Fix inconsistent indentation of the Inspector Notes field. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi --- internal/domain/domain.go | 20 +++++++++--------- internal/service/gardens.go | 4 +++- web/src/editor/GardenCanvas.tsx | 36 +++++++++++++++++++-------------- web/src/editor/Inspector.tsx | 16 +++++++-------- web/src/editor/PlopOverlay.tsx | 10 +++------ web/src/lib/geometry.ts | 12 +++++------ 6 files changed, 51 insertions(+), 47 deletions(-) diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 2c2a327..1183185 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -120,13 +120,13 @@ type Session struct { // Garden is a planning surface owned by one user, at real-world scale (cm). type Garden struct { - ID int64 `json:"id"` - OwnerID int64 `json:"ownerId"` - Name string `json:"name"` - WidthCM float64 `json:"widthCm"` - HeightCM float64 `json:"heightCm"` - UnitPref string `json:"unitPref"` - Notes string `json:"notes"` + ID int64 `json:"id"` + OwnerID int64 `json:"ownerId"` + Name string `json:"name"` + WidthCM float64 `json:"widthCm"` + HeightCM float64 `json:"heightCm"` + UnitPref string `json:"unitPref"` + Notes string `json:"notes"` // GridSizeCM is the garden-scale grid spacing (cm) the editor draws its grid // from; SnapToGrid, when true, snaps objects to that grid on place/move/resize. GridSizeCM float64 `json:"gridSizeCm"` @@ -183,9 +183,9 @@ type GardenObject struct { GridSizeCM float64 `json:"gridSizeCm"` SnapToGrid bool `json:"snapToGrid"` Notes string `json:"notes"` - Version int64 `json:"version"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + Version int64 `json:"version"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } // Plant is a catalog entry. OwnerID nil means a read-only seeded built-in. diff --git a/internal/service/gardens.go b/internal/service/gardens.go index d062889..460b3d7 100644 --- a/internal/service/gardens.go +++ b/internal/service/gardens.go @@ -166,7 +166,9 @@ func (s *Service) DeleteGarden(ctx context.Context, actorID, gardenID int64) err // gardenFromInput validates and normalizes input into a domain.Garden (without // identity/ownership fields). With applyDefaults, absent dimensions/unit are -// filled in (create); without it, every field must be supplied (update). +// filled in (create); without it, dimensions and unit must be supplied (update). +// The grid size is the exception: 0 always means "unset" and is defaulted on both +// paths, so an older client that omits it can't wipe the column to an invalid 0. func gardenFromInput(in GardenInput, applyDefaults bool) (*domain.Garden, error) { name := strings.TrimSpace(in.Name) if name == "" || len(name) > maxGardenNameLen { diff --git a/web/src/editor/GardenCanvas.tsx b/web/src/editor/GardenCanvas.tsx index d7ae5ac..90be8f6 100644 --- a/web/src/editor/GardenCanvas.tsx +++ b/web/src/editor/GardenCanvas.tsx @@ -157,15 +157,14 @@ export function GardenCanvas({ if (!rect) return const world = screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top }, viewport) const local = worldToLocal(world, { x: focusedObject.xCm, y: focusedObject.yCm }, focusedObject.rotationDeg) - const halfW = focusedObject.widthCm / 2 - const halfH = focusedObject.heightCm / 2 - // Snap the plant to the bed's grid when the bed opts in; snapLocalToBedGrid - // also clamps, so the plain clamp covers the non-snapping case. - const placed = focusedObject.snapToGrid - ? snapLocalToBedGrid(local, focusedObject.gridSizeCm, halfW, halfH) - : { x: Math.max(-halfW, Math.min(halfW, local.x)), y: Math.max(-halfH, Math.min(halfH, local.y)) } - const x = placed.x - const y = placed.y + // snapLocalToBedGrid clamps to the bed either way; step 0 (snapping off) makes + // it a pure clamp, so both paths go through one helper. + const { x, y } = snapLocalToBedGrid( + local, + focusedObject.snapToGrid ? focusedObject.gridSizeCm : 0, + focusedObject.widthCm / 2, + focusedObject.heightCm / 2, + ) const radiusCm = Math.max(1.5 * armedPlant.spacingCm, 15) // Stay armed for repeat-placement; don't select (the placement sheet covers // the object, so a selection would be hidden until placement ends anyway). @@ -178,9 +177,16 @@ export function GardenCanvas({ // Draw the bed's own grid inside a focused plantable bed that has snapping on, // so the user sees exactly where plants will land. Lines are in the bed's local // frame (origin at center), anchored to the corner to match snapLocalToBedGrid. - const showBedGrid = !!focusedObject && focusedObject.plantable && focusedObject.snapToGrid - const bedVLines = showBedGrid ? bedGridLines(halfFW, focusedObject.gridSizeCm) : [] - const bedHLines = showBedGrid ? bedGridLines(halfFH, focusedObject.gridSizeCm) : [] + // Memoized so a high-frequency plop drag (which re-renders the canvas on every + // pointermove) doesn't rebuild the line arrays each frame. null when the focused + // object isn't a snapping plantable bed. + const bedGrid = useMemo(() => { + if (!focusedObject || !focusedObject.plantable || !focusedObject.snapToGrid) return null + return { + v: bedGridLines(focusedObject.widthCm / 2, focusedObject.gridSizeCm), + h: bedGridLines(focusedObject.heightCm / 2, focusedObject.gridSizeCm), + } + }, [focusedObject]) return (
@@ -210,12 +216,12 @@ export function GardenCanvas({ ))} - {showBedGrid && focusedObject && ( + {focusedObject && bedGrid && ( - {bedVLines.map((x) => ( + {bedGrid.v.map((x) => ( ))} - {bedHLines.map((y) => ( + {bedGrid.h.map((y) => ( ))} diff --git a/web/src/editor/Inspector.tsx b/web/src/editor/Inspector.tsx index 8582553..ed3f293 100644 --- a/web/src/editor/Inspector.tsx +++ b/web/src/editor/Inspector.tsx @@ -263,14 +263,14 @@ export function Inspector({
)} -