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
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
cmFromDisplay,
|
||
cmFromFtIn,
|
||
cmFromMeters,
|
||
cmFromSpacing,
|
||
displayFromCm,
|
||
formatCm,
|
||
formatDimensions,
|
||
formatSpacing,
|
||
spacingFromCm,
|
||
} from './units'
|
||
|
||
describe('cm conversions', () => {
|
||
// Every cm column is REAL, so entry is exact — no whole-cm quantization. A 1 ft
|
||
// grid is 30.48 cm, not 30, or the error accumulates across the garden (#47).
|
||
it('feet+inches → exact cm', () => {
|
||
expect(cmFromFtIn(1)).toBe(30.48)
|
||
expect(cmFromFtIn(4)).toBe(121.92)
|
||
expect(cmFromFtIn(8)).toBe(243.84)
|
||
expect(cmFromFtIn(4, 6)).toBe(137.16)
|
||
})
|
||
|
||
it('meters → exact cm', () => {
|
||
expect(cmFromMeters(10)).toBe(1000)
|
||
expect(cmFromMeters(1.22)).toBe(122)
|
||
expect(cmFromMeters(0.305)).toBe(30.5)
|
||
})
|
||
|
||
it('rounds away float dust, not precision', () => {
|
||
// 12 × 2.54 is 30.479999999999997 in IEEE-754; 1 ft must read as 30.48.
|
||
expect(cmFromFtIn(0, 12)).toBe(30.48)
|
||
expect(cmFromSpacing(12, 'imperial')).toBe(30.48)
|
||
expect(cmFromSpacing(1, 'imperial')).toBe(2.54)
|
||
})
|
||
|
||
it('round-trips display → cm → display at display precision', () => {
|
||
const imperial = [0.5, 1, 2.5, 4, 8, 12.5, 24]
|
||
for (const ft of imperial) {
|
||
expect(displayFromCm(cmFromDisplay(ft, 'imperial'), 'imperial')).toBe(ft)
|
||
}
|
||
const metric = [0.15, 1, 1.22, 3.05, 10, 24.5]
|
||
for (const m of metric) {
|
||
expect(displayFromCm(cmFromDisplay(m, 'metric'), 'metric')).toBe(m)
|
||
}
|
||
})
|
||
})
|
||
|
||
describe('displayFromCm', () => {
|
||
it('metric keeps cm precision', () => {
|
||
expect(displayFromCm(1000, 'metric')).toBe(10)
|
||
expect(displayFromCm(122, 'metric')).toBe(1.22)
|
||
})
|
||
|
||
it('imperial rounds to 0.1 ft so integers stay clean', () => {
|
||
expect(displayFromCm(122, 'imperial')).toBe(4) // not 4.0026
|
||
expect(displayFromCm(244, 'imperial')).toBe(8) // not 8.01
|
||
})
|
||
})
|
||
|
||
describe('formatCm', () => {
|
||
it('metric shows meters', () => {
|
||
expect(formatCm(122, 'metric')).toBe('1.22 m')
|
||
expect(formatCm(1000, 'metric')).toBe('10 m')
|
||
})
|
||
|
||
it('imperial shows feet and inches, carrying 12″ up', () => {
|
||
expect(formatCm(122, 'imperial')).toBe('4′ 0″')
|
||
expect(formatCm(244, 'imperial')).toBe('8′ 0″')
|
||
expect(formatCm(137, 'imperial')).toBe('4′ 6″')
|
||
})
|
||
|
||
it('formats width × height', () => {
|
||
expect(formatDimensions(122, 244, 'imperial')).toBe('4′ 0″ × 8′ 0″')
|
||
})
|
||
})
|
||
|
||
describe('plant spacing (small scale)', () => {
|
||
it('formats cm in metric, whole inches in imperial', () => {
|
||
expect(formatSpacing(15, 'metric')).toBe('15 cm')
|
||
expect(formatSpacing(15, 'imperial')).toBe('6″') // 15/2.54 = 5.9 → 6
|
||
expect(formatSpacing(30, 'imperial')).toBe('12″')
|
||
})
|
||
|
||
it('parses an entered value back to exact cm', () => {
|
||
expect(cmFromSpacing(15, 'metric')).toBe(15)
|
||
expect(cmFromSpacing(6, 'imperial')).toBe(15.24)
|
||
})
|
||
|
||
it('prefills an input from cm', () => {
|
||
expect(spacingFromCm(15, 'metric')).toBe(15)
|
||
expect(spacingFromCm(30, 'imperial')).toBe(11.8) // 30/2.54 = 11.81 → 11.8
|
||
expect(spacingFromCm(30.48, 'imperial')).toBe(12) // an exactly-12in grid reads back as 12
|
||
})
|
||
})
|