From 1627d5e76ead8740769fd6ba3543ff86adf30bd0 Mon Sep 17 00:00:00 2001 From: MateoSaettone Date: Wed, 20 May 2026 14:35:36 -0400 Subject: [PATCH] fix: respect imperial units in wall panel --- .../src/components/editor/floorplan-panel.tsx | 12 +-- .../components/editor/site-edge-labels.tsx | 14 +-- .../editor/wall-measurement-label.tsx | 16 +-- .../tools/item/use-placement-coordinator.tsx | 18 +--- .../components/ui/controls/metric-control.tsx | 97 +++++++++++++------ packages/editor/src/index.tsx | 8 ++ packages/editor/src/lib/measurements.test.ts | 49 ++++++++++ packages/editor/src/lib/measurements.ts | 37 +++++++ packages/nodes/src/fence/tool.tsx | 14 +-- packages/nodes/src/wall/panel.tsx | 63 +++++++----- packages/nodes/src/wall/tool.tsx | 14 +-- 11 files changed, 216 insertions(+), 126 deletions(-) create mode 100644 packages/editor/src/lib/measurements.test.ts create mode 100644 packages/editor/src/lib/measurements.ts diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index fc1cd50c7..da0556c26 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -68,6 +68,7 @@ import { type FloorplanNodeTransform as SharedFloorplanNodeTransform, } from '../../lib/floorplan' import { guideEmitter } from '../../lib/guide-events' +import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measurements' import { sfxEmitter } from '../../lib/sfx-bus' import { cn } from '../../lib/utils' import type { GuideUiState } from '../../store/use-editor' @@ -2154,14 +2155,7 @@ function formatMeasurement( metersPerUnit: number | null = null, ) { const measuredValue = metersPerUnit && metersPerUnit > 0 ? value * metersPerUnit : value - if (unit === 'imperial') { - const feet = measuredValue * 3.280_84 - const wholeFeet = Math.floor(feet) - const inches = Math.round((feet - wholeFeet) * 12) - if (inches === 12) return `${wholeFeet + 1}'0"` - return `${wholeFeet}'${inches}"` - } - return `${Number.parseFloat(measuredValue.toFixed(2))}m` + return formatLinearMeasurement(measuredValue, unit) } function formatNumber(value: number, fractionDigits = 2) { @@ -2173,7 +2167,7 @@ function convertReferenceLengthToMeters(value: number, unit: ReferenceScaleUnit) case 'centimeters': return value / 100 case 'feet': - return value * 0.3048 + return linearUnitToMeters(value, 'imperial') case 'inches': return value * 0.0254 default: diff --git a/packages/editor/src/components/editor/site-edge-labels.tsx b/packages/editor/src/components/editor/site-edge-labels.tsx index 19e331717..7ac7509ee 100644 --- a/packages/editor/src/components/editor/site-edge-labels.tsx +++ b/packages/editor/src/components/editor/site-edge-labels.tsx @@ -7,17 +7,7 @@ import { Html } from '@react-three/drei' import { createPortal, useFrame } from '@react-three/fiber' import { useMemo, useRef, useState } from 'react' import type { Object3D } from 'three' - -function formatMeasurement(value: number, unit: 'metric' | 'imperial') { - if (unit === 'imperial') { - const feet = value * 3.280_84 - const wholeFeet = Math.floor(feet) - const inches = Math.round((feet - wholeFeet) * 12) - if (inches === 12) return `${wholeFeet + 1}'0"` - return `${wholeFeet}'${inches}"` - } - return `${Number.parseFloat(value.toFixed(2))}m` -} +import { formatLinearMeasurement } from '../../lib/measurements' export function SiteEdgeLabels() { // Narrow subscription to just the site node — subscribing to the full @@ -86,7 +76,7 @@ export function SiteEdgeLabels() { textShadow: `-1.5px -1.5px 0 ${shadowColor}, 1.5px -1.5px 0 ${shadowColor}, -1.5px 1.5px 0 ${shadowColor}, 1.5px 1.5px 0 ${shadowColor}, 0 0 4px ${shadowColor}, 0 0 4px ${shadowColor}`, }} > - {formatMeasurement(edge.dist, unit)} + {formatLinearMeasurement(edge.dist, unit)} ))} diff --git a/packages/editor/src/components/editor/wall-measurement-label.tsx b/packages/editor/src/components/editor/wall-measurement-label.tsx index 3b24257f5..4a0b2fc11 100644 --- a/packages/editor/src/components/editor/wall-measurement-label.tsx +++ b/packages/editor/src/components/editor/wall-measurement-label.tsx @@ -23,6 +23,7 @@ import { Html } from '@react-three/drei' import { createPortal, useFrame } from '@react-three/fiber' import { useMemo, useState } from 'react' import * as THREE from 'three' +import { formatLinearMeasurement } from '../../lib/measurements' const GUIDE_Y_OFFSET = 0.08 const LABEL_LIFT = 0.08 @@ -56,17 +57,6 @@ type WallFaceLine = { end: Point2D } -function formatMeasurement(value: number, unit: 'metric' | 'imperial') { - if (unit === 'imperial') { - const feet = value * 3.280_84 - const wholeFeet = Math.floor(feet) - const inches = Math.round((feet - wholeFeet) * 12) - if (inches === 12) return `${wholeFeet + 1}'0"` - return `${wholeFeet}'${inches}"` - } - return `${Number.parseFloat(value.toFixed(2))}m` -} - export function WallMeasurementLabel() { const selectedIds = useViewer((state) => state.selection.selectedIds) const nodes = useScene((state) => state.nodes) @@ -542,8 +532,8 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) { } return total }, [guide, wall]) - const label = formatMeasurement(length, unit) - const heightLabel = `H ${formatMeasurement(wall.height ?? DEFAULT_WALL_HEIGHT, unit)}` + const label = formatLinearMeasurement(length, unit) + const heightLabel = `H ${formatLinearMeasurement(wall.height ?? DEFAULT_WALL_HEIGHT, unit)}` if (!(guide && Number.isFinite(length) && length >= 0.01)) return null diff --git a/packages/editor/src/components/tools/item/use-placement-coordinator.tsx b/packages/editor/src/components/tools/item/use-placement-coordinator.tsx index 5b268439c..71c99fb1f 100644 --- a/packages/editor/src/components/tools/item/use-placement-coordinator.tsx +++ b/packages/editor/src/components/tools/item/use-placement-coordinator.tsx @@ -34,6 +34,7 @@ import { import { distance, smoothstep, uv, vec2 } from 'three/tsl' import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu' import { EDITOR_LAYER } from '../../../lib/constants' +import { formatLinearMeasurement } from '../../../lib/measurements' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' import { getGridAlignedDimensions, snapToGrid, snapUpToGridStep } from './placement-math' @@ -50,17 +51,6 @@ import type { DraftNodeHandle } from './use-draft-node' const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1] -function formatMeasurement(value: number, unit: 'metric' | 'imperial') { - if (unit === 'imperial') { - const feet = value * 3.280_84 - const wholeFeet = Math.floor(feet) - const inches = Math.round((feet - wholeFeet) * 12) - if (inches === 12) return `${wholeFeet + 1}'0"` - return `${wholeFeet}'${inches}"` - } - return `${Number.parseFloat(value.toFixed(2))}m` -} - type PreviewBounds = { min: [number, number, number] max: [number, number, number] @@ -1610,9 +1600,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea const initialDepthGuideGeometry = useMemo(() => createLineGeometry(), []) const initialHeightGuideGeometry = useMemo(() => createLineGeometry(), []) const currentDimensionBounds = dimensionBounds ?? initialDimensionBounds - const widthLabel = formatMeasurement(currentDimensionBounds.dimensions[0], unit) - const depthLabel = formatMeasurement(currentDimensionBounds.dimensions[2], unit) - const heightLabel = formatMeasurement(currentDimensionBounds.dimensions[1], unit) + const widthLabel = formatLinearMeasurement(currentDimensionBounds.dimensions[0], unit) + const depthLabel = formatLinearMeasurement(currentDimensionBounds.dimensions[2], unit) + const heightLabel = formatLinearMeasurement(currentDimensionBounds.dimensions[1], unit) const widthLabelPosition: [number, number, number] = [ currentDimensionBounds.center[0], 0.04, diff --git a/packages/editor/src/components/ui/controls/metric-control.tsx b/packages/editor/src/components/ui/controls/metric-control.tsx index 14e152fa9..f6cce3e0b 100644 --- a/packages/editor/src/components/ui/controls/metric-control.tsx +++ b/packages/editor/src/components/ui/controls/metric-control.tsx @@ -3,6 +3,11 @@ import { useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useCallback, useEffect, useRef, useState } from 'react' +import { + getLinearUnitLabel, + linearUnitToMeters, + metersToLinearUnit, +} from '../../../lib/measurements' import { cn } from '../../../lib/utils' interface MetricControlProps { @@ -34,10 +39,24 @@ export function MetricControl({ }: MetricControlProps) { const viewerUnit = useViewer((state) => state.unit) const isImperial = viewerUnit === 'imperial' && unit === 'm' - const multiplier = isImperial ? 3.280_84 : 1 - const displayUnit = isImperial ? 'ft' : unit + const displayUnit = isImperial ? getLinearUnitLabel('imperial') : unit - const displayValue = value * multiplier + const toDisplayValue = useCallback( + (storedValue: number) => (isImperial ? metersToLinearUnit(storedValue, 'imperial') : storedValue), + [isImperial], + ) + const toStoredValue = useCallback( + (displayValue: number) => + isImperial ? linearUnitToMeters(displayValue, 'imperial') : displayValue, + [isImperial], + ) + const roundStoredValueForDisplayPrecision = useCallback( + (storedValue: number) => + toStoredValue(Number.parseFloat(toDisplayValue(storedValue).toFixed(precision))), + [precision, toDisplayValue, toStoredValue], + ) + + const displayValue = toDisplayValue(value) const [isEditing, setIsEditing] = useState(false) const [isDragging, setIsDragging] = useState(false) @@ -84,12 +103,12 @@ export function MetricControl({ e.preventDefault() const direction = e.deltaY < 0 ? 1 : -1 - let scrollStep = step / multiplier - if (e.shiftKey) scrollStep = (step * 10) / multiplier - else if (e.altKey) scrollStep = (step * 0.1) / multiplier + let scrollStep = toStoredValue(step) + if (e.shiftKey) scrollStep = toStoredValue(step * 10) + else if (e.altKey) scrollStep = toStoredValue(step * 0.1) const newValue = clamp(valueRef.current + direction * scrollStep) - const finalValue = Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier + const finalValue = roundStoredValueForDisplayPrecision(newValue) if (Math.abs(finalValue - valueRef.current) > 1e-6) { applyCommittedValue(finalValue) @@ -98,7 +117,7 @@ export function MetricControl({ container.addEventListener('wheel', handleWheel, { passive: false }) return () => container.removeEventListener('wheel', handleWheel) - }, [isEditing, step, clamp, applyCommittedValue, precision, multiplier]) + }, [isEditing, step, clamp, applyCommittedValue, toStoredValue, roundStoredValueForDisplayPrecision]) useEffect(() => { if (!isHovered || isEditing) return @@ -110,13 +129,12 @@ export function MetricControl({ if (direction !== 0) { e.preventDefault() - let scrollStep = step / multiplier - if (e.shiftKey) scrollStep = (step * 10) / multiplier - else if (e.altKey) scrollStep = (step * 0.1) / multiplier + let scrollStep = toStoredValue(step) + if (e.shiftKey) scrollStep = toStoredValue(step * 10) + else if (e.altKey) scrollStep = toStoredValue(step * 0.1) const newValue = clamp(valueRef.current + direction * scrollStep) - const finalValue = - Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier + const finalValue = roundStoredValueForDisplayPrecision(newValue) if (Math.abs(finalValue - valueRef.current) > 1e-6) { applyCommittedValue(finalValue) @@ -126,7 +144,15 @@ export function MetricControl({ window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [isHovered, isEditing, step, clamp, applyCommittedValue, precision, multiplier]) + }, [ + isHovered, + isEditing, + step, + clamp, + applyCommittedValue, + toStoredValue, + roundStoredValueForDisplayPrecision, + ]) const handlePointerDown = useCallback( (e: React.PointerEvent) => { @@ -143,14 +169,13 @@ export function MetricControl({ const handlePointerMove = (moveEvent: PointerEvent) => { const deltaX = moveEvent.clientX - startXRef.current - let dragStep = step / multiplier - if (moveEvent.shiftKey) dragStep = (step * 10) / multiplier - else if (moveEvent.altKey) dragStep = (step * 0.1) / multiplier + let dragStep = toStoredValue(step) + if (moveEvent.shiftKey) dragStep = toStoredValue(step * 10) + else if (moveEvent.altKey) dragStep = toStoredValue(step * 0.1) const deltaValue = deltaX * dragStep const newValue = clamp(startValueRef.current + deltaValue) - const newFinalValue = - Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier + const newFinalValue = roundStoredValueForDisplayPrecision(newValue) if (Math.abs(newFinalValue - finalValue) > 1e-6) { finalValue = newFinalValue @@ -182,13 +207,23 @@ export function MetricControl({ document.addEventListener('pointermove', handlePointerMove) document.addEventListener('pointerup', handlePointerUp) }, - [isEditing, value, onChange, onCommit, restoreOnCommit, clamp, precision, step, multiplier], + [ + isEditing, + value, + onChange, + onCommit, + restoreOnCommit, + clamp, + step, + toStoredValue, + roundStoredValueForDisplayPrecision, + ], ) const handleValueClick = useCallback(() => { setIsEditing(true) - setInputValue((value * multiplier).toFixed(precision)) - }, [value, multiplier, precision]) + setInputValue(toDisplayValue(value).toFixed(precision)) + }, [value, toDisplayValue, precision]) const handleInputChange = useCallback((e: React.ChangeEvent) => { setInputValue(e.target.value) @@ -197,12 +232,12 @@ export function MetricControl({ const submitValue = useCallback(() => { const numValue = Number.parseFloat(inputValue) if (Number.isNaN(numValue)) { - setInputValue((value * multiplier).toFixed(precision)) + setInputValue(toDisplayValue(value).toFixed(precision)) } else { - applyCommittedValue(clamp(numValue / multiplier)) + applyCommittedValue(clamp(toStoredValue(numValue))) } setIsEditing(false) - }, [inputValue, applyCommittedValue, clamp, multiplier, value, precision]) + }, [inputValue, applyCommittedValue, clamp, toStoredValue, value, precision, toDisplayValue]) const handleInputBlur = useCallback(() => { submitValue() @@ -213,21 +248,21 @@ export function MetricControl({ if (e.key === 'Enter') { submitValue() } else if (e.key === 'Escape') { - setInputValue((value * multiplier).toFixed(precision)) + setInputValue(toDisplayValue(value).toFixed(precision)) setIsEditing(false) } else if (e.key === 'ArrowUp') { e.preventDefault() - const newV = clamp(value + step / multiplier) + const newV = clamp(value + toStoredValue(step)) applyCommittedValue(newV) - setInputValue((newV * multiplier).toFixed(precision)) + setInputValue(toDisplayValue(newV).toFixed(precision)) } else if (e.key === 'ArrowDown') { e.preventDefault() - const newV = clamp(value - step / multiplier) + const newV = clamp(value - toStoredValue(step)) applyCommittedValue(newV) - setInputValue((newV * multiplier).toFixed(precision)) + setInputValue(toDisplayValue(newV).toFixed(precision)) } }, - [submitValue, value, multiplier, precision, step, clamp, applyCommittedValue], + [submitValue, value, toDisplayValue, precision, step, clamp, applyCommittedValue, toStoredValue], ) return ( diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 43054b723..9e634ca1f 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -161,6 +161,14 @@ export { getActivePaintMaterialLabel, hasActivePaintMaterial, } from './lib/material-paint' +export { + formatLinearMeasurement, + getLinearUnitLabel, + type LinearUnit, + linearControlValueToMeters, + linearUnitToMeters, + metersToLinearUnit, +} from './lib/measurements' export { duplicateRoofSubtree } from './lib/roof-duplication' export type { SceneGraph } from './lib/scene' export { applySceneGraphToEditor } from './lib/scene' diff --git a/packages/editor/src/lib/measurements.test.ts b/packages/editor/src/lib/measurements.test.ts new file mode 100644 index 000000000..a7471c84c --- /dev/null +++ b/packages/editor/src/lib/measurements.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from 'bun:test' +import { + formatLinearMeasurement, + getLinearUnitLabel, + linearControlValueToMeters, + linearUnitToMeters, + metersToLinearUnit, +} from './measurements' + +describe('linear measurements', () => { + test('formats metric measurements in meters', () => { + expect(formatLinearMeasurement(3, 'metric')).toBe('3m') + expect(formatLinearMeasurement(3.456, 'metric')).toBe('3.46m') + }) + + test('formats imperial measurements as feet and inches', () => { + expect(formatLinearMeasurement(3.048, 'imperial')).toBe(`10'0"`) + expect(formatLinearMeasurement(3.2004, 'imperial')).toBe(`10'6"`) + }) + + test('carries rounded 12 inches into the next foot', () => { + expect(formatLinearMeasurement(3.047, 'imperial')).toBe(`10'0"`) + }) + + test('converts between meters and the active linear unit', () => { + expect(metersToLinearUnit(1, 'metric')).toBe(1) + expect(linearUnitToMeters(1, 'metric')).toBe(1) + + expect(metersToLinearUnit(0.3048, 'imperial')).toBeCloseTo(1) + expect(linearUnitToMeters(1, 'imperial')).toBeCloseTo(0.3048) + }) + + test('converts numeric control input back to meters for wall panel edits', () => { + expect(linearControlValueToMeters(10, 'imperial')).toBeCloseTo(3.048) + expect(linearControlValueToMeters(0.5, 'imperial')).toBeCloseTo(0.1524) + expect(linearControlValueToMeters(-1, 'imperial')).toBeCloseTo(-0.3048) + expect(linearControlValueToMeters(3.5, 'metric')).toBe(3.5) + }) + + test('clamps numeric control input after converting to meters', () => { + expect(linearControlValueToMeters(0.1, 'imperial', { minMeters: 0.1 })).toBe(0.1) + expect(linearControlValueToMeters(0.2, 'metric', { minMeters: 0.1 })).toBe(0.2) + }) + + test('returns the display label for numeric controls', () => { + expect(getLinearUnitLabel('metric')).toBe('m') + expect(getLinearUnitLabel('imperial')).toBe('ft') + }) +}) diff --git a/packages/editor/src/lib/measurements.ts b/packages/editor/src/lib/measurements.ts new file mode 100644 index 000000000..ce929c6d1 --- /dev/null +++ b/packages/editor/src/lib/measurements.ts @@ -0,0 +1,37 @@ +export type LinearUnit = 'metric' | 'imperial' + +const METERS_PER_FOOT = 0.3048 +const FEET_PER_METER = 1 / METERS_PER_FOOT + +export function metersToLinearUnit(meters: number, unit: LinearUnit): number { + return unit === 'imperial' ? meters * FEET_PER_METER : meters +} + +export function linearUnitToMeters(value: number, unit: LinearUnit): number { + return unit === 'imperial' ? value * METERS_PER_FOOT : value +} + +export function linearControlValueToMeters( + value: number, + unit: LinearUnit, + options: { minMeters?: number } = {}, +): number { + const meters = linearUnitToMeters(value, unit) + return options.minMeters === undefined ? meters : Math.max(options.minMeters, meters) +} + +export function getLinearUnitLabel(unit: LinearUnit): string { + return unit === 'imperial' ? 'ft' : 'm' +} + +export function formatLinearMeasurement(meters: number, unit: LinearUnit): string { + if (unit === 'imperial') { + const feet = metersToLinearUnit(meters, unit) + const wholeFeet = Math.floor(feet) + const inches = Math.round((feet - wholeFeet) * 12) + if (inches === 12) return `${wholeFeet + 1}'0"` + return `${wholeFeet}'${inches}"` + } + + return `${Number.parseFloat(meters.toFixed(2))}m` +} diff --git a/packages/nodes/src/fence/tool.tsx b/packages/nodes/src/fence/tool.tsx index d2339dab5..a3f5fa525 100644 --- a/packages/nodes/src/fence/tool.tsx +++ b/packages/nodes/src/fence/tool.tsx @@ -18,6 +18,7 @@ import { EDITOR_LAYER, type FencePlanPoint, formatAngleRadians, + formatLinearMeasurement, getAngleArcToSegmentReference, getAngleToSegmentReference, getSegmentAngleReferenceAtPoint, @@ -85,17 +86,6 @@ type AngleSource = { draftVector: FencePlanPoint } -function formatMeasurement(value: number, unit: 'metric' | 'imperial') { - if (unit === 'imperial') { - const feet = value * 3.280_84 - const wholeFeet = Math.floor(feet) - const inches = Math.round((feet - wholeFeet) * 12) - if (inches === 12) return `${wholeFeet + 1}'0"` - return `${wholeFeet}'${inches}"` - } - return `${Number.parseFloat(value.toFixed(2))}m` -} - function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)) } @@ -348,7 +338,7 @@ function getDraftMeasurementState( const length = Math.hypot(dx, dz) if (length < 0.01) return null return { - lengthLabel: formatMeasurement(length, unit), + lengthLabel: formatLinearMeasurement(length, unit), lengthPosition: [(start[0] + end[0]) / 2, baseY + DRAFT_LABEL_Y, (start[1] + end[1]) / 2], angleLabels: getDraftAngleLabels(start, end, segments, baseY), } diff --git a/packages/nodes/src/wall/panel.tsx b/packages/nodes/src/wall/panel.tsx index bfd05d6e0..380c06edf 100644 --- a/packages/nodes/src/wall/panel.tsx +++ b/packages/nodes/src/wall/panel.tsx @@ -13,6 +13,10 @@ import { import { ActionButton, ActionGroup, + getLinearUnitLabel, + linearControlValueToMeters, + linearUnitToMeters, + metersToLinearUnit, PanelSection, PanelWrapper, SliderControl, @@ -25,6 +29,7 @@ import { useCallback, useRef } from 'react' export default function WallPanel() { const selectedId = useViewer((s) => s.selection.selectedIds[0]) + const unit = useViewer((s) => s.unit) const setSelection = useViewer((s) => s.setSelection) const setMovingNode = useEditor((s) => s.setMovingNode) const setCurvingWall = useEditor((s) => s.setCurvingWall) @@ -110,14 +115,18 @@ export default function WallPanel() { if (!(node && node.type === 'wall' && selectedId)) return null - const dx = node.end[0] - node.start[0] - const dz = node.end[1] - node.start[1] const length = getWallCurveLength(node) const height = node.height ?? 2.5 const thickness = node.thickness ?? 0.1 const curveOffset = getClampedWallCurveOffset(node) const maxCurveOffset = getMaxWallCurveOffset(node) + const unitLabel = getLinearUnitLabel(unit) + const displayLength = metersToLinearUnit(length, unit) + const displayHeight = metersToLinearUnit(height, unit) + const displayThickness = metersToLinearUnit(thickness, unit) + const displayCurveOffset = metersToLinearUnit(curveOffset, unit) + const displayMaxCurveOffset = metersToLinearUnit(maxCurveOffset, unit) return ( handleUpdateLength(linearUnitToMeters(value, unit))} precision={2} - step={0.01} - unit="m" - value={length} + step={unit === 'imperial' ? 0.1 : 0.01} + unit={unitLabel} + value={displayLength} /> handleUpdate({ height: Math.max(0.1, v) })} + max={metersToLinearUnit(6, unit)} + min={metersToLinearUnit(0.1, unit)} + onChange={(v) => + handleUpdate({ height: linearControlValueToMeters(v, unit, { minMeters: 0.1 }) }) + } precision={2} step={0.1} - unit="m" - value={Math.round(height * 100) / 100} + unit={unitLabel} + value={Math.round(displayHeight * 100) / 100} /> handleUpdate({ thickness: Math.max(0.05, v) })} + max={metersToLinearUnit(1, unit)} + min={metersToLinearUnit(0.05, unit)} + onChange={(v) => + handleUpdate({ thickness: linearControlValueToMeters(v, unit, { minMeters: 0.05 }) }) + } precision={3} step={0.01} - unit="m" - value={Math.round(thickness * 1000) / 1000} + unit={unitLabel} + value={Math.round(displayThickness * 1000) / 1000} /> {!hasWallChildrenBlockingCurve && ( handleUpdate({ curveOffset: normalizeWallCurveOffset(node, v) })} + max={Math.max(metersToLinearUnit(0.01, unit), displayMaxCurveOffset)} + min={-Math.max(metersToLinearUnit(0.01, unit), displayMaxCurveOffset)} + onChange={(v) => + handleUpdate({ + curveOffset: normalizeWallCurveOffset(node, linearUnitToMeters(v, unit)), + }) + } precision={2} step={0.1} - unit="m" - value={Math.round(curveOffset * 100) / 100} + unit={unitLabel} + value={Math.round(displayCurveOffset * 100) / 100} /> )} diff --git a/packages/nodes/src/wall/tool.tsx b/packages/nodes/src/wall/tool.tsx index d9cd7285f..6d98e1ca3 100644 --- a/packages/nodes/src/wall/tool.tsx +++ b/packages/nodes/src/wall/tool.tsx @@ -14,6 +14,7 @@ import { createWallOnCurrentLevel, EDITOR_LAYER, formatAngleRadians, + formatLinearMeasurement, getAngleArcToSegmentReference, getAngleToSegmentReference, getSegmentAngleReferenceAtPoint, @@ -87,17 +88,6 @@ type AngleSource = { draftVector: WallPlanPoint } -function formatMeasurement(value: number, unit: 'metric' | 'imperial') { - if (unit === 'imperial') { - const feet = value * 3.280_84 - const wholeFeet = Math.floor(feet) - const inches = Math.round((feet - wholeFeet) * 12) - if (inches === 12) return `${wholeFeet + 1}'0"` - return `${wholeFeet}'${inches}"` - } - return `${Number.parseFloat(value.toFixed(2))}m` -} - function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)) } @@ -340,7 +330,7 @@ function getDraftMeasurementState( const length = Math.hypot(dx, dz) if (length < 0.01) return null return { - lengthLabel: formatMeasurement(length, unit), + lengthLabel: formatLinearMeasurement(length, unit), lengthPosition: [(start[0] + end[0]) / 2, baseY + DRAFT_LABEL_Y, (start[1] + end[1]) / 2], angleLabels: getDraftAngleLabels(start, end, walls, baseY), }