Units correctness: exact imperial, garden grid at dimension scale, no silently-invisible grid #47

Closed
opened 2026-07-21 04:16:00 +00:00 by steve · 0 comments
Owner

Design: DESIGN.md

Context

Measured against the live Home garden, not estimated. Three separate defects compound in the measurement layer.

1. The garden grid is entered at spacing scale. GardenFormModal.tsx:181 labels it Grid size (in) under imperial, directly beneath Width (ft). Two adjacent numeric fields, two different scales, distinguished only by a suffix. Home ended up with gridSizeCm: 3 — almost certainly "1" typed meaning one foot. The bed grid in the object inspector is correctly at spacing scale (it's a plant-spacing concern); the garden grid is a layout concern and belongs at dimension scale.

2. Whole-cm quantization throws away precision the schema can hold. units.ts:84:

cmFromSpacing = unit === 'imperial' ? Math.round(value * CM_PER_INCH) : Math.round(value)

cmFromFtIn and cmFromMeters round the same way. But every measurement column is REAL (width_cm, x_cm, grid_size_cm, spacing_cm), and the server's validDimensionCM checks range and finiteness only — never integrality. The rounding is a client-side choice the schema never asked for.

Measured on a sandbox copy: entering a 12 in grid stores 30 cm, i.e. an 11.811″ grid. Every cell is short by 0.189″, and because the error is systematic it accumulates — across the 24 ft garden the 24th gridline lands 4.61″ off where a true 1 ft grid would put it.

3. Snap stays active when the grid stops drawing. GardenCanvas.tsx:104 fades the grid below 6px per cell. At 3 cm and ~0.84 px/cm that's 2.5px → opacity 0. Home had snapToGrid: true the whole time with no grid drawn and no snapping you could feel; its bed coordinates are values like 80.90185676392574.

Note the positions are fine on blur: Inspector.tsx:83 compares at display precision before writing, so a blur without a real edit fires no PATCH. The grid field has no equivalent guard against the inch→cm rounding.

Scope

  • web/src/lib/units.ts: drop Math.round from cmFromFtIn, cmFromMeters, cmFromSpacing. Keep the display-side rounding in displayFromCm/spacingFromCm (that's what stops cm-quantization showing through) — only entry becomes exact.
  • Garden grid moves to dimension scale: GardenFormModal uses cmFromDisplay/displayFromCm and dimensionUnitLabel for the grid field, so imperial reads Grid size (ft). Bed grid in Inspector.tsx stays at spacing scale (in/cm) — it is genuinely a different scale, so make the labels unambiguous rather than uniform.
  • Grid visibility and snap can't disagree. Pick one and document it:
    • preferred: when the true grid would fall below GRID_MIN_CELL_PX, draw a coarser multiple of it (2×, 5×, 10×) so something aligned to the real grid is always visible; or
    • fall back: keep the fade, but surface an explicit "grid too fine to draw at this zoom" hint whenever snapToGrid is on and gridOpacity is 0.
  • Sanity-bound the garden grid on entry (a sub-inch garden-scale grid is a mis-entry, not a preference) — a soft minimum with a hint beats a silent accept.
  • Unit tests in units.test.ts: 12 in → exactly 30.48; 1 ft → exactly 30.48; 1 m → exactly 100; round-trip display → cm → display is identity at display precision for a table of imperial and metric values.

Out of scope

  • Migrating existing grid_size_cm values. Home's 3 stays 3 and will simply display honestly (0.1 ft) until changed by hand — the sandbox garden is already at 30 for comparison.
  • Feet-and-inches compound entry (8' 6"). displayFromCm at 0.1 ft ≈ 1.2 in granularity is coarse for bed positions; worth its own polish issue if it annoys in practice.

Key files

web/src/lib/units.ts · web/src/lib/units.test.ts · web/src/components/gardens/GardenFormModal.tsx · web/src/editor/Inspector.tsx · web/src/editor/GardenCanvas.tsx (GRID_MIN_CELL_PX, gridOpacity)

Dependencies

None. Should land first and alone — every layout judgement downstream is easier to trust on a grid that means what it says.

Acceptance criteria

  • Entering a 1 ft garden grid stores exactly 30.48; entering a 12 in bed grid stores exactly 30.48. Verified through the UI, not just unit tests.
  • Reopening either form shows back exactly what was typed.
  • With snap on, there is never a state where no grid is visible and no explanation is given.
  • Garden grid field reads ft under imperial; bed grid still reads in; both legible without cross-referencing another field.
  • npm test and go test ./... green.
Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md) ## Context Measured against the live Home garden, not estimated. Three separate defects compound in the measurement layer. **1. The garden grid is entered at spacing scale.** `GardenFormModal.tsx:181` labels it `Grid size (in)` under imperial, directly beneath `Width (ft)`. Two adjacent numeric fields, two different scales, distinguished only by a suffix. Home ended up with `gridSizeCm: 3` — almost certainly "1" typed meaning one foot. The bed grid in the object inspector is correctly at spacing scale (it's a plant-spacing concern); the *garden* grid is a layout concern and belongs at dimension scale. **2. Whole-cm quantization throws away precision the schema can hold.** `units.ts:84`: ```js cmFromSpacing = unit === 'imperial' ? Math.round(value * CM_PER_INCH) : Math.round(value) ``` `cmFromFtIn` and `cmFromMeters` round the same way. But every measurement column is `REAL` (`width_cm`, `x_cm`, `grid_size_cm`, `spacing_cm`), and the server's `validDimensionCM` checks range and finiteness only — never integrality. **The rounding is a client-side choice the schema never asked for.** Measured on a sandbox copy: entering a **12 in** grid stores **30 cm**, i.e. an **11.811″** grid. Every cell is short by 0.189″, and because the error is systematic it accumulates — across the 24 ft garden the 24th gridline lands **4.61″** off where a true 1 ft grid would put it. **3. Snap stays active when the grid stops drawing.** `GardenCanvas.tsx:104` fades the grid below 6px per cell. At 3 cm and ~0.84 px/cm that's 2.5px → opacity 0. Home had `snapToGrid: true` the whole time with no grid drawn and no snapping you could feel; its bed coordinates are values like `80.90185676392574`. Note the *positions* are fine on blur: `Inspector.tsx:83` compares at display precision before writing, so a blur without a real edit fires no PATCH. The grid field has no equivalent guard against the inch→cm rounding. ## Scope - [ ] `web/src/lib/units.ts`: drop `Math.round` from `cmFromFtIn`, `cmFromMeters`, `cmFromSpacing`. Keep the display-side rounding in `displayFromCm`/`spacingFromCm` (that's what stops cm-quantization showing through) — only entry becomes exact. - [ ] Garden grid moves to **dimension** scale: `GardenFormModal` uses `cmFromDisplay`/`displayFromCm` and `dimensionUnitLabel` for the grid field, so imperial reads `Grid size (ft)`. Bed grid in `Inspector.tsx` stays at spacing scale (`in`/`cm`) — it is genuinely a different scale, so make the labels unambiguous rather than uniform. - [ ] Grid visibility and snap can't disagree. Pick one and document it: - preferred: when the true grid would fall below `GRID_MIN_CELL_PX`, draw a coarser multiple of it (2×, 5×, 10×) so *something* aligned to the real grid is always visible; or - fall back: keep the fade, but surface an explicit "grid too fine to draw at this zoom" hint whenever `snapToGrid` is on and `gridOpacity` is 0. - [ ] Sanity-bound the garden grid on entry (a sub-inch garden-scale grid is a mis-entry, not a preference) — a soft minimum with a hint beats a silent accept. - [ ] Unit tests in `units.test.ts`: 12 in → exactly 30.48; 1 ft → exactly 30.48; 1 m → exactly 100; round-trip `display → cm → display` is identity at display precision for a table of imperial and metric values. ## Out of scope - Migrating existing `grid_size_cm` values. Home's `3` stays `3` and will simply display honestly (0.1 ft) until changed by hand — the sandbox garden is already at 30 for comparison. - Feet-and-inches compound entry (`8' 6"`). `displayFromCm` at 0.1 ft ≈ 1.2 in granularity is coarse for bed positions; worth its own polish issue if it annoys in practice. ## Key files `web/src/lib/units.ts` · `web/src/lib/units.test.ts` · `web/src/components/gardens/GardenFormModal.tsx` · `web/src/editor/Inspector.tsx` · `web/src/editor/GardenCanvas.tsx` (`GRID_MIN_CELL_PX`, `gridOpacity`) ## Dependencies None. Should land **first and alone** — every layout judgement downstream is easier to trust on a grid that means what it says. ## Acceptance criteria - Entering a 1 ft garden grid stores exactly `30.48`; entering a 12 in bed grid stores exactly `30.48`. Verified through the UI, not just unit tests. - Reopening either form shows back exactly what was typed. - With snap on, there is never a state where no grid is visible and no explanation is given. - Garden grid field reads `ft` under imperial; bed grid still reads `in`; both legible without cross-referencing another field. - `npm test` and `go test ./...` green.
steve added the frontenddrawingpolish labels 2026-07-21 04:16:00 +00:00
steve closed this issue 2026-07-21 04:50:49 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/pansy#47