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
+21 -7
View File
@@ -12,19 +12,32 @@ const INCHES_PER_FOOT = 12
export const MIN_DIMENSION_CM = 1
export const MAX_DIMENSION_CM = 10_000
// A garden grid finer than this at *dimension* scale is a mis-entry, not a
// preference (typing "1" in a feet field meaning one foot, and landing on a 1 cm
// grid). Soft: the form warns, it doesn't refuse.
export const MIN_GARDEN_GRID_CM = 10
/** Whether a centimeter dimension is within the server's accepted range. */
export function isValidDimensionCm(cm: number): boolean {
return Number.isFinite(cm) && cm >= MIN_DIMENSION_CM && cm <= MAX_DIMENSION_CM
}
/** Feet (+ optional inches) → whole centimeters. */
export function cmFromFtIn(feet: number, inches = 0): number {
return Math.round((feet * INCHES_PER_FOOT + inches) * CM_PER_INCH)
/** Round away IEEE-754 dust without rounding away precision: 12 × 2.54 is
* 30.479999999999997 in binary floating point, and 1 nanometer is far below
* anything a garden cares about. Every cm column is REAL, so entry stays exact
* — this only stops 30.48 from being stored as 30.479999999999997. */
function roundCm(cm: number): number {
return Math.round(cm * 1e6) / 1e6
}
/** Meters → whole centimeters. */
/** Feet (+ optional inches) → centimeters, exactly (1 ft → 30.48). */
export function cmFromFtIn(feet: number, inches = 0): number {
return roundCm((feet * INCHES_PER_FOOT + inches) * CM_PER_INCH)
}
/** Meters → centimeters, exactly. */
export function cmFromMeters(meters: number): number {
return Math.round(meters * CM_PER_METER)
return roundCm(meters * CM_PER_METER)
}
/** A value typed in the given unit (meters, or feet) → centimeters. */
@@ -79,9 +92,10 @@ export function formatSpacing(cm: number, unit: UnitPref): string {
return `${Math.round(cm)} cm`
}
/** A spacing value typed in the given unit (cm, or inches) → whole centimeters. */
/** A spacing value typed in the given unit (cm, or inches) → centimeters,
* exactly (12 in → 30.48). */
export function cmFromSpacing(value: number, unit: UnitPref): number {
return unit === 'imperial' ? Math.round(value * CM_PER_INCH) : Math.round(value)
return roundCm(unit === 'imperial' ? value * CM_PER_INCH : value)
}
/** Centimeters → a spacing number in the given unit, for prefilling an input.