Units correctness: exact imperial entry, garden grid at dimension scale
Build image / build-and-push (push) Successful in 16s
Gadfly review (reusable) / review (pull_request) Successful in 5m53s
Adversarial Review (Gadfly) / review (pull_request) Successful in 5m53s

Three compounding defects in the measurement layer (#47), measured against the
live Home garden rather than estimated.

Entry was quantized to whole centimeters even though every measurement column is
REAL and the server only range-checks. A 12in grid stored 30cm — an 11.811in
grid, short by 0.189in per cell, and because the error is systematic it
accumulates: 4.61in of drift across a 24ft garden. cmFromFtIn / cmFromMeters /
cmFromSpacing now convert exactly, rounding only at 1e-6 cm to clear IEEE-754
dust (12 x 2.54 is 30.479999999999997 in binary). Display-side rounding is
untouched, so cm quantization still can't show through in a field.

The garden grid was entered at spacing scale: "Grid size (in)" sat directly under
"Width (ft)", two adjacent numeric fields at two different scales distinguished
only by a suffix. Home ended up on a 3cm grid, almost certainly "1" typed meaning
one foot. The garden grid is a layout concern, so it moves to dimension scale and
reads "Garden grid (ft)"; the bed grid in the object inspector is a plant-spacing
concern and deliberately stays at cm/in. A sub-10cm garden grid now draws a soft
warning rather than being silently accepted.

Snap could stay active while the grid stopped drawing: the canvas faded the grid
out below 6px per cell, so Home ran with snapToGrid on, no grid drawn, and no
snapping you could feel. Replaced the fade with coarsening — visibleGridStepCm
picks the smallest multiple of the true grid (2x, 5x, 10x...) whose cells are
legible, so every line drawn is still a real grid line and something aligned to
the grid is always visible. When the drawn step isn't the true grid the canvas
says so; a degenerate grid it can't draw at all says that instead of nothing.

Closes #47

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
2026-07-21 00:38:06 -04:00
co-authored by Claude Opus 4.8
parent e22bdd6cab
commit 782f2394b2
6 changed files with 188 additions and 57 deletions
+32
View File
@@ -6,6 +6,7 @@ import {
snapLocalToBedGrid,
snapPoint,
snapValue,
visibleGridStepCm,
worldToLocal,
worldToScreen,
zoomToFitRect,
@@ -128,3 +129,34 @@ describe('grid snapping', () => {
expect(p.x).toBeGreaterThanOrEqual(-60)
})
})
describe('visibleGridStepCm', () => {
it('draws the true grid when its cells are already legible', () => {
expect(visibleGridStepCm(30.48, 1, 6)).toBe(30.48)
expect(visibleGridStepCm(10, 0.6, 6)).toBe(10) // exactly at the minimum
})
it('coarsens to a multiple rather than fading out', () => {
// The #47 failure: a 3cm grid at 0.84 px/cm is 2.5px per cell. Instead of
// drawing nothing while snapping stays on, draw every 5th line (12.6px).
expect(visibleGridStepCm(3, 0.84, 6)).toBe(15)
expect(visibleGridStepCm(1, 0.84, 6)).toBe(10)
})
it('only ever returns a whole multiple of the true grid', () => {
for (const scale of [0.05, 0.3, 0.84, 1, 4]) {
const step = visibleGridStepCm(7, scale, 6)
expect(step).not.toBeNull()
expect(Math.round(step! / 7) * 7).toBeCloseTo(step!, 9)
expect(step! * scale).toBeGreaterThanOrEqual(6)
}
})
it('reports null for a degenerate grid rather than drawing nothing silently', () => {
expect(visibleGridStepCm(0, 1, 6)).toBeNull()
expect(visibleGridStepCm(-5, 1, 6)).toBeNull()
expect(visibleGridStepCm(NaN, 1, 6)).toBeNull()
expect(visibleGridStepCm(30, 0, 6)).toBeNull()
expect(visibleGridStepCm(1e-9, 1, 6)).toBeNull() // beyond the coarsening cap
})
})