Skip to content

Commit 08cdc16

Browse files
committed
fix: respect imperial units in wall panel
1 parent 50304d7 commit 08cdc16

11 files changed

Lines changed: 283 additions & 123 deletions

File tree

packages/editor/src/components/editor/floorplan-panel.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import {
7474
type FloorplanNodeTransform as SharedFloorplanNodeTransform,
7575
} from '../../lib/floorplan'
7676
import { guideEmitter } from '../../lib/guide-events'
77+
import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measurements'
7778
import { sfxEmitter } from '../../lib/sfx-bus'
7879
import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
7980
import { cn } from '../../lib/utils'
@@ -2614,14 +2615,7 @@ function formatMeasurement(
26142615
metersPerUnit: number | null = null,
26152616
) {
26162617
const measuredValue = metersPerUnit && metersPerUnit > 0 ? value * metersPerUnit : value
2617-
if (unit === 'imperial') {
2618-
const feet = measuredValue * 3.280_84
2619-
const wholeFeet = Math.floor(feet)
2620-
const inches = Math.round((feet - wholeFeet) * 12)
2621-
if (inches === 12) return `${wholeFeet + 1}'0"`
2622-
return `${wholeFeet}'${inches}"`
2623-
}
2624-
return `${Number.parseFloat(measuredValue.toFixed(2))}m`
2618+
return formatLinearMeasurement(measuredValue, unit)
26252619
}
26262620

26272621
function formatNumber(value: number, fractionDigits = 2) {
@@ -2633,7 +2627,7 @@ function convertReferenceLengthToMeters(value: number, unit: ReferenceScaleUnit)
26332627
case 'centimeters':
26342628
return value / 100
26352629
case 'feet':
2636-
return value * 0.3048
2630+
return linearUnitToMeters(value, 'imperial')
26372631
case 'inches':
26382632
return value * 0.0254
26392633
default:

packages/editor/src/components/editor/site-edge-labels.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import { Html } from '@react-three/drei'
77
import { createPortal, useFrame, useThree } from '@react-three/fiber'
88
import { useCallback, useMemo, useRef, useState } from 'react'
99
import { type Camera, type Object3D, Vector3 } from 'three'
10+
import { formatLinearMeasurement } from '../../lib/measurements'
1011
import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
1112
import useEditor from '../../store/use-editor'
12-
import { formatMeasurement } from './measurement-pill'
1313

1414
type ViewportSize = {
1515
width: number
@@ -110,7 +110,7 @@ export function SiteEdgeLabels() {
110110
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}`,
111111
}}
112112
>
113-
{formatMeasurement(edge.dist, unit)}
113+
{formatLinearMeasurement(edge.dist, unit)}
114114
</div>
115115
</Html>
116116
))}

packages/editor/src/components/editor/wall-measurement-label.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Html } from '@react-three/drei'
2323
import { createPortal, useFrame } from '@react-three/fiber'
2424
import { useMemo, useState } from 'react'
2525
import * as THREE from 'three'
26+
import { formatLinearMeasurement } from '../../lib/measurements'
2627

2728
const GUIDE_Y_OFFSET = 0.08
2829
const LABEL_LIFT = 0.08
@@ -62,17 +63,6 @@ type WallFaceLine = {
6263
end: Point2D
6364
}
6465

65-
function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
66-
if (unit === 'imperial') {
67-
const feet = value * 3.280_84
68-
const wholeFeet = Math.floor(feet)
69-
const inches = Math.round((feet - wholeFeet) * 12)
70-
if (inches === 12) return `${wholeFeet + 1}'0"`
71-
return `${wholeFeet}'${inches}"`
72-
}
73-
return `${Number.parseFloat(value.toFixed(2))}m`
74-
}
75-
7666
export function WallMeasurementLabel() {
7767
const selectedIds = useViewer((state) => state.selection.selectedIds)
7868
const nodes = useScene((state) => state.nodes)
@@ -547,8 +537,8 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
547537
}
548538
return total
549539
}, [guide, wall])
550-
const label = formatMeasurement(length, unit)
551-
const heightLabel = `H ${formatMeasurement(wall.height ?? DEFAULT_WALL_HEIGHT, unit)}`
540+
const label = formatLinearMeasurement(length, unit)
541+
const heightLabel = `H ${formatLinearMeasurement(wall.height ?? DEFAULT_WALL_HEIGHT, unit)}`
552542

