Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/editor/src/components/editor/alignment-3d-guide-layer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const Alignment3DGuideLayer = memo(function Alignment3DGuideLayer() {
const guides = useAlignmentGuides((s) => s.guides)
const levelId = useViewer((s) => s.selection.levelId)
const unit = useViewer((s) => s.unit)
const metricNotation = useViewer((s) => s.metricNotation)
const groupRef = useRef<Group>(null)

// Guides carry only XZ in WORLD coords; their Y has to track the active
Expand All @@ -85,16 +86,24 @@ export const Alignment3DGuideLayer = memo(function Alignment3DGuideLayer() {
return (
<group ref={groupRef}>
{guides.map((guide, i) => (
<GuideLine guide={guide} key={i} unit={unit} />
<GuideLine guide={guide} key={i} metricNotation={metricNotation} unit={unit} />
))}
</group>
)
})

function GuideLine({ guide, unit }: { guide: AlignmentGuide; unit: 'metric' | 'imperial' }) {
function GuideLine({
guide,
metricNotation,
unit,
}: {
guide: AlignmentGuide
metricNotation: 'meters' | 'millimeters'
unit: 'metric' | 'imperial'
}) {
const { x: fx, z: fz } = guide.from
const { x: tx, z: tz } = guide.to
const distLabel = formatMeasurement(guide.distance, unit)
const distLabel = formatMeasurement(guide.distance, unit, metricNotation)

// Lay out the dash centres along the from→to direction. The ribbon
// stretches the dash period up if the line is long enough to exceed the
Expand Down
17 changes: 13 additions & 4 deletions packages/editor/src/components/editor/floating-action-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export function FloatingActionMenu() {
const setMovingNode = useEditor((s) => s.setMovingNode)
const setSelection = useViewer((s) => s.setSelection)
const unit = useViewer((s) => s.unit)
const metricNotation = useViewer((s) => s.metricNotation)
// Drives the height-drag dimension pill below the menu. `activeHandleDrag`
// flips only at drag start / end, so subscribing here is cheap — the live
// height value is written imperatively in the useFrame below.
Expand Down Expand Up @@ -418,7 +419,7 @@ export function FloatingActionMenu() {
? getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
: FENCE_DEFAULT_HEIGHT
const liveHeight = override?.height ?? node.height ?? fallbackHeight
pillHeightRef.current.textContent = `H ${formatMeasurement(liveHeight, unit)}`
pillHeightRef.current.textContent = `H ${formatMeasurement(liveHeight, unit, metricNotation)}`
}

const obj = sceneRegistry.nodes.get(selectedId)
Expand Down Expand Up @@ -898,7 +899,7 @@ export function FloatingActionMenu() {
under it for duct fittings. */}
{node && hasPorts(node.type) ? (
<div className="-translate-x-1/2 pointer-events-none absolute bottom-full left-1/2 mb-2 flex flex-col items-center gap-1">
<SystemSummaryPill nodeId={node.id} unit={unit} />
<SystemSummaryPill metricNotation={metricNotation} nodeId={node.id} unit={unit} />
{hasAxisCycling(node.type) ? (
<div className="flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur">
<span className="font-medium text-foreground">
Expand Down Expand Up @@ -932,7 +933,15 @@ export function FloatingActionMenu() {
* subscription it needs (connectivity changes when ANY joint moves) doesn't
* re-render the always-mounted parent menu on every unrelated scene tick.
*/
function SystemSummaryPill({ nodeId, unit }: { nodeId: AnyNodeId; unit: 'metric' | 'imperial' }) {
function SystemSummaryPill({
metricNotation,
nodeId,
unit,
}: {
metricNotation: 'meters' | 'millimeters'
nodeId: AnyNodeId
unit: 'metric' | 'imperial'
}) {
const allNodes = useScene((s) => s.nodes)
const summary = useMemo(() => summarizeSystemFor(nodeId, allNodes), [nodeId, allNodes])
if (!summary) return null
Expand All @@ -949,7 +958,7 @@ function SystemSummaryPill({ nodeId, unit }: { nodeId: AnyNodeId; unit: 'metric'
·
</span>
<span className="text-muted-foreground">
{formatMeasurement(summary.runLengthM, unit)} · {summary.runCount}{' '}
{formatMeasurement(summary.runLengthM, unit, metricNotation)} · {summary.runCount}{' '}
{summary.runCount === 1 ? 'run' : 'runs'}
</span>
</>
Expand Down
24 changes: 12 additions & 12 deletions packages/editor/src/components/editor/measurement-pill.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
'use client'

import { useViewer } from '@pascal-app/viewer'
import { type ForwardedRef, Fragment, forwardRef } from 'react'
import { formatLinearMeasurement, type MetricNotation } from '../../lib/measurements'

// Canonical in-world dimension formatter — metric metres or imperial
// feet/inches. Shared by every measurement readout so they read the same.
export function formatMeasurement(
value: number,
unit: 'metric' | 'imperial',
metricNotation: 'meters' | 'millimeters' = 'meters',
metricNotation: MetricNotation = 'meters',
): string {
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}"`
}
if (metricNotation === 'millimeters') return `${Math.round(value * 1000)}mm`
return `${Number.parseFloat(value.toFixed(2))}m`
return formatLinearMeasurement(value, unit, metricNotation)
}

type MeasurePart = 'height' | 'length' | 'thickness'
Expand Down Expand Up @@ -55,12 +49,18 @@ export function DimensionPill({
primary?: string
primaryRef?: ForwardedRef<HTMLSpanElement>
}) {
const metricNotation = useViewer((state) => state.metricNotation)

return (
<div className="flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur">
{parts.map((part, index) => {
const text = part.signed
? `${part.value < 0 ? '-' : '+'}${formatMeasurement(Math.abs(part.value), unit)}`
: formatMeasurement(part.value, unit)
? `${part.value < 0 ? '-' : '+'}${formatMeasurement(
Math.abs(part.value),
unit,
metricNotation,
)}`
: formatMeasurement(part.value, unit, metricNotation)
return (
<Fragment key={part.key}>
{index > 0 ? (
Expand Down
17 changes: 13 additions & 4 deletions packages/editor/src/components/editor/opening-guides-3d-layer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,26 @@ const mid = (a: OpeningGuideVec3, b: OpeningGuideVec3): OpeningGuideVec3 => [
export const OpeningGuides3DLayer = memo(function OpeningGuides3DLayer() {
const guides = useOpeningGuides((s) => s.guides)
const unit = useViewer((s) => s.unit)
const metricNotation = useViewer((s) => s.metricNotation)
if (guides.length === 0) return null
return (
<>
{guides.map((guide) => (
<OpeningGuide guide={guide} key={guide.id} unit={unit} />
<OpeningGuide guide={guide} key={guide.id} metricNotation={metricNotation} unit={unit} />
))}
</>
)
})

function OpeningGuide({ guide, unit }: { guide: OpeningGuide3D; unit: 'metric' | 'imperial' }) {
function OpeningGuide({
guide,
metricNotation,
unit,
}: {
guide: OpeningGuide3D
metricNotation: 'meters' | 'millimeters'
unit: 'metric' | 'imperial'
}) {
if (guide.kind === 'badge') {
return (
<Html
Expand All @@ -76,7 +85,7 @@ function OpeningGuide({ guide, unit }: { guide: OpeningGuide3D; unit: 'metric' |
className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-sans font-semibold text-[11px] text-white"
style={{ backgroundColor: BADGE_PILL }}
>
{`= ${formatMeasurement(guide.value, unit)}`}
{`= ${formatMeasurement(guide.value, unit, metricNotation)}`}
</div>
</Html>
)
Expand All @@ -97,7 +106,7 @@ function OpeningGuide({ guide, unit }: { guide: OpeningGuide3D; unit: 'metric' |
className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-medium font-sans text-[11px] text-white"
style={{ backgroundColor: DIMENSION_PILL }}
>
{formatMeasurement(guide.value, unit)}
{formatMeasurement(guide.value, unit, metricNotation)}
</div>
</Html>
) : null}
Expand Down
4 changes: 4 additions & 0 deletions packages/editor/src/lib/measurements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ describe('linear measurements', () => {
test('formats metric measurements in whole millimeters', () => {
expect(formatLinearMeasurement(3.456, 'metric', 'millimeters')).toBe('3456mm')
expect(formatLinearMeasurement(-0.1524, 'metric', 'millimeters')).toBe('-152mm')
expect(formatLinearMeasurement(0.2, 'metric', 'millimeters')).toBe('200mm')
expect(formatLinearMeasurement(18.4, 'metric', 'millimeters')).toBe('18400mm')
expect(formatLinearMeasurement(0.0005, 'metric', 'millimeters')).toBe('1mm')
expect(formatLinearMeasurement(-0.0005, 'metric', 'millimeters')).toBe('-1mm')
})

test('formats imperial measurements as feet and inches', () => {
Expand Down
24 changes: 20 additions & 4 deletions packages/nodes/src/dormer/placement-guides.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function DormerPlacementGuides({
movingId?: string
}) {
const unit = useViewer((s) => s.unit)
const metricNotation = useViewer((s) => s.metricNotation)

const [cx, , cz] = center
const faceBounds = getRoofSurfaceFaceBoundsAt(segment, cx, cz)
Expand Down Expand Up @@ -181,22 +182,37 @@ export function DormerPlacementGuides({
return (
<>
{guides.map((g) => (
<Guide key={g.id} guide={g} unit={unit} />
<Guide key={g.id} guide={g} metricNotation={metricNotation} unit={unit} />
))}
</>
)
}

function Guide({ guide, unit }: { guide: DormerGuide; unit: 'metric' | 'imperial' }) {
function Guide({
guide,
metricNotation,
unit,
}: {
guide: DormerGuide
metricNotation: 'meters' | 'millimeters'
unit: 'metric' | 'imperial'
}) {
if (guide.kind === 'badge') {
return <GuideBadge at={guide.at} pill={`= ${formatMeasurement(guide.value, unit)}`} />
return (
<GuideBadge
at={guide.at}
pill={`= ${formatMeasurement(guide.value, unit, metricNotation)}`}
/>
)
}

return (
<GuideLine
from={guide.from}
kind={guide.kind}
pill={guide.value === undefined ? undefined : formatMeasurement(guide.value, unit)}
pill={
guide.value === undefined ? undefined : formatMeasurement(guide.value, unit, metricNotation)
}
to={guide.to}
/>
)
Expand Down
14 changes: 10 additions & 4 deletions packages/nodes/src/measurement/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,10 @@ function buildRenderData(measurement: ResolvedMeasurementPayload): MeasurementRe
function formatMeasurement(
measurement: ResolvedMeasurementPayload,
unit: 'metric' | 'imperial',
metricNotation: 'meters' | 'millimeters',
): string {
if (measurement.kind === 'distance') {
return formatLinearMeasurement(measurementDistance(...measurement.points), unit)
return formatLinearMeasurement(measurementDistance(...measurement.points), unit, metricNotation)
}
if (measurement.kind === 'angle') {
return formatAngleRadians(measurementAngle(...measurement.points))
Expand All @@ -302,7 +303,11 @@ function formatMeasurement(
return `A ${formatAreaLabel(measurementArea(measurement.base), unit)}`
}
if (measurement.kind === 'perimeter') {
return `P ${formatLinearMeasurement(measurementPerimeter(measurement.base), unit)}`
return `P ${formatLinearMeasurement(
measurementPerimeter(measurement.base),
unit,
metricNotation,
)}`
}
return `V ${formatVolumeLabel(measurementPrismVolume(measurement.base, measurement.extrusion), unit)}`
}
Expand All @@ -325,6 +330,7 @@ export const MeasurementRenderer = ({ node }: { node: MeasurementNode }) => {
const handlers = useNodeEvents(node, 'measurement')
const showMeasurements = useViewer((state) => state.showMeasurements)
const unit = useViewer((state) => state.unit)
const metricNotation = useViewer((state) => state.metricNotation)
const active = useViewer(
(state) =>
state.hoveredId === node.id || state.selection.selectedIds.some((id) => id === node.id),
Expand All @@ -350,9 +356,9 @@ export const MeasurementRenderer = ({ node }: { node: MeasurementNode }) => {
})
const data = buildRenderData(resolved.payload)
const label = useMemo(() => {
const value = formatMeasurement(resolved.payload, unit)
const value = formatMeasurement(resolved.payload, unit, metricNotation)
return resolved.dangling.length > 0 ? `Unlinked · ${value}` : value
}, [resolved, unit])
}, [metricNotation, resolved, unit])
const color = measurementPresentationColor(resolved.dangling.length > 0, active)
const lineMaterial = useMemo(
() =>
Expand Down
10 changes: 8 additions & 2 deletions packages/nodes/src/measurement/tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,7 @@ const MeasurementDraftPreview: FC<{
const extrusionHeight = useMeasurementDraft((state) => state.extrusionHeight)
const error = useMeasurementDraft((state) => state.error)
const unit = useViewer((state) => state.unit)
const metricNotation = useViewer((state) => state.metricNotation)
const axisIntersectionCache = useRef<{
intersections: QueriedAxisSurfaceIntersection[]
key: string
Expand Down Expand Up @@ -1525,7 +1526,7 @@ const MeasurementDraftPreview: FC<{
const end = livePoints[livePoints.length - 1]!
label = {
position: localToPreviewFrame(levelObject, buildingObject, midpoint(start, end)),
text: formatLinearMeasurement(measurementDistance(start, end), unit),
text: formatLinearMeasurement(measurementDistance(start, end), unit, metricNotation),
}
} else if (kind === 'angle' && livePoints.length >= 3) {
const anglePoints = livePoints.slice(0, 3) as [
Expand All @@ -1547,7 +1548,11 @@ const MeasurementDraftPreview: FC<{
text:
kind === 'area'
? `A ${formatAreaLabel(measurementArea(livePoints), unit)}`
: `P ${formatLinearMeasurement(measurementPerimeter(livePoints), unit)}`,
: `P ${formatLinearMeasurement(
measurementPerimeter(livePoints),
unit,
metricNotation,
)}`,
}
}
} else if (kind === 'volume' && points.length >= 3 && baseNormal) {
Expand Down Expand Up @@ -1603,6 +1608,7 @@ const MeasurementDraftPreview: FC<{
hoverOwner,
kind,
levelId,
metricNotation,
points,
stage,
surfaceQuery,
Expand Down