Address Gadfly review on #15: real bugs + shared editor constants
Build image / build-and-push (push) Successful in 9s

Correctness / stuck-state:
- PlopInspector: the planted date can now be cleared (blur commits null instead
  of being blocked); radius/count reset to the current value on invalid input.
- applyPlantingPatch now applies removedAt too (completes the optimistic patch).
- PlopMarker computes the count live from the current radius, so the on-canvas
  near-zoom label updates while resizing (not just the inspector).
- GardenEditorPage clears focus when the focused object vanishes — deleted, or a
  stale ?focus id on load — so the canvas can't get stuck dimmed with no exit.
- useRemovePlanting adopts the 409 current row, so a stale-version soft-remove
  can be retried instead of looping.
- Placement is gated on plantable, so a deep-linked non-plantable focus can't try
  to POST a plop the server would 400.

Dedup / perf (introduced by this PR):
- editor/shared.ts: SELECT_COLOR, HANDLE_PX, MIN_RADIUS_CM, DIMMED_OPACITY, and
  an objectTransform() helper, now used by ObjectShape/SelectionOverlay/PlopLayer/
  PlopOverlay/PlopMarker/GardenCanvas (unified the transform string + the two
  different dim opacities).
- Dropped PlopMarker's always-false `dimmed` prop (dimming is at the group level).
- Build plantsById once in the page and pass it to GardenCanvas.
- Moved the livePlanting subscription into PlopInspector so a plop drag
  re-renders only the panel, not the whole page (matches the liveObject pattern).
- Memoized PlopLayer's plop grouping so it doesn't rebuild every pan/zoom frame.

Skipped: gesture-scaffolding hook extraction (risky churn in #11 code),
elliptical clamp for circle objects (consistent with the server's rect bounds),
optimistic create (matches useCreateObject).

tsc --noEmit clean; 24/24 vitest; production build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
2026-07-18 23:21:17 -04:00
co-authored by Claude Opus 4.8
parent 617dbfd703
commit 0f67623da7
10 changed files with 115 additions and 76 deletions
+2 -4
View File
@@ -2,13 +2,11 @@ import { useEffect, useRef, type PointerEvent as ReactPointerEvent, type RefObje
import { screenToWorld, worldToLocal, type Point } from '@/lib/geometry'
import { useUpdatePlanting } from '@/lib/objects'
import type { EditorPlanting } from '@/lib/plantings'
import { HANDLE_PX, MIN_RADIUS_CM, SELECT_COLOR, objectTransform } from './shared'
import { useEditorStore } from './store'
import type { EditorObject } from './types'
const HANDLE_PX = 12
const MIN_RADIUS_CM = 1
const MAX_RADIUS_CM = 10_000
const SELECT_COLOR = '#2f7a3e'
/**
* Move/resize handles for the selected plop, rendered inside its parent object's
@@ -127,7 +125,7 @@ export function PlopOverlay({
const r = plop.radiusCm
return (
<g transform={`translate(${object.xCm} ${object.yCm}) rotate(${object.rotationDeg})`}>
<g transform={objectTransform(object)}>
{/* Transparent body: drag to move (at least handle-sized so tiny plops stay grabbable). */}
<circle
cx={plop.xCm}