553543
if (!(guide && Number.isFinite(length) && length >= 0.01)) return null
554544

packages/editor/src/components/tools/item/use-placement-coordinator.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
4141
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
4242
import { EDITOR_LAYER } from '../../../lib/constants'
43+
import { formatLinearMeasurement } from '../../../lib/measurements'
4344
import { sfxEmitter } from '../../../lib/sfx-bus'
4445
import { resolveAlignmentForActiveBuilding } from '../../../lib/world-grid-snap'
4546
import useEditor from '../../../store/use-editor'
@@ -68,17 +69,6 @@ const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
6869
* floor-plan overlay and the 3D registry move tool. */
6970
const ALIGNMENT_THRESHOLD_M = 0.08
7071

71-
function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
72-
if (unit === 'imperial') {
73-
const feet = value * 3.280_84
74-
const wholeFeet = Math.floor(feet)
75-
const inches = Math.round((feet - wholeFeet) * 12)
76-
if (inches === 12) return `${wholeFeet + 1}'0"`
77-
return `${wholeFeet}'${inches}"`
78-
}
79-
return `${Number.parseFloat(value.toFixed(2))}m`
80-
}
81-
8272
/**
8373
* Expand `bounds` outward so each axis is rounded up to the active grid step.
8474
* The wireframe stays centered on the original bounds centre on each axis we
@@ -1869,9 +1859,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
18691859
const initialDepthGuideGeometry = useMemo(() => createLineGeometry(), [])
18701860
const initialHeightGuideGeometry = useMemo(() => createLineGeometry(), [])
18711861
const currentDimensionBounds = dimensionBounds ?? initialDimensionBounds
1872-
const widthLabel = formatMeasurement(currentDimensionBounds.dimensions[0], unit)
1873-
const depthLabel = formatMeasurement(currentDimensionBounds.dimensions[2], unit)
1874-
const heightLabel = formatMeasurement(currentDimensionBounds.dimensions[1], unit)
1862+
const widthLabel = formatLinearMeasurement(currentDimensionBounds.dimensions[0], unit)
1863+
const depthLabel = formatLinearMeasurement(currentDimensionBounds.dimensions[2], unit)
1864+
const heightLabel = formatLinearMeasurement(currentDimensionBounds.dimensions[1], unit)
18751865
const widthLabelPosition: [number, number, number] = [
18761866
currentDimensionBounds.center[0],
18771867
0.04,

packages/editor/src/components/ui/controls/metric-control.tsx

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
import { useScene } from '@pascal-app/core'
44
import { useViewer } from '@pascal-app/viewer'
55
import { useCallback, useEffect, useRef, useState } from 'react'
6+
import {
7+
getLinearUnitLabel,
8+
linearUnitToMeters,
9+
metersToLinearUnit,
10+
} from '../../../lib/measurements'
611
import { cn } from '../../../lib/utils'
712

813
interface MetricControlProps {
@@ -34,10 +39,30 @@ export function MetricControl({
3439
}: MetricControlProps) {
3540
const viewerUnit = useViewer((state) => state.unit)
3641
const isImperial = viewerUnit === 'imperial' && unit === 'm'
37-
const multiplier = isImperial ? 3.280_84 : 1
38-
const displayUnit = isImperial ? 'ft' : unit
42+
const displayUnit = isImperial ? getLinearUnitLabel('imperial') : unit
3943

40-
const displayValue = value * multiplier
44+
const toDisplayValue = useCallback(
45+
(storedValue: number) => (isImperial ? metersToLinearUnit(storedValue, 'imperial') : storedValue),
46+
[isImperial],
47+
)
48+
const toStoredValue = useCallback(
49+
(displayValue: number) =>
50+
isImperial ? linearUnitToMeters(displayValue, 'imperial') : displayValue,
51+
[isImperial],
52+
)
53+
const clamp = useCallback(
54+
(val: number) => {
55+
return Math.min(Math.max(val, min), max)
56+
},
57+
[min, max],
58+
)
59+
const roundStoredValueForDisplayPrecision = useCallback(
60+
(storedValue: number) =>
61+
clamp(toStoredValue(Number.parseFloat(toDisplayValue(storedValue).toFixed(precision)))),
62+
[clamp, precision, toDisplayValue, toStoredValue],
63+
)
64+
65+
const displayValue = toDisplayValue(value)
4166

4267
const [isEditing, setIsEditing] = useState(false)
4368
const [isDragging, setIsDragging] = useState(false)
@@ -50,13 +75,6 @@ export function MetricControl({
5075
const valueRef = useRef(value)
5176
valueRef.current = value
5277

53-
const clamp = useCallback(
54-
(val: number) => {
55-
return Math.min(Math.max(val, min), max)
56-
},
57-
[min, max],
58-
)
59-
6078
const applyCommittedValue = useCallback(
6179
(nextValue: number) => {
6280
if (onCommit) {
@@ -84,12 +102,12 @@ export function MetricControl({
84102
e.preventDefault()
85103

86104
const direction = e.deltaY < 0 ? 1 : -1
87-
let scrollStep = step / multiplier
88-
if (e.shiftKey) scrollStep = (step * 10) / multiplier
89-
else if (e.altKey) scrollStep = (step * 0.1) / multiplier
105+
let scrollStep = toStoredValue(step)
106+
if (e.shiftKey) scrollStep = toStoredValue(step * 10)
107+
else if (e.altKey) scrollStep = toStoredValue(step * 0.1)
90108

91109
const newValue = clamp(valueRef.current + direction * scrollStep)
92-
const finalValue = Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier
110+
const finalValue = roundStoredValueForDisplayPrecision(newValue)
93111

94112
if (Math.abs(finalValue - valueRef.current) > 1e-6) {
95113
applyCommittedValue(finalValue)
@@ -98,7 +116,7 @@ export function MetricControl({
98116

99117
container.addEventListener('wheel', handleWheel, { passive: false })
100118
return () => container.removeEventListener('wheel', handleWheel)
101-
}, [isEditing, step, clamp, applyCommittedValue, precision, multiplier])
119+
}, [isEditing, step, clamp, applyCommittedValue, toStoredValue, roundStoredValueForDisplayPrecision])
102120

103121
useEffect(() => {
104122
if (!isHovered || isEditing) return
@@ -110,13 +128,12 @@ export function MetricControl({
110128

111129
if (direction !== 0) {
112130
e.preventDefault()
113-
let scrollStep = step / multiplier
114-
if (e.shiftKey) scrollStep = (step * 10) / multiplier
115-
else if (e.altKey) scrollStep = (step * 0.1) / multiplier
131+
let scrollStep = toStoredValue(step)
132+
if (e.shiftKey) scrollStep = toStoredValue(step * 10)
133+
else if (e.altKey) scrollStep = toStoredValue(step * 0.1)
116134

117135
const newValue = clamp(valueRef.current + direction * scrollStep)
118-
const finalValue =
119-
Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier
136+
const finalValue = roundStoredValueForDisplayPrecision(newValue)
120137

121138
if (Math.abs(finalValue - valueRef.current) > 1e-6) {
122139
applyCommittedValue(finalValue)
@@ -126,7 +143,15 @@ export function MetricControl({
126143

127144
window.addEventListener('keydown', handleKeyDown)
128145
return () => window.removeEventListener('keydown', handleKeyDown)
129-
}, [isHovered, isEditing, step, clamp, applyCommittedValue, precision, multiplier])
146+
}, [
147+
isHovered,
148+
isEditing,
149+
step,
150+
clamp,
151+
applyCommittedValue,
152+
toStoredValue,
153+
roundStoredValueForDisplayPrecision,
154+
])
130155

131156
const handlePointerDown = useCallback(
132157
(e: React.PointerEvent) => {
@@ -143,14 +168,13 @@ export function MetricControl({
143168
const handlePointerMove = (moveEvent: PointerEvent) => {
144169
const deltaX = moveEvent.clientX - startXRef.current
145170

146-
let dragStep = step / multiplier
147-
if (moveEvent.shiftKey) dragStep = (step * 10) / multiplier
148-
else if (moveEvent.altKey) dragStep = (step * 0.1) / multiplier
171+
let dragStep = toStoredValue(step)
172+
if (moveEvent.shiftKey) dragStep = toStoredValue(step * 10)
173+
else if (moveEvent.altKey) dragStep = toStoredValue(step * 0.1)
149174

150175
const deltaValue = deltaX * dragStep
151176
const newValue = clamp(startValueRef.current + deltaValue)
152-
const newFinalValue =
153-
Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier
177+
const newFinalValue = roundStoredValueForDisplayPrecision(newValue)
154178

155179
if (Math.abs(newFinalValue - finalValue) > 1e-6) {
156180
finalValue = newFinalValue
@@ -182,13 +206,23 @@ export function MetricControl({
182206
document.addEventListener('pointermove', handlePointerMove)
183207
document.addEventListener('pointerup', handlePointerUp)
184208
},
185-
[isEditing, value, onChange, onCommit, restoreOnCommit, clamp, precision, step, multiplier],
209+
[
210+
isEditing,
211+
value,
212+
onChange,
213+
onCommit,
214+
restoreOnCommit,
215+
clamp,
216+
step,
217+
toStoredValue,
218+
roundStoredValueForDisplayPrecision,
219+
],
186220
)
187221

188222
const handleValueClick = useCallback(() => {
189223
setIsEditing(true)
190-
setInputValue((value * multiplier).toFixed(precision))
191-
}, [value, multiplier, precision])
224+
setInputValue(toDisplayValue(value).toFixed(precision))
225+
}, [value, toDisplayValue, precision])
192226

193227
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
194228
setInputValue(e.target.value)
@@ -197,12 +231,12 @@ export function MetricControl({
197231
const submitValue = useCallback(() => {
198232
const numValue = Number.parseFloat(inputValue)
199233
if (Number.isNaN(numValue)) {
200-
setInputValue((value * multiplier).toFixed(precision))
234+
setInputValue(toDisplayValue(value).toFixed(precision))
201235
} else {
202-
applyCommittedValue(clamp(numValue / multiplier))
236+
applyCommittedValue(clamp(toStoredValue(numValue)))
203237
}
204238
setIsEditing(false)
205-
}, [inputValue, applyCommittedValue, clamp, multiplier, value, precision])
239+
}, [inputValue, applyCommittedValue, clamp, toStoredValue, value, precision, toDisplayValue])
206240

207241
const handleInputBlur = useCallback(() => {
208242
submitValue()
@@ -213,21 +247,21 @@ export function MetricControl({
213247
if (e.key === 'Enter') {
214248
submitValue()
215249
} else if (e.key === 'Escape') {
216-
setInputValue((value * multiplier).toFixed(precision))
250+
setInputValue(toDisplayValue(value).toFixed(precision))
217251
setIsEditing(false)
218252
} else if (e.key === 'ArrowUp') {
219253
e.preventDefault()
220-
const newV = clamp(value + step / multiplier)
254+
const newV = clamp(value + toStoredValue(step))
221255
applyCommittedValue(newV)
222-
setInputValue((newV * multiplier).toFixed(precision))
256+
setInputValue(toDisplayValue(newV).toFixed(precision))
223257
} else if (e.key === 'ArrowDown') {
224258
e.preventDefault()
225-
const newV = clamp(value - step / multiplier)
259+
const newV = clamp(value - toStoredValue(step))
226260
applyCommittedValue(newV)
227-
setInputValue((newV * multiplier).toFixed(precision))
261+
setInputValue(toDisplayValue(newV).toFixed(precision))
228262
}
229263
},
230-
[submitValue, value, multiplier, precision, step, clamp, applyCommittedValue],
264+
[submitValue, value, toDisplayValue, precision, step, clamp, applyCommittedValue, toStoredValue],
231265
)
232266

233267
return (

packages/editor/src/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,14 @@ export {
210210
getActivePaintMaterialLabel,
211211
hasActivePaintMaterial,
212212
} from './lib/material-paint'
213+
export {
214+
formatLinearMeasurement,
215+
getLinearUnitLabel,
216+
type LinearUnit,
217+
linearControlValueToMeters,
218+
linearUnitToMeters,
219+
metersToLinearUnit,
220+
} from './lib/measurements'
213221
export {
214222
addFreshPlacementMetadata,
215223
getPlacementMetadataRecord,

0 commit comments

Comments
 (0)