fix: respect imperial units in wall panel (#321)

This commit is contained in:
MateoSaettone
2026-06-10 07:48:34 -04:00
committed by GitHub
parent ab3c256804
commit 11135b608b
11 changed files with 283 additions and 123 deletions
@@ -74,6 +74,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 { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
import { cn } from '../../lib/utils'
@@ -2614,14 +2615,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) {
@@ -2633,7 +2627,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:
@@ -7,9 +7,9 @@ import { Html } from '@react-three/drei'
import { createPortal, useFrame, useThree } from '@react-three/fiber'
import { useCallback, useMemo, useRef, useState } from 'react'
import { type Camera, type Object3D, Vector3 } from 'three'
import { formatLinearMeasurement } from '../../lib/measurements'
import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
import useEditor from '../../store/use-editor'
import { formatMeasurement } from './measurement-pill'
type ViewportSize = {
width: number
@@ -110,7 +110,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)}
</div>
</Html>
))}
@@ -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
@@ -62,17 +63,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)
@@ -547,8 +537,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
@@ -40,6 +40,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 { resolveAlignmentForActiveBuilding } from '../../../lib/world-grid-snap'
import useEditor from '../../../store/use-editor'
@@ -68,17 +69,6 @@ const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
* floor-plan overlay and the 3D registry move tool. */
const ALIGNMENT_THRESHOLD_M = 0.08
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`
}
/**
* Expand `bounds` outward so each axis is rounded up to the active grid step.
* The wireframe stays centered on the original bounds centre on each axis we
@@ -1869,9 +1859,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,
@@ -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,30 @@ 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 clamp = useCallback(
(val: number) => {
return Math.min(Math.max(val, min), max)
},
[min, max],
)
const roundStoredValueForDisplayPrecision = useCallback(
(storedValue: number) =>
clamp(toStoredValue(Number.parseFloat(toDisplayValue(storedValue).toFixed(precision)))),
[clamp, precision, toDisplayValue, toStoredValue],
)
const displayValue = toDisplayValue(value)
const [isEditing, setIsEditing] = useState(false)
const [isDragging, setIsDragging] = useState(false)
@@ -50,13 +75,6 @@ export function MetricControl({
const valueRef = useRef(value)
valueRef.current = value
const clamp = useCallback(
(val: number) => {
return Math.min(Math.max(val, min), max)
},
[min, max],
)
const applyCommittedValue = useCallback(
(nextValue: number) => {
if (onCommit) {
@@ -84,12 +102,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 +116,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 +128,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 +143,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 +168,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 +206,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<HTMLInputElement>) => {
setInputValue(e.target.value)
@@ -197,12 +231,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 +247,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 (
+8
View File
@@ -210,6 +210,14 @@ export {
getActivePaintMaterialLabel,
hasActivePaintMaterial,
} from './lib/material-paint'
export {
formatLinearMeasurement,
getLinearUnitLabel,
type LinearUnit,
linearControlValueToMeters,
linearUnitToMeters,
metersToLinearUnit,
} from './lib/measurements'
export {
addFreshPlacementMetadata,
getPlacementMetadataRecord,
@@ -0,0 +1,75 @@
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('returns a placeholder for non-finite measurements', () => {
expect(formatLinearMeasurement(NaN, 'imperial')).toBe('--')
expect(formatLinearMeasurement(Infinity, 'imperial')).toBe('--')
expect(formatLinearMeasurement(NaN, 'metric')).toBe('--')
})
test('formats zero measurements', () => {
expect(formatLinearMeasurement(0, 'imperial')).toBe(`0'0"`)
expect(formatLinearMeasurement(0, 'metric')).toBe('0m')
})
test('formats sub-foot imperial measurements', () => {
expect(formatLinearMeasurement(0.1524, 'imperial')).toBe(`0'6"`)
})
test('formats negative measurements with a sign', () => {
expect(formatLinearMeasurement(-0.1524, 'imperial')).toBe(`-0'6"`)
expect(formatLinearMeasurement(-0.1524, 'metric')).toBe('-0.15m')
})
test('converts between meters and the active linear unit', () => {
expect(metersToLinearUnit(0, 'imperial')).toBe(0)
expect(linearUnitToMeters(0, 'imperial')).toBe(0)
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.3, 'imperial', { minMeters: 0.1 })).toBe(0.1)
expect(linearControlValueToMeters(19.7, 'imperial', { maxMeters: 6 })).toBe(6)
expect(linearControlValueToMeters(0.2, 'metric', { minMeters: 0.1 })).toBe(0.2)
expect(linearControlValueToMeters(0.2, 'metric', { maxMeters: 0.15 })).toBe(0.15)
})
test('returns the display label for numeric controls', () => {
expect(getLinearUnitLabel('metric')).toBe('m')
expect(getLinearUnitLabel('imperial')).toBe('ft')
})
})
+58
View File
@@ -0,0 +1,58 @@
export type LinearUnit = 'metric' | 'imperial'
const METERS_PER_FOOT = 0.3048
const FEET_PER_METER = 1 / METERS_PER_FOOT
type LinearControlValueOptions = {
minMeters?: number
maxMeters?: number
}
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: LinearControlValueOptions = {},
): number {
const meters = linearUnitToMeters(value, unit)
const minMeters = options.minMeters ?? Number.NEGATIVE_INFINITY
const maxMeters = options.maxMeters ?? Number.POSITIVE_INFINITY
return Math.min(Math.max(meters, minMeters), maxMeters)
}
export function getLinearUnitLabel(unit: LinearUnit): string {
return unit === 'imperial' ? 'ft' : 'm'
}
export function formatLinearMeasurement(meters: number, unit: LinearUnit): string {
if (!Number.isFinite(meters)) return '--'
const absoluteMeters = Math.abs(meters)
if (unit === 'imperial') {
const feet = metersToLinearUnit(absoluteMeters, unit)
let wholeFeet = Math.floor(feet)
let inches = Math.round((feet - wholeFeet) * 12)
if (inches === 12) {
wholeFeet += 1
inches = 0
}
const sign = meters < 0 && (wholeFeet !== 0 || inches !== 0) ? '-' : ''
return `${sign}${wholeFeet}'${inches}"`
}
const roundedMeters = Number.parseFloat(absoluteMeters.toFixed(2))
const sign = meters < 0 && roundedMeters !== 0 ? '-' : ''
return `${sign}${roundedMeters}m`
}
+2 -12
View File
@@ -20,6 +20,7 @@ import {
EDITOR_LAYER,
type FencePlanPoint,
formatAngleRadians,
formatLinearMeasurement,
getAngleArcToSegmentReference,
getAngleToSegmentReference,
getSegmentAngleReferenceAtPoint,
@@ -94,17 +95,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))
}
@@ -365,7 +355,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 + previewHeight + DRAFT_LABEL_Y_OFFSET,
+54 -23
View File
@@ -14,6 +14,9 @@ import {
import {
ActionButton,
ActionGroup,
getLinearUnitLabel,
linearControlValueToMeters,
metersToLinearUnit,
PanelSection,
PanelWrapper,
SliderControl,
@@ -26,6 +29,7 @@ import { useCallback, useMemo, 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 setCurvingWall = useEditor((s) => s.setCurvingWall)
@@ -117,14 +121,19 @@ 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)
const curveOffsetLimit = Math.max(0.01, maxCurveOffset)
return (
<PanelWrapper
@@ -136,44 +145,66 @@ export default function WallPanel() {
<PanelSection title="Dimensions">
<SliderControl
label="Length"
max={20}
min={0.1}
onChange={handleUpdateLength}
max={metersToLinearUnit(20, unit)}
min={metersToLinearUnit(0.1, unit)}
onChange={(value) =>
handleUpdateLength(
linearControlValueToMeters(value, unit, { maxMeters: 20, minMeters: 0.1 }),
)
}
precision={2}
step={0.01}
unit="m"
value={length}
step={unit === 'imperial' ? 0.1 : 0.01}
unit={unitLabel}
value={displayLength}
/>
<SliderControl
label="Height"
max={6}
min={0.1}
onChange={(v) => handleUpdate({ height: Math.max(0.1, v) })}
max={metersToLinearUnit(6, unit)}
min={metersToLinearUnit(0.1, unit)}
onChange={(v) =>
handleUpdate({
height: linearControlValueToMeters(v, unit, { maxMeters: 6, minMeters: 0.1 }),
})
}
precision={2}
step={0.1}
unit="m"
value={Math.round(height * 100) / 100}
unit={unitLabel}
value={Math.round(displayHeight * 100) / 100}
/>
<SliderControl
label="Thickness"
max={1}
min={0.05}
onChange={(v) => handleUpdate({ thickness: Math.max(0.05, v) })}
max={metersToLinearUnit(1, unit)}
min={metersToLinearUnit(0.05, unit)}
onChange={(v) =>
handleUpdate({
thickness: linearControlValueToMeters(v, unit, { maxMeters: 1, 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 && (
<SliderControl
label="Curve"
max={Math.max(0.01, maxCurveOffset)}
min={-Math.max(0.01, maxCurveOffset)}
onChange={(v) => 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,
linearControlValueToMeters(v, unit, {
maxMeters: curveOffsetLimit,
minMeters: -curveOffsetLimit,
}),
),
})
}
precision={2}
step={0.1}
unit="m"
value={Math.round(curveOffset * 100) / 100}
unit={unitLabel}
value={Math.round(displayCurveOffset * 100) / 100}
/>
)}
</PanelSection>
+2 -12
View File
@@ -16,6 +16,7 @@ import {
createWallOnCurrentLevel,
EDITOR_LAYER,
formatAngleRadians,
formatLinearMeasurement,
getAngleArcToSegmentReference,
getAngleToSegmentReference,
getSegmentAngleReferenceAtPoint,
@@ -124,17 +125,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))
}
@@ -426,7 +416,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 + previewHeight + DRAFT_LABEL_Y_OFFSET,