Five findings, all fair: The soft-minimum hint contradicted itself. "did you mean ft?" made sense when the grid field was labelled inches sitting under a feet field — which is exactly what this PR removed. The remaining failure mode is different: someone reaching for plant spacing, which belongs to the bed, not the garden. The hint now says that, and points at where the bed grid actually lives. The stale "1 typed meaning one foot" comments went with it. gridNote was a three-level nested ternary; it is now gridNoteFor, with the same logic in explicit branches and the reasoning in one doc comment rather than threaded between the arms. cmFromDisplay(parseFloat(gridSize), unit) was computed once for the hint and again in the submit handler. Hoisted to a single per-render value so the two can't disagree about what was entered. roundCm's comment called the 1e-6 cm grain "1 nanometer"; it is 10 nm. Corrected — the constant was always fine, only the comment was wrong. Extracted the duplicated overlay-badge class string. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"frontend-design@claude-plugins-official": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,6 +68,14 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
|||||||
setUnit(next)
|
setUnit(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converted once per render and used by both the submit handler and the
|
||||||
|
// too-fine hint, so the two can never disagree about what was entered.
|
||||||
|
const gridSizeCm = cmFromDisplay(parseFloat(gridSize), unit)
|
||||||
|
// Soft floor: hint, don't refuse. A garden-scale grid this fine is usually
|
||||||
|
// someone reaching for plant spacing, which lives on the bed instead — but it
|
||||||
|
// is a legitimate choice for a very small garden, so the save still goes through.
|
||||||
|
const gridTooFine = Number.isFinite(gridSizeCm) && gridSizeCm > 0 && gridSizeCm < MIN_GARDEN_GRID_CM
|
||||||
|
|
||||||
async function onSubmit(e: FormEvent) {
|
async function onSubmit(e: FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setFormError(null)
|
setFormError(null)
|
||||||
@@ -86,7 +94,6 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
|||||||
setFormError('Width and height must be between 1 cm and 100 m.')
|
setFormError('Width and height must be between 1 cm and 100 m.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const gridSizeCm = cmFromDisplay(parseFloat(gridSize), unit)
|
|
||||||
if (!isValidDimensionCm(gridSizeCm)) {
|
if (!isValidDimensionCm(gridSizeCm)) {
|
||||||
setFormError('Grid size must be between 1 cm and 100 m.')
|
setFormError('Grid size must be between 1 cm and 100 m.')
|
||||||
return
|
return
|
||||||
@@ -130,10 +137,6 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unitLabel = dimensionUnitLabel(unit)
|
const unitLabel = dimensionUnitLabel(unit)
|
||||||
// Soft floor: warn, don't refuse. A sub-4″ garden grid is almost always "1"
|
|
||||||
// typed meaning one foot, but it's a legitimate choice for a tiny garden.
|
|
||||||
const gridEntered = cmFromDisplay(parseFloat(gridSize), unit)
|
|
||||||
const gridTooFine = Number.isFinite(gridEntered) && gridEntered > 0 && gridEntered < MIN_GARDEN_GRID_CM
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title={isEdit ? 'Edit garden' : 'New garden'} onClose={onClose} busy={pending}>
|
<Modal title={isEdit ? 'Edit garden' : 'New garden'} onClose={onClose} busy={pending}>
|
||||||
@@ -201,7 +204,8 @@ export function GardenFormModal({ garden, onClose }: { garden?: Garden; onClose:
|
|||||||
</div>
|
</div>
|
||||||
{gridTooFine && (
|
{gridTooFine && (
|
||||||
<p className="mt-1 text-xs text-muted">
|
<p className="mt-1 text-xs text-muted">
|
||||||
That's a {formatCm(gridEntered, unit)} grid for the whole garden — did you mean {unitLabel}?
|
{formatCm(gridSizeCm, unit)} is very fine for a whole-garden grid. Plant spacing lives on each bed
|
||||||
|
(Bed grid in the inspector), not here.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type Rect,
|
type Rect,
|
||||||
type Size,
|
type Size,
|
||||||
} from '@/lib/geometry'
|
} from '@/lib/geometry'
|
||||||
import { formatCm } from '@/lib/units'
|
import { formatCm, type UnitPref } from '@/lib/units'
|
||||||
import { useCreateObject, useCreatePlanting } from '@/lib/objects'
|
import { useCreateObject, useCreatePlanting } from '@/lib/objects'
|
||||||
import type { Plant } from '@/lib/plants'
|
import type { Plant } from '@/lib/plants'
|
||||||
import type { EditorPlanting } from '@/lib/plantings'
|
import type { EditorPlanting } from '@/lib/plantings'
|
||||||
@@ -24,8 +24,28 @@ import type { EditorGarden, EditorObject } from './types'
|
|||||||
|
|
||||||
const GRID_MIN_CELL_PX = 6
|
const GRID_MIN_CELL_PX = 6
|
||||||
const GRID_OPACITY = 0.18
|
const GRID_OPACITY = 0.18
|
||||||
|
const OVERLAY_BADGE_CLASS = 'rounded-md bg-surface/80 px-2 py-1 text-xs text-muted backdrop-blur'
|
||||||
const BED_GRID_MAX_LINES = 200 // safety cap on lines drawn inside one bed
|
const BED_GRID_MAX_LINES = 200 // safety cap on lines drawn inside one bed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What to tell the user about the grid they're looking at, or null when the
|
||||||
|
* drawn lines are simply the garden's own grid and need no explanation. Two
|
||||||
|
* states are worth naming rather than leaving them to infer: lines drawn every
|
||||||
|
* Nth grid cell (the grid was too fine to draw at this zoom), and — degenerate —
|
||||||
|
* a grid so fine no multiple of it is drawable, which matters most when snapping
|
||||||
|
* is on and would otherwise be invisibly active (#47).
|
||||||
|
*/
|
||||||
|
function gridNoteFor(
|
||||||
|
drawnCm: number | null,
|
||||||
|
gridCm: number,
|
||||||
|
snapToGrid: boolean,
|
||||||
|
unit: UnitPref,
|
||||||
|
): string | null {
|
||||||
|
if (drawnCm == null) return snapToGrid ? 'grid too fine to draw — zoom in' : null
|
||||||
|
if (drawnCm === gridCm) return null
|
||||||
|
return `grid every ${formatCm(drawnCm, unit)}`
|
||||||
|
}
|
||||||
|
|
||||||
/** The world-space bounds rect of an object (center + size → top-left rect). */
|
/** The world-space bounds rect of an object (center + size → top-left rect). */
|
||||||
function objectRect(o: EditorObject): Rect {
|
function objectRect(o: EditorObject): Rect {
|
||||||
return { x: o.xCm - o.widthCm / 2, y: o.yCm - o.heightCm / 2, w: o.widthCm, h: o.heightCm }
|
return { x: o.xCm - o.widthCm / 2, y: o.yCm - o.heightCm / 2, w: o.widthCm, h: o.heightCm }
|
||||||
@@ -113,16 +133,7 @@ export function GardenCanvas({
|
|||||||
// the drawn lines are a subset of the snap positions, never a different grid.
|
// the drawn lines are a subset of the snap positions, never a different grid.
|
||||||
const gridCm = garden.gridSizeCm
|
const gridCm = garden.gridSizeCm
|
||||||
const drawnGridCm = visibleGridStepCm(gridCm, viewport.scale, GRID_MIN_CELL_PX)
|
const drawnGridCm = visibleGridStepCm(gridCm, viewport.scale, GRID_MIN_CELL_PX)
|
||||||
// Two states worth explaining rather than leaving the user to infer: lines that
|
const gridNote = gridNoteFor(drawnGridCm, gridCm, garden.snapToGrid, garden.unitPref)
|
||||||
// are every Nth grid cell, and (degenerate) a grid too fine to draw at all.
|
|
||||||
const gridNote =
|
|
||||||
drawnGridCm == null
|
|
||||||
? garden.snapToGrid
|
|
||||||
? 'grid too fine to draw — zoom in'
|
|
||||||
: null
|
|
||||||
: drawnGridCm !== gridCm
|
|
||||||
? `grid every ${formatCm(drawnGridCm, garden.unitPref)}`
|
|
||||||
: null
|
|
||||||
|
|
||||||
const sorted = useMemo(() => [...objects].sort((a, b) => a.zIndex - b.zIndex), [objects])
|
const sorted = useMemo(() => [...objects].sort((a, b) => a.zIndex - b.zIndex), [objects])
|
||||||
const rendered = useMemo(
|
const rendered = useMemo(
|
||||||
@@ -300,12 +311,8 @@ export function GardenCanvas({
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<div className="pointer-events-none absolute left-3 top-3 flex flex-col items-start gap-1">
|
<div className="pointer-events-none absolute left-3 top-3 flex flex-col items-start gap-1">
|
||||||
<span className="rounded-md bg-surface/80 px-2 py-1 text-xs text-muted backdrop-blur">
|
<span className={OVERLAY_BADGE_CLASS}>{viewport.scale.toFixed(2)} px/cm</span>
|
||||||
{viewport.scale.toFixed(2)} px/cm
|
{gridNote && <span className={OVERLAY_BADGE_CLASS}>{gridNote}</span>}
|
||||||
</span>
|
|
||||||
{gridNote && (
|
|
||||||
<span className="rounded-md bg-surface/80 px-2 py-1 text-xs text-muted backdrop-blur">{gridNote}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ const INCHES_PER_FOOT = 12
|
|||||||
export const MIN_DIMENSION_CM = 1
|
export const MIN_DIMENSION_CM = 1
|
||||||
export const MAX_DIMENSION_CM = 10_000
|
export const MAX_DIMENSION_CM = 10_000
|
||||||
|
|
||||||
// A garden grid finer than this at *dimension* scale is a mis-entry, not a
|
// Below this, a garden-scale grid is fine enough to be worth questioning — it
|
||||||
// preference (typing "1" in a feet field meaning one foot, and landing on a 1 cm
|
// usually means someone reached for plant spacing, which belongs to the bed, not
|
||||||
// grid). Soft: the form warns, it doesn't refuse.
|
// the garden. Soft: the form hints, it doesn't refuse.
|
||||||
export const MIN_GARDEN_GRID_CM = 10
|
export const MIN_GARDEN_GRID_CM = 10
|
||||||
|
|
||||||
/** Whether a centimeter dimension is within the server's accepted range. */
|
/** Whether a centimeter dimension is within the server's accepted range. */
|
||||||
@@ -23,9 +23,9 @@ export function isValidDimensionCm(cm: number): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Round away IEEE-754 dust without rounding away precision: 12 × 2.54 is
|
/** 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
|
* 30.479999999999997 in binary floating point, and the 1e-6 cm (10 nm) grain
|
||||||
* anything a garden cares about. Every cm column is REAL, so entry stays exact
|
* here is far below anything a garden cares about. Every cm column is REAL, so
|
||||||
* — this only stops 30.48 from being stored as 30.479999999999997. */
|
* entry stays exact — this only stops 30.48 being stored as 30.479999999999997. */
|
||||||
function roundCm(cm: number): number {
|
function roundCm(cm: number): number {
|
||||||
return Math.round(cm * 1e6) / 1e6
|
return Math.round(cm * 1e6) / 1e6
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user