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:
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:181labels itGrid size (in)under imperial, directly beneathWidth (ft). Two adjacent numeric fields, two different scales, distinguished only by a suffix. Home ended up withgridSizeCm: 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:cmFromFtInandcmFromMetersround the same way. But every measurement column isREAL(width_cm,x_cm,grid_size_cm,spacing_cm), and the server'svalidDimensionCMchecks 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:104fades the grid below 6px per cell. At 3 cm and ~0.84 px/cm that's 2.5px → opacity 0. Home hadsnapToGrid: truethe whole time with no grid drawn and no snapping you could feel; its bed coordinates are values like80.90185676392574.Note the positions are fine on blur:
Inspector.tsx:83compares 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: dropMath.roundfromcmFromFtIn,cmFromMeters,cmFromSpacing. Keep the display-side rounding indisplayFromCm/spacingFromCm(that's what stops cm-quantization showing through) — only entry becomes exact.GardenFormModalusescmFromDisplay/displayFromCmanddimensionUnitLabelfor the grid field, so imperial readsGrid size (ft). Bed grid inInspector.tsxstays at spacing scale (in/cm) — it is genuinely a different scale, so make the labels unambiguous rather than uniform.GRID_MIN_CELL_PX, draw a coarser multiple of it (2×, 5×, 10×) so something aligned to the real grid is always visible; orsnapToGridis on andgridOpacityis 0.units.test.ts: 12 in → exactly 30.48; 1 ft → exactly 30.48; 1 m → exactly 100; round-tripdisplay → cm → displayis identity at display precision for a table of imperial and metric values.Out of scope
grid_size_cmvalues. Home's3stays3and will simply display honestly (0.1 ft) until changed by hand — the sandbox garden is already at 30 for comparison.8' 6").displayFromCmat 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
30.48; entering a 12 in bed grid stores exactly30.48. Verified through the UI, not just unit tests.ftunder imperial; bed grid still readsin; both legible without cross-referencing another field.npm testandgo test ./...green.