feat: add production measurement tools (#505)
* feat: add persistent measurement tools * feat: make measurements associative * fix: finish measurements with Escape * feat: improve measurement snapping guides * feat: clarify measurement axis feedback * feat: smart measure lens, zone reports, and direct measurement editing - Smart measurement lens: registry-owned wall/slab/zone hover reports with a single top-center HUD, click-to-pin, latest-event back pressure, and no scene writes - Conservative derived zone quantities (footprint, perimeter, proven enclosure, gross wall/floor surface, flat-room volume) with the selected-zone blueprint panel - Direct editing of committed measurements via selected-only 2D/3D vertex affordances with midpoint insertion, cancellation, and one-write history - Shared measurement surface-query session; 2D tracing joins the slab/ceiling magnetic pipeline with registered-corner snapping - Angle arcs on the smaller angle, indigo active/black resting hierarchy, screen-sized normal-aligned contact rings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: make measurement snapping always magnetic Measurement drafting and committed-edit paths gated wall, semantic, and axis magnetism on isMagneticSnapActive(), which is only true in the 'lines' snapping mode — in the default 'grid' mode corners and wall intersections barely attracted (3D association fell to the 0.012 m verify tolerance, 2D wall radii to the 0.05 m connect stick). Measurement is an analysis tool whose anchors exist to bind real geometry, so its snapping no longer consults the construction snapping-mode chip: 2D/3D drafting and committed vertex edits are always magnetic, Alt is the temporary bypass in both views (releasing the axis pull, wall magnetism, and the 2D projected-geometry pull, and shrinking association to contact tolerance). A discrete 2D wall snap (endpoint / midpoint / crossing) now outranks the locked axis pull, and committed 2D edits route the fallback through the raw pointer so free drags no longer quantize to the construction grid. Volume extrusion height keeps its mode-driven grid quantize. Codex adversarial review confirmed the diagnosis and plumbing; its 2D Alt-depth and grid-quantize findings are applied. New surface-plan-snap tests pin the magnetic override seam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(measurement): stabilize area surface intent --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
22c9472066
commit
ae87ca5475
@@ -1,12 +1,19 @@
|
||||
import { type NodeDefinition, ZoneNode as ZoneNodeSchema } from '@pascal-app/core'
|
||||
import {
|
||||
type NodeDefinition,
|
||||
resolveAutoZonePolygon,
|
||||
ZoneNode as ZoneNodeSchema,
|
||||
} from '@pascal-app/core'
|
||||
import { polygonMeasurementFeatures } from '../shared/polygon-measurement'
|
||||
import { buildZoneFloorplan } from './floorplan'
|
||||
import {
|
||||
zoneAddVertexAffordance,
|
||||
zoneDeleteVertexAffordance,
|
||||
zoneMoveEdgeAffordance,
|
||||
zoneMoveVertexAffordance,
|
||||
} from './floorplan-affordances'
|
||||
import { zoneFloorplanMoveTarget } from './floorplan-move'
|
||||
import { zoneParametrics } from './parametrics'
|
||||
import { zoneQuickMeasurement } from './quick-measurement'
|
||||
import { ZoneNode } from './schema'
|
||||
|
||||
/**
|
||||
@@ -38,6 +45,16 @@ export const zoneDefinition: NodeDefinition<typeof ZoneNode> = {
|
||||
},
|
||||
|
||||
parametrics: zoneParametrics,
|
||||
measurement: {
|
||||
features: (node, ctx) =>
|
||||
polygonMeasurementFeatures({
|
||||
featurePrefix: 'zone',
|
||||
height: 0,
|
||||
label: 'Zone',
|
||||
polygon: resolveAutoZonePolygon(node, ctx.resolve),
|
||||
}),
|
||||
quickMeasure: (node, ctx) => zoneQuickMeasurement(node, ctx),
|
||||
},
|
||||
// No dirty consumer rebuilds this kind — see NodeDefinition.dirtyTracking.
|
||||
dirtyTracking: false,
|
||||
|
||||
@@ -50,18 +67,20 @@ export const zoneDefinition: NodeDefinition<typeof ZoneNode> = {
|
||||
priority: 4,
|
||||
},
|
||||
floorplan: buildZoneFloorplan,
|
||||
floorplanDependencies: (node) => (node.autoFromWalls ? node.boundaryWallIds : []),
|
||||
// 2D body move — centroid-pivot polygon mover (same as slab / ceiling).
|
||||
// Without this, zone fell through to the overlay's generic free-translate
|
||||
// path, which committed a `position` field zone has no schema for, so the
|
||||
// polygon never actually moved on drop.
|
||||
floorplanMoveTarget: zoneFloorplanMoveTarget,
|
||||
// Polygon editor when selected — same three operations slabs / ceilings
|
||||
// Polygon editor when selected — same four operations slabs / ceilings
|
||||
// expose. The shared factories key off `node.polygon`, optional
|
||||
// `node.holes` (absent on zones). See `floorplan-affordances.ts`.
|
||||
floorplanAffordances: {
|
||||
'move-vertex': zoneMoveVertexAffordance,
|
||||
'add-vertex': zoneAddVertexAffordance,
|
||||
'move-edge': zoneMoveEdgeAffordance,
|
||||
'delete-vertex': zoneDeleteVertexAffordance,
|
||||
},
|
||||
|
||||
presentation: {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { ZoneNode } from '@pascal-app/core'
|
||||
import {
|
||||
createPolygonAddVertexAffordance,
|
||||
createPolygonDeleteVertexAffordance,
|
||||
createPolygonMoveEdgeAffordance,
|
||||
createPolygonVertexAffordance,
|
||||
} from '../shared/polygon-vertex-affordance'
|
||||
|
||||
/**
|
||||
* 2D drag affordances for zone — same three polygon-editing operations
|
||||
* 2D affordances for zone — same four polygon-editing operations
|
||||
* slabs and ceilings expose. Zones have no `holes` field, but the
|
||||
* shared factory accepts that case (holeIndex stays undefined and the
|
||||
* boundary polygon is the target).
|
||||
@@ -14,7 +15,25 @@ import {
|
||||
* - `move-vertex` — drag an existing polygon vertex.
|
||||
* - `add-vertex` — insert a new vertex at an edge midpoint, then drag.
|
||||
* - `move-edge` — drag an entire edge perpendicular to itself.
|
||||
* - `delete-vertex` — remove a double-clicked vertex down to three.
|
||||
*/
|
||||
export const zoneMoveVertexAffordance = createPolygonVertexAffordance<ZoneNode>('zone')
|
||||
export const zoneAddVertexAffordance = createPolygonAddVertexAffordance<ZoneNode>('zone')
|
||||
export const zoneMoveEdgeAffordance = createPolygonMoveEdgeAffordance<ZoneNode>('zone')
|
||||
const zoneManualEditOptions = {
|
||||
boundaryCommitData: { autoFromWalls: false, boundaryWallIds: [] },
|
||||
}
|
||||
|
||||
export const zoneMoveVertexAffordance = createPolygonVertexAffordance<ZoneNode>(
|
||||
'zone',
|
||||
zoneManualEditOptions,
|
||||
)
|
||||
export const zoneAddVertexAffordance = createPolygonAddVertexAffordance<ZoneNode>(
|
||||
'zone',
|
||||
zoneManualEditOptions,
|
||||
)
|
||||
export const zoneMoveEdgeAffordance = createPolygonMoveEdgeAffordance<ZoneNode>(
|
||||
'zone',
|
||||
zoneManualEditOptions,
|
||||
)
|
||||
export const zoneDeleteVertexAffordance = createPolygonDeleteVertexAffordance<ZoneNode>(
|
||||
'zone',
|
||||
zoneManualEditOptions,
|
||||
)
|
||||
|
||||
@@ -12,4 +12,9 @@ import { createPolygonCentroidMoveTarget } from '../shared/polygon-centroid-move
|
||||
* polygon mover translates the actual vertices. `meshY = 0`.
|
||||
*/
|
||||
export const zoneFloorplanMoveTarget: FloorplanMoveTarget<ZoneNode> = ({ node, nodes }) =>
|
||||
createPolygonCentroidMoveTarget({ node, nodes, meshY: 0 })
|
||||
createPolygonCentroidMoveTarget({
|
||||
node,
|
||||
nodes,
|
||||
meshY: 0,
|
||||
extraCommitData: node.autoFromWalls ? { autoFromWalls: false, boundaryWallIds: [] } : undefined,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { FloorplanGeometry, FloorplanPoint, GeometryContext, ZoneNode } from '@pascal-app/core'
|
||||
import {
|
||||
type FloorplanGeometry,
|
||||
type FloorplanPoint,
|
||||
type GeometryContext,
|
||||
resolveAutoZonePolygon,
|
||||
type ZoneNode,
|
||||
} from '@pascal-app/core'
|
||||
|
||||
/**
|
||||
* Stage C floor-plan builder for zone. Zones are colored polygons —
|
||||
@@ -11,7 +17,7 @@ import type { FloorplanGeometry, FloorplanPoint, GeometryContext, ZoneNode } fro
|
||||
* furniture in the SVG document order (= z-order).
|
||||
*/
|
||||
export function buildZoneFloorplan(node: ZoneNode, ctx: GeometryContext): FloorplanGeometry | null {
|
||||
const ring = node.polygon
|
||||
const ring = resolveAutoZonePolygon(node, ctx.resolve)
|
||||
if (!ring || ring.length < 3) return null
|
||||
|
||||
const view = ctx.viewState
|
||||
|
||||
@@ -2,4 +2,5 @@ import type { ParametricDescriptor, ZoneNode } from '@pascal-app/core'
|
||||
|
||||
export const zoneParametrics: ParametricDescriptor<ZoneNode> = {
|
||||
groups: [],
|
||||
trailingSection: () => import('./quantities-panel'),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
deriveZoneQuantityReport,
|
||||
resolveAutoZonePolygon,
|
||||
useLiveNodeOverrides,
|
||||
useScene,
|
||||
type ZoneNode,
|
||||
type ZoneQuantityValue,
|
||||
} from '@pascal-app/core'
|
||||
import {
|
||||
formatAreaLabel,
|
||||
formatLinearMeasurement,
|
||||
formatVolumeLabel,
|
||||
PanelSection,
|
||||
} from '@pascal-app/editor'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useMemo } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
type Point2D = readonly [number, number]
|
||||
|
||||
function ZonePlanSketch({
|
||||
edgeLengths,
|
||||
polygon,
|
||||
unit,
|
||||
}: {
|
||||
edgeLengths: readonly number[]
|
||||
polygon: readonly Point2D[]
|
||||
unit: 'metric' | 'imperial'
|
||||
}) {
|
||||
if (polygon.length < 3) {
|
||||
return (
|
||||
<div className="flex h-28 items-center justify-center rounded-md border border-border/50 text-muted-foreground text-xs">
|
||||
Zone boundary unavailable
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const viewWidth = 276
|
||||
const viewHeight = 176
|
||||
const padding = 34
|
||||
const xs = polygon.map((point) => point[0])
|
||||
const ys = polygon.map((point) => point[1])
|
||||
const minX = Math.min(...xs)
|
||||
const maxX = Math.max(...xs)
|
||||
const minY = Math.min(...ys)
|
||||
const maxY = Math.max(...ys)
|
||||
const width = Math.max(maxX - minX, 1e-6)
|
||||
const height = Math.max(maxY - minY, 1e-6)
|
||||
const scale = Math.min((viewWidth - padding * 2) / width, (viewHeight - padding * 2) / height)
|
||||
const offsetX = (viewWidth - width * scale) / 2
|
||||
const offsetY = (viewHeight - height * scale) / 2
|
||||
const projected = polygon.map(
|
||||
([x, y]) => [offsetX + (x - minX) * scale, offsetY + (maxY - y) * scale] as Point2D,
|
||||
)
|
||||
const center = projected.reduce(
|
||||
(sum, point) => [sum[0] + point[0] / projected.length, sum[1] + point[1] / projected.length],
|
||||
[0, 0] as [number, number],
|
||||
)
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-label="Top view with zone edge dimensions"
|
||||
className="h-auto w-full rounded-md border border-cyan-950/20 bg-[#f8faf7]"
|
||||
role="img"
|
||||
viewBox={`0 0 ${viewWidth} ${viewHeight}`}
|
||||
>
|
||||
<defs>
|
||||
<pattern height="12" id="zone-quantity-grid" patternUnits="userSpaceOnUse" width="12">
|
||||
<path d="M 12 0 L 0 0 0 12" fill="none" stroke="#0891b2" strokeOpacity="0.08" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect fill="url(#zone-quantity-grid)" height={viewHeight} width={viewWidth} />
|
||||
<polygon
|
||||
fill="#67e8f9"
|
||||
fillOpacity="0.12"
|
||||
points={projected.map((point) => point.join(',')).join(' ')}
|
||||
stroke="#0e7490"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
{projected.map((start, index) => {
|
||||
const end = projected[(index + 1) % projected.length]
|
||||
if (!end) return null
|
||||
const midpoint: Point2D = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
|
||||
const fromCenter = [midpoint[0] - center[0], midpoint[1] - center[1]] as const
|
||||
const directionLength = Math.hypot(fromCenter[0], fromCenter[1]) || 1
|
||||
const labelPoint: Point2D = [
|
||||
midpoint[0] + (fromCenter[0] / directionLength) * 15,
|
||||
midpoint[1] + (fromCenter[1] / directionLength) * 15,
|
||||
]
|
||||
const label = formatLinearMeasurement(edgeLengths[index] ?? 0, unit)
|
||||
const labelWidth = Math.max(24, label.length * 5.5 + 8)
|
||||
|
||||
return (
|
||||
<g key={`${start[0]}-${start[1]}-${index}`}>
|
||||
<circle cx={start[0]} cy={start[1]} fill="#f8faf7" r="2.5" stroke="#0e7490" />
|
||||
<rect
|
||||
fill="#f8faf7"
|
||||
height="13"
|
||||
rx="2"
|
||||
stroke="#0e7490"
|
||||
strokeOpacity="0.25"
|
||||
width={labelWidth}
|
||||
x={labelPoint[0] - labelWidth / 2}
|
||||
y={labelPoint[1] - 7}
|
||||
/>
|
||||
<text
|
||||
dominantBaseline="middle"
|
||||
fill="#164e63"
|
||||
fontFamily="ui-monospace, SFMono-Regular, monospace"
|
||||
fontSize="7.5"
|
||||
textAnchor="middle"
|
||||
x={labelPoint[0]}
|
||||
y={labelPoint[1]}
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function QuantityRow({
|
||||
abbreviation,
|
||||
format,
|
||||
label,
|
||||
quantity,
|
||||
}: {
|
||||
abbreviation: string
|
||||
format: (value: number) => string
|
||||
label: string
|
||||
quantity: ZoneQuantityValue
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-md border border-border/50 bg-background/35 px-2.5 py-2">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="font-mono font-semibold text-cyan-600 text-[10px]">{abbreviation}</span>
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
<span className="ml-auto font-mono font-medium text-foreground text-xs tabular-nums">
|
||||
{quantity.status === 'available' ? format(quantity.value) : 'Not proven'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] text-muted-foreground leading-snug">
|
||||
{quantity.status === 'available' ? quantity.note : quantity.reason}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ZoneQuantitiesPanel() {
|
||||
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
||||
const unit = useViewer((state) => state.unit)
|
||||
const nodes = useScene((state) => state.nodes)
|
||||
const zone = selectedZoneId ? (nodes[selectedZoneId] as ZoneNode | undefined) : undefined
|
||||
const livePolygon = useLiveNodeOverrides((state) =>
|
||||
selectedZoneId ? state.overrides.get(selectedZoneId)?.polygon : undefined,
|
||||
) as ZoneNode['polygon'] | undefined
|
||||
const boundaryWallIds = zone?.autoFromWalls ? zone.boundaryWallIds : []
|
||||
const boundaryOverrides = useLiveNodeOverrides(
|
||||
useShallow((state) => boundaryWallIds.map((id) => state.overrides.get(id))),
|
||||
)
|
||||
const proceduralPolygon = zone
|
||||
? resolveAutoZonePolygon(zone, (id) => {
|
||||
const dependency = nodes[id]
|
||||
if (!dependency) return undefined
|
||||
const override =
|
||||
boundaryOverrides[boundaryWallIds.indexOf(id as (typeof boundaryWallIds)[number])]
|
||||
return override ? { ...dependency, ...override } : dependency
|
||||
})
|
||||
: undefined
|
||||
const effectiveZone = zone
|
||||
? { ...zone, polygon: livePolygon ?? proceduralPolygon ?? zone.polygon }
|
||||
: undefined
|
||||
const effectiveNodes = useMemo(() => {
|
||||
if (boundaryOverrides.every((override) => !override)) return nodes
|
||||
const merged = { ...nodes }
|
||||
boundaryWallIds.forEach((id, index) => {
|
||||
const dependency = nodes[id]
|
||||
const override = boundaryOverrides[index]
|
||||
if (dependency && override) merged[id] = { ...dependency, ...override }
|
||||
})
|
||||
return merged
|
||||
}, [boundaryOverrides, boundaryWallIds, nodes])
|
||||
const report = useMemo(
|
||||
() => (effectiveZone ? deriveZoneQuantityReport(effectiveZone, effectiveNodes) : null),
|
||||
[effectiveNodes, effectiveZone],
|
||||
)
|
||||
|
||||
if (!effectiveZone || !report) return null
|
||||
|
||||
return (
|
||||
<PanelSection title="Zone quantities">
|
||||
<div className="overflow-hidden rounded-md border border-cyan-950/20 bg-[#f8faf7] text-slate-950">
|
||||
<div className="flex items-center border-cyan-950/15 border-b px-2.5 py-2">
|
||||
<span className="font-semibold text-[11px]">{effectiveZone.name}</span>
|
||||
<span className="ml-auto rounded-full border border-cyan-800/25 bg-cyan-50 px-2 py-0.5 text-cyan-900 text-[9px]">
|
||||
{report.classification === 'enclosed-room' ? 'Enclosed room' : 'Footprint only'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2 px-2.5 py-2 font-mono text-[10px]">
|
||||
<span className="text-cyan-800">A</span>
|
||||
<span>{formatAreaLabel(report.footprintArea, unit, 2)}</span>
|
||||
<span className="ml-auto text-slate-600">P</span>
|
||||
<span>{formatLinearMeasurement(report.perimeter, unit)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ZonePlanSketch
|
||||
edgeLengths={report.edgeLengths}
|
||||
polygon={effectiveZone.polygon}
|
||||
unit={unit}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<QuantityRow
|
||||
abbreviation="Aw"
|
||||
format={(value) => formatAreaLabel(value, unit, 2)}
|
||||
label="Wall surface"
|
||||
quantity={report.wallSurface}
|
||||
/>
|
||||
<QuantityRow
|
||||
abbreviation="Af"
|
||||
format={(value) => formatAreaLabel(value, unit, 2)}
|
||||
label="Floor surface"
|
||||
quantity={report.floorSurface}
|
||||
/>
|
||||
<QuantityRow
|
||||
abbreviation="V"
|
||||
format={(value) => formatVolumeLabel(value, unit, 2)}
|
||||
label="Volume"
|
||||
quantity={report.volume}
|
||||
/>
|
||||
</div>
|
||||
</PanelSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
type GeometryContext,
|
||||
type QuickMeasurementReport,
|
||||
resolveAutoZonePolygon,
|
||||
type ZoneNode,
|
||||
} from '@pascal-app/core'
|
||||
import {
|
||||
polygonBoundaryLength,
|
||||
polygonReportAnchor,
|
||||
polygonSurfaceArea,
|
||||
} from '../shared/quick-measurement'
|
||||
|
||||
export function zoneQuickMeasurement(
|
||||
node: ZoneNode,
|
||||
context?: GeometryContext,
|
||||
): QuickMeasurementReport | null {
|
||||
const polygon = context ? resolveAutoZonePolygon(node, context.resolve) : node.polygon
|
||||
if (polygon.length < 3) return null
|
||||
|
||||
return {
|
||||
title: node.name,
|
||||
kindLabel: 'Zone',
|
||||
anchor: polygonReportAnchor(polygon, 0.08),
|
||||
metrics: [
|
||||
{
|
||||
key: 'area',
|
||||
label: 'Footprint',
|
||||
abbreviation: 'A',
|
||||
quantity: 'area',
|
||||
value: polygonSurfaceArea(polygon),
|
||||
},
|
||||
{
|
||||
key: 'perimeter',
|
||||
label: 'Perimeter',
|
||||
abbreviation: 'P',
|
||||
quantity: 'length',
|
||||
value: polygonBoundaryLength(polygon),
|
||||
},
|
||||
],
|
||||
note: 'Footprint only — room envelope not proven.',
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { useLiveNodeOverrides, useRegistry, type ZoneNode } from '@pascal-app/core'
|
||||
import {
|
||||
type AnyNode,
|
||||
resolveAutoZonePolygon,
|
||||
useLiveNodeOverrides,
|
||||
useRegistry,
|
||||
useScene,
|
||||
type ZoneNode,
|
||||
} from '@pascal-app/core'
|
||||
import { useNodeEvents, useViewer, ZONE_LAYER } from '@pascal-app/viewer'
|
||||
import { Html } from '@react-three/drei'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three'
|
||||
import { color, float, uniform, uv } from 'three/tsl'
|
||||
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
const Y_OFFSET = 0.01
|
||||
const WALL_HEIGHT = 2.3
|
||||
@@ -123,7 +131,25 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
||||
const livePolygon = useLiveNodeOverrides((s) => s.overrides.get(node.id)?.polygon) as
|
||||
| Array<[number, number]>
|
||||
| undefined
|
||||
const polygon = livePolygon ?? node?.polygon
|
||||
const dependencyIds = node.autoFromWalls ? node.boundaryWallIds : []
|
||||
const dependencyNodes = useScene(
|
||||
useShallow((state) => dependencyIds.map((id) => state.nodes[id])),
|
||||
)
|
||||
const dependencyOverrides = useLiveNodeOverrides(
|
||||
useShallow((state) => dependencyIds.map((id) => state.overrides.get(id))),
|
||||
)
|
||||
const proceduralPolygon = useMemo(
|
||||
() =>
|
||||
resolveAutoZonePolygon(node, (id) => {
|
||||
const index = dependencyIds.indexOf(id as (typeof dependencyIds)[number])
|
||||
const dependency = dependencyNodes[index]
|
||||
if (!dependency) return undefined
|
||||
const override = dependencyOverrides[index]
|
||||
return override ? ({ ...dependency, ...override } as AnyNode) : dependency
|
||||
}),
|
||||
[dependencyIds, dependencyNodes, dependencyOverrides, node],
|
||||
)
|
||||
const polygon = livePolygon ?? proceduralPolygon
|
||||
|
||||
// Create floor shape from polygon
|
||||
const floorShape = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user