Units correctness: exact imperial entry, garden grid at dimension scale (#47) (#60)
Build image / build-and-push (push) Successful in 6s
Build image / build-and-push (push) Successful in 6s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #60.
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { useEffect, useId, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'
|
||||
import { screenToWorld, snapLocalToBedGrid, snapPoint, worldToLocal, type Rect, type Size } from '@/lib/geometry'
|
||||
import {
|
||||
screenToWorld,
|
||||
snapLocalToBedGrid,
|
||||
snapPoint,
|
||||
visibleGridStepCm,
|
||||
worldToLocal,
|
||||
type Rect,
|
||||
type Size,
|
||||
} from '@/lib/geometry'
|
||||
import { formatCm, type UnitPref } from '@/lib/units'
|
||||
import { useCreateObject, useCreatePlanting } from '@/lib/objects'
|
||||
import type { Plant } from '@/lib/plants'
|
||||
import type { EditorPlanting } from '@/lib/plantings'
|
||||
@@ -14,9 +23,29 @@ import { useViewport } from './useViewport'
|
||||
import type { EditorGarden, EditorObject } from './types'
|
||||
|
||||
const GRID_MIN_CELL_PX = 6
|
||||
const GRID_MAX_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
|
||||
|
||||
/**
|
||||
* 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). */
|
||||
function objectRect(o: EditorObject): Rect {
|
||||
return { x: o.xCm - o.widthCm / 2, y: o.yCm - o.heightCm / 2, w: o.widthCm, h: o.heightCm }
|
||||
@@ -99,9 +128,12 @@ export function GardenCanvas({
|
||||
fitToRect(target ? objectRect(target) : gardenRect, size)
|
||||
}, [size, garden.id, garden.widthCm, garden.heightCm, focusedObjectId, objects, gardenRect, fitToRect])
|
||||
|
||||
// Draw the true grid when its cells are legible, else the coarsest-but-smallest
|
||||
// multiple of it that is (see visibleGridStepCm). Snapping still uses gridCm —
|
||||
// the drawn lines are a subset of the snap positions, never a different grid.
|
||||
const gridCm = garden.gridSizeCm
|
||||
const cellPx = gridCm * viewport.scale
|
||||
const gridOpacity = Math.max(0, Math.min(GRID_MAX_OPACITY, ((cellPx - GRID_MIN_CELL_PX) / GRID_MIN_CELL_PX) * GRID_MAX_OPACITY))
|
||||
const drawnGridCm = visibleGridStepCm(gridCm, viewport.scale, GRID_MIN_CELL_PX)
|
||||
const gridNote = gridNoteFor(drawnGridCm, gridCm, garden.snapToGrid, garden.unitPref)
|
||||
|
||||
const sorted = useMemo(() => [...objects].sort((a, b) => a.zIndex - b.zIndex), [objects])
|
||||
const rendered = useMemo(
|
||||
@@ -197,11 +229,11 @@ export function GardenCanvas({
|
||||
onPointerDown={onCanvasPointerDown}
|
||||
>
|
||||
<g transform={`translate(${viewport.tx} ${viewport.ty}) scale(${viewport.scale})`}>
|
||||
{gridOpacity > 0.005 && (
|
||||
{drawnGridCm != null && (
|
||||
<>
|
||||
<defs>
|
||||
<pattern id={gridId} width={gridCm} height={gridCm} patternUnits="userSpaceOnUse">
|
||||
<path d={`M ${gridCm} 0 L 0 0 0 ${gridCm}`} fill="none" stroke="#808080" strokeOpacity={gridOpacity} strokeWidth={1} vectorEffect="non-scaling-stroke" />
|
||||
<pattern id={gridId} width={drawnGridCm} height={drawnGridCm} patternUnits="userSpaceOnUse">
|
||||
<path d={`M ${drawnGridCm} 0 L 0 0 0 ${drawnGridCm}`} fill="none" stroke="#808080" strokeOpacity={GRID_OPACITY} strokeWidth={1} vectorEffect="non-scaling-stroke" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect x={0} y={0} width={garden.widthCm} height={garden.heightCm} fill={`url(#${gridId})`} />
|
||||
@@ -278,8 +310,9 @@ export function GardenCanvas({
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div className="pointer-events-none absolute left-3 top-3 rounded-md bg-surface/80 px-2 py-1 text-xs text-muted backdrop-blur">
|
||||
{viewport.scale.toFixed(2)} px/cm
|
||||
<div className="pointer-events-none absolute left-3 top-3 flex flex-col items-start gap-1">
|
||||
<span className={OVERLAY_BADGE_CLASS}>{viewport.scale.toFixed(2)} px/cm</span>
|
||||
{gridNote && <span className={OVERLAY_BADGE_CLASS}>{gridNote}</span>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user