@pascal-app/editor 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5829 -12005
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +178 -149
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +433 -156
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -538
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +10 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +320 -37
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useWallSnapIndicator, type WallSnapKind } from '@pascal-app/editor'
|
|
4
|
+
import { memo } from 'react'
|
|
5
|
+
import { useFloorplanRender } from './floorplan-render-context'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* "Magnetic" wall-snap beacon for the 2D floor plan — the top-down twin of the
|
|
9
|
+
* 3D `WallSnapBeaconLayer`. Subscribes to the shared `useWallSnapIndicator`
|
|
10
|
+
* store (published by the floor-plan wall draft + endpoint-move handlers) and
|
|
11
|
+
* draws a marker at the snap point whose shape tells you *what* it caught,
|
|
12
|
+
* matching the 3D glyphs:
|
|
13
|
+
*
|
|
14
|
+
* endpoint (corner) → square midpoint → triangle
|
|
15
|
+
* intersection → ✕ cross wall body (edge) → circle
|
|
16
|
+
*
|
|
17
|
+
* Indigo to match the 3D beacon and stay distinct from the red Figma alignment
|
|
18
|
+
* guides. Sizes are pixel-budgeted via `unitsPerPixel` so the marker stays a
|
|
19
|
+
* constant size on screen at any zoom. Mounted inside the `data-floorplan-scene`
|
|
20
|
+
* group so coordinates are world meters (XZ) 1:1, like the alignment guides.
|
|
21
|
+
*/
|
|
22
|
+
const COLOR = '#6366f1' // indigo-500 — matches the 3D beacon
|
|
23
|
+
|
|
24
|
+
export const FloorplanSnapBeaconLayer = memo(function FloorplanSnapBeaconLayer() {
|
|
25
|
+
const point = useWallSnapIndicator((s) => s.point)
|
|
26
|
+
const ctx = useFloorplanRender()
|
|
27
|
+
|
|
28
|
+
if (!point) return null
|
|
29
|
+
|
|
30
|
+
const upp = ctx?.unitsPerPixel ?? 0.01
|
|
31
|
+
const m = 6 * upp // base half-size of the glyph in world meters
|
|
32
|
+
const stroke = 1.5 * upp
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<g pointerEvents="none">
|
|
36
|
+
<SnapMarker color={COLOR} kind={point.kind} m={m} stroke={stroke} x={point.x} z={point.z} />
|
|
37
|
+
</g>
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
function SnapMarker({
|
|
42
|
+
color,
|
|
43
|
+
kind,
|
|
44
|
+
m,
|
|
45
|
+
stroke,
|
|
46
|
+
x,
|
|
47
|
+
z,
|
|
48
|
+
}: {
|
|
49
|
+
color: string
|
|
50
|
+
kind: WallSnapKind
|
|
51
|
+
m: number
|
|
52
|
+
stroke: number
|
|
53
|
+
x: number
|
|
54
|
+
z: number
|
|
55
|
+
}) {
|
|
56
|
+
if (kind === 'endpoint') {
|
|
57
|
+
return <rect fill={color} height={m * 2} width={m * 2} x={x - m} y={z - m} />
|
|
58
|
+
}
|
|
59
|
+
if (kind === 'midpoint') {
|
|
60
|
+
const t = m * 1.3
|
|
61
|
+
const points = `${x},${z - t} ${x - t},${z + t} ${x + t},${z + t}`
|
|
62
|
+
return <polygon fill={color} points={points} />
|
|
63
|
+
}
|
|
64
|
+
if (kind === 'intersection') {
|
|
65
|
+
const c = m * 1.4
|
|
66
|
+
return (
|
|
67
|
+
<g stroke={color} strokeLinecap="round" strokeWidth={stroke * 2}>
|
|
68
|
+
<line x1={x - c} x2={x + c} y1={z - c} y2={z + c} />
|
|
69
|
+
<line x1={x - c} x2={x + c} y1={z + c} y2={z - c} />
|
|
70
|
+
</g>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
// 'wall' (edge / along-wall) → circle
|
|
74
|
+
return <circle cx={x} cy={z} fill={color} r={m} />
|
|
75
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { memo } from 'react'
|
|
4
|
+
import { useWallMoveGhosts } from '../../store/use-wall-move-ghosts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders translucent dashed previews of bridge walls that the wall
|
|
8
|
+
* junction planner will insert on commit. Mirrors the 3D
|
|
9
|
+
* `GhostWallPreviewMesh` so 2D and 3D show the same intent mid-drag.
|
|
10
|
+
*
|
|
11
|
+
* Subscribes to `useWallMoveGhosts.bridges`; writes happen inside
|
|
12
|
+
* `wallFloorplanMoveTarget.apply` (cleared on `commit` and by the move
|
|
13
|
+
* overlay's unmount cleanup as a safety net).
|
|
14
|
+
*/
|
|
15
|
+
export const FloorplanWallMoveGhostLayer = memo(function FloorplanWallMoveGhostLayer() {
|
|
16
|
+
const bridges = useWallMoveGhosts((s) => s.bridges)
|
|
17
|
+
|
|
18
|
+
if (bridges.length === 0) return null
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<g pointerEvents="none">
|
|
22
|
+
{bridges.map((bridge) => {
|
|
23
|
+
// Draw the bridge as a thick stroke at the wall's plan-space
|
|
24
|
+
// thickness — same convention as the actual wall renderer.
|
|
25
|
+
// Dashes scale with thickness so they read consistently no
|
|
26
|
+
// matter the wall size or viewport zoom (no `non-scaling-stroke`
|
|
27
|
+
// here; we want the line to be N meters thick, not N pixels).
|
|
28
|
+
const dash = bridge.thickness * 1.4
|
|
29
|
+
const gap = bridge.thickness * 0.9
|
|
30
|
+
return (
|
|
31
|
+
<line
|
|
32
|
+
key={bridge.id}
|
|
33
|
+
opacity={0.45}
|
|
34
|
+
stroke={bridge.color}
|
|
35
|
+
strokeDasharray={`${dash} ${gap}`}
|
|
36
|
+
strokeLinecap="butt"
|
|
37
|
+
strokeWidth={bridge.thickness}
|
|
38
|
+
x1={bridge.start[0]}
|
|
39
|
+
x2={bridge.end[0]}
|
|
40
|
+
y1={bridge.start[1]}
|
|
41
|
+
y2={bridge.end[1]}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
})}
|
|
45
|
+
</g>
|
|
46
|
+
)
|
|
47
|
+
})
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type FloorplanGeometry, loadAssetUrl } from '@pascal-app/core'
|
|
4
|
+
import { memo, useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pure-data → SVG converter. Walks a `FloorplanGeometry` tree returned by
|
|
8
|
+
* `def.floorplan(node, ctx)` and emits the matching React-SVG nodes.
|
|
9
|
+
*
|
|
10
|
+
* Coordinates are level-local meters. The wrapping floor-plan panel
|
|
11
|
+
* applies the world→SVG transform via its viewBox, so kinds emit
|
|
12
|
+
* geometry in the same units they reason about in 3D.
|
|
13
|
+
*
|
|
14
|
+
* Group transforms compose: `transform={translate(x y) rotate(deg)}`.
|
|
15
|
+
* Rotations are radians at the data layer (consistent with three.js
|
|
16
|
+
* conventions used by `def.geometry`) and converted to degrees for SVG
|
|
17
|
+
* here — kinds never touch units.
|
|
18
|
+
*
|
|
19
|
+
* Styling props map straight onto SVG attributes. Builders that need
|
|
20
|
+
* theme colors should declare them inline or expose them as registry
|
|
21
|
+
* tokens later (deferred until a real need surfaces — AI-authored kinds
|
|
22
|
+
* can pick safe defaults today).
|
|
23
|
+
*/
|
|
24
|
+
export const FloorplanGeometryRenderer = memo(function FloorplanGeometryRenderer({
|
|
25
|
+
geometry,
|
|
26
|
+
pointerEventsOverride,
|
|
27
|
+
}: {
|
|
28
|
+
geometry: FloorplanGeometry
|
|
29
|
+
pointerEventsOverride?: string
|
|
30
|
+
}) {
|
|
31
|
+
return renderNode(geometry, 0, pointerEventsOverride)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function styleAttrs(
|
|
35
|
+
g: FloorplanGeometry & { kind: Exclude<FloorplanGeometry['kind'], 'group'> },
|
|
36
|
+
pointerEventsOverride?: string,
|
|
37
|
+
) {
|
|
38
|
+
// Shared SVG attribute mapping for any styled primitive. Keeps the per-
|
|
39
|
+
// primitive switch arms terse and ensures new style fields land
|
|
40
|
+
// everywhere at once. `as any` avoids re-asserting every variant
|
|
41
|
+
// includes the style fields — they all do, except `group` (which is
|
|
42
|
+
// filtered out by the caller's type bound).
|
|
43
|
+
const s = g as unknown as {
|
|
44
|
+
fill?: string
|
|
45
|
+
fillOpacity?: number
|
|
46
|
+
stroke?: string
|
|
47
|
+
strokeWidth?: number
|
|
48
|
+
strokeDasharray?: string
|
|
49
|
+
strokeLinecap?: 'butt' | 'round' | 'square'
|
|
50
|
+
strokeLinejoin?: 'miter' | 'round' | 'bevel'
|
|
51
|
+
strokeOpacity?: number
|
|
52
|
+
opacity?: number
|
|
53
|
+
vectorEffect?: 'non-scaling-stroke'
|
|
54
|
+
pointerEvents?: string
|
|
55
|
+
cursor?: string
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
fill: s.fill ?? 'none',
|
|
59
|
+
fillOpacity: s.fillOpacity,
|
|
60
|
+
stroke: s.stroke,
|
|
61
|
+
strokeWidth: s.strokeWidth,
|
|
62
|
+
strokeDasharray: s.strokeDasharray,
|
|
63
|
+
strokeLinecap: s.strokeLinecap,
|
|
64
|
+
strokeLinejoin: s.strokeLinejoin,
|
|
65
|
+
strokeOpacity: s.strokeOpacity,
|
|
66
|
+
opacity: s.opacity,
|
|
67
|
+
vectorEffect: s.vectorEffect,
|
|
68
|
+
pointerEvents: pointerEventsOverride ?? s.pointerEvents,
|
|
69
|
+
style: s.cursor ? { cursor: s.cursor } : undefined,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderNode(
|
|
74
|
+
g: FloorplanGeometry,
|
|
75
|
+
keyHint: number,
|
|
76
|
+
pointerEventsOverride?: string,
|
|
77
|
+
): React.ReactElement | null {
|
|
78
|
+
switch (g.kind) {
|
|
79
|
+
case 'path':
|
|
80
|
+
return <path d={g.d} key={keyHint} {...styleAttrs(g, pointerEventsOverride)} />
|
|
81
|
+
|
|
82
|
+
case 'polygon':
|
|
83
|
+
return (
|
|
84
|
+
<polygon
|
|
85
|
+
key={keyHint}
|
|
86
|
+
points={pointsToAttr(g.points)}
|
|
87
|
+
{...styleAttrs(g, pointerEventsOverride)}
|
|
88
|
+
/>
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
case 'polyline':
|
|
92
|
+
return (
|
|
93
|
+
<polyline
|
|
94
|
+
key={keyHint}
|
|
95
|
+
points={pointsToAttr(g.points)}
|
|
96
|
+
{...styleAttrs(g, pointerEventsOverride)}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
case 'rect':
|
|
101
|
+
return (
|
|
102
|
+
<rect
|
|
103
|
+
height={g.height}
|
|
104
|
+
key={keyHint}
|
|
105
|
+
rx={g.rx}
|
|
106
|
+
ry={g.ry}
|
|
107
|
+
width={g.width}
|
|
108
|
+
x={g.x}
|
|
109
|
+
y={g.y}
|
|
110
|
+
{...styleAttrs(g, pointerEventsOverride)}
|
|
111
|
+
/>
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
case 'circle':
|
|
115
|
+
return (
|
|
116
|
+
<circle
|
|
117
|
+
cx={g.cx}
|
|
118
|
+
cy={g.cy}
|
|
119
|
+
key={keyHint}
|
|
120
|
+
r={g.r}
|
|
121
|
+
{...styleAttrs(g, pointerEventsOverride)}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
case 'line':
|
|
126
|
+
return (
|
|
127
|
+
<line
|
|
128
|
+
key={keyHint}
|
|
129
|
+
x1={g.x1}
|
|
130
|
+
x2={g.x2}
|
|
131
|
+
y1={g.y1}
|
|
132
|
+
y2={g.y2}
|
|
133
|
+
{...styleAttrs(g, pointerEventsOverride)}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
case 'text':
|
|
138
|
+
return (
|
|
139
|
+
<text
|
|
140
|
+
dominantBaseline={g.dominantBaseline ?? 'middle'}
|
|
141
|
+
fill={g.fill ?? '#171717'}
|
|
142
|
+
fontFamily={g.fontFamily}
|
|
143
|
+
fontSize={g.fontSize}
|
|
144
|
+
fontWeight={g.fontWeight}
|
|
145
|
+
key={keyHint}
|
|
146
|
+
opacity={g.opacity}
|
|
147
|
+
paintOrder={g.paintOrder}
|
|
148
|
+
stroke={g.stroke}
|
|
149
|
+
strokeLinecap={g.stroke ? 'round' : undefined}
|
|
150
|
+
strokeLinejoin={g.stroke ? 'round' : undefined}
|
|
151
|
+
strokeWidth={g.strokeWidth}
|
|
152
|
+
textAnchor={g.textAnchor ?? 'start'}
|
|
153
|
+
pointerEvents={pointerEventsOverride}
|
|
154
|
+
x={g.x}
|
|
155
|
+
y={g.y}
|
|
156
|
+
>
|
|
157
|
+
{g.text}
|
|
158
|
+
</text>
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
case 'image':
|
|
162
|
+
return (
|
|
163
|
+
<FloorplanImage
|
|
164
|
+
center={g.center}
|
|
165
|
+
height={g.height}
|
|
166
|
+
key={keyHint}
|
|
167
|
+
opacity={g.opacity}
|
|
168
|
+
preserveAspectRatio={g.preserveAspectRatio ?? 'xMidYMid meet'}
|
|
169
|
+
rotation={g.rotation ?? 0}
|
|
170
|
+
url={g.url}
|
|
171
|
+
width={g.width}
|
|
172
|
+
/>
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
case 'group': {
|
|
176
|
+
const transform = formatTransform(g.transform)
|
|
177
|
+
return (
|
|
178
|
+
<g key={keyHint} transform={transform}>
|
|
179
|
+
{g.children.map((child, i) => renderNode(child, i, pointerEventsOverride))}
|
|
180
|
+
</g>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The interactive primitives (hatch / hit-line / endpoint-handle /
|
|
185
|
+
// dimension-label) need the SVG context + theme palette + units-per-
|
|
186
|
+
// pixel that only the registry layer has access to. They're rendered
|
|
187
|
+
// by `floorplan-registry-layer.tsx`'s interactive walker instead. If
|
|
188
|
+
// a caller routes one of these through this pure renderer it
|
|
189
|
+
// silently drops — the static renderer is for static output.
|
|
190
|
+
default:
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function pointsToAttr(points: readonly (readonly [number, number])[]): string {
|
|
196
|
+
return points.map(([x, y]) => `${x},${y}`).join(' ')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function formatTransform(t?: {
|
|
200
|
+
translate?: readonly [number, number]
|
|
201
|
+
rotate?: number
|
|
202
|
+
}): string | undefined {
|
|
203
|
+
if (!t) return undefined
|
|
204
|
+
const parts: string[] = []
|
|
205
|
+
if (t.translate) parts.push(`translate(${t.translate[0]} ${t.translate[1]})`)
|
|
206
|
+
if (t.rotate !== undefined) parts.push(`rotate(${(t.rotate * 180) / Math.PI})`)
|
|
207
|
+
return parts.length > 0 ? parts.join(' ') : undefined
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* `image` primitive renderer. Resolves the URL asynchronously via
|
|
212
|
+
* `loadAssetUrl` (handles CDN / Supabase storage) and renders an SVG
|
|
213
|
+
* `<image>` centered at `center`, rotated around it, sized in plan-local
|
|
214
|
+
* metres. While the resolution is in flight, renders nothing.
|
|
215
|
+
*/
|
|
216
|
+
function FloorplanImage({
|
|
217
|
+
url,
|
|
218
|
+
center,
|
|
219
|
+
width,
|
|
220
|
+
height,
|
|
221
|
+
rotation,
|
|
222
|
+
preserveAspectRatio,
|
|
223
|
+
opacity,
|
|
224
|
+
}: {
|
|
225
|
+
url: string
|
|
226
|
+
center: readonly [number, number]
|
|
227
|
+
width: number
|
|
228
|
+
height: number
|
|
229
|
+
rotation: number
|
|
230
|
+
preserveAspectRatio: string
|
|
231
|
+
opacity?: number
|
|
232
|
+
}) {
|
|
233
|
+
const [resolvedUrl, setResolvedUrl] = useState<string | null>(null)
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!url) {
|
|
236
|
+
setResolvedUrl(null)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
let cancelled = false
|
|
240
|
+
setResolvedUrl(null)
|
|
241
|
+
loadAssetUrl(url).then((next) => {
|
|
242
|
+
if (!cancelled) setResolvedUrl(next)
|
|
243
|
+
})
|
|
244
|
+
return () => {
|
|
245
|
+
cancelled = true
|
|
246
|
+
}
|
|
247
|
+
}, [url])
|
|
248
|
+
if (!resolvedUrl) return null
|
|
249
|
+
const rotationDeg = (rotation * 180) / Math.PI
|
|
250
|
+
return (
|
|
251
|
+
<g
|
|
252
|
+
pointerEvents="none"
|
|
253
|
+
transform={`translate(${center[0]} ${center[1]}) rotate(${rotationDeg})`}
|
|
254
|
+
>
|
|
255
|
+
<image
|
|
256
|
+
height={height}
|
|
257
|
+
href={resolvedUrl}
|
|
258
|
+
opacity={opacity}
|
|
259
|
+
preserveAspectRatio={preserveAspectRatio}
|
|
260
|
+
width={width}
|
|
261
|
+
x={-width / 2}
|
|
262
|
+
y={-height / 2}
|
|
263
|
+
/>
|
|
264
|
+
</g>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
@@ -12,6 +12,8 @@ const FLOORPLAN_MEASUREMENT_END_TICK = 0.18
|
|
|
12
12
|
export type LinearMeasurementOverlay = {
|
|
13
13
|
dashedExtensions?: boolean
|
|
14
14
|
id: string
|
|
15
|
+
dimensionPathEnd?: string | null
|
|
16
|
+
dimensionPathStart?: string | null
|
|
15
17
|
dimensionLineEnd: { x1: number; y1: number; x2: number; y2: number }
|
|
16
18
|
dimensionLineStart: { x1: number; y1: number; x2: number; y2: number }
|
|
17
19
|
extensionStart: { x1: number; y1: number; x2: number; y2: number }
|
|
@@ -34,6 +36,7 @@ type FloorplanMeasurementPalette = {
|
|
|
34
36
|
type FloorplanMeasurementLineProps = {
|
|
35
37
|
palette: FloorplanMeasurementPalette
|
|
36
38
|
segment: { x1: number; y1: number; x2: number; y2: number }
|
|
39
|
+
path?: string | null
|
|
37
40
|
isSelected?: boolean
|
|
38
41
|
dashed?: boolean
|
|
39
42
|
stroke?: string
|
|
@@ -42,6 +45,7 @@ type FloorplanMeasurementLineProps = {
|
|
|
42
45
|
function FloorplanMeasurementLine({
|
|
43
46
|
palette,
|
|
44
47
|
segment,
|
|
48
|
+
path,
|
|
45
49
|
isSelected,
|
|
46
50
|
dashed = false,
|
|
47
51
|
stroke,
|
|
@@ -50,7 +54,20 @@ function FloorplanMeasurementLine({
|
|
|
50
54
|
? FLOORPLAN_MEASUREMENT_LINE_OPACITY
|
|
51
55
|
: FLOORPLAN_MEASUREMENT_LINE_OPACITY * 0.4
|
|
52
56
|
|
|
53
|
-
return (
|
|
57
|
+
return path ? (
|
|
58
|
+
<path
|
|
59
|
+
d={path}
|
|
60
|
+
fill="none"
|
|
61
|
+
shapeRendering="geometricPrecision"
|
|
62
|
+
stroke={stroke ?? palette.measurementStroke}
|
|
63
|
+
strokeDasharray={dashed ? FLOORPLAN_MEASUREMENT_EXTENSION_DASH : undefined}
|
|
64
|
+
strokeLinecap="round"
|
|
65
|
+
strokeLinejoin="round"
|
|
66
|
+
strokeOpacity={lineOpacity}
|
|
67
|
+
strokeWidth={FLOORPLAN_MEASUREMENT_LINE_WIDTH}
|
|
68
|
+
vectorEffect="non-scaling-stroke"
|
|
69
|
+
/>
|
|
70
|
+
) : (
|
|
54
71
|
<line
|
|
55
72
|
shapeRendering="geometricPrecision"
|
|
56
73
|
stroke={stroke ?? palette.measurementStroke}
|
|
@@ -111,12 +128,26 @@ type FloorplanMeasurementsLayerProps = {
|
|
|
111
128
|
className: string
|
|
112
129
|
measurements: LinearMeasurementOverlay[]
|
|
113
130
|
palette: FloorplanMeasurementPalette
|
|
131
|
+
sceneRotationDeg?: number
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeReadableScreenAngle(angleDeg: number) {
|
|
135
|
+
let normalized = ((((angleDeg + 180) % 360) + 360) % 360) - 180
|
|
136
|
+
|
|
137
|
+
if (normalized > 90) {
|
|
138
|
+
normalized -= 180
|
|
139
|
+
} else if (normalized <= -90) {
|
|
140
|
+
normalized += 180
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return normalized
|
|
114
144
|
}
|
|
115
145
|
|
|
116
146
|
export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLayer({
|
|
117
147
|
className,
|
|
118
148
|
measurements,
|
|
119
149
|
palette,
|
|
150
|
+
sceneRotationDeg = 0,
|
|
120
151
|
}: FloorplanMeasurementsLayerProps) {
|
|
121
152
|
if (measurements.length === 0) {
|
|
122
153
|
return null
|
|
@@ -124,79 +155,90 @@ export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLay
|
|
|
124
155
|
|
|
125
156
|
return (
|
|
126
157
|
<>
|
|
127
|
-
{measurements.map((measurement) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
palette={palette}
|
|
150
|
-
segment={measurement.dimensionLineEnd}
|
|
151
|
-
stroke={measurement.stroke}
|
|
152
|
-
/>
|
|
153
|
-
<FloorplanMeasurementLine
|
|
154
|
-
dashed={measurement.dashedExtensions ?? true}
|
|
155
|
-
isSelected={measurement.isSelected}
|
|
156
|
-
palette={palette}
|
|
157
|
-
segment={measurement.extensionEnd}
|
|
158
|
-
stroke={measurement.extensionStroke}
|
|
159
|
-
/>
|
|
160
|
-
{measurement.showTicks !== false ? (
|
|
161
|
-
<>
|
|
162
|
-
<FloorplanMeasurementTick
|
|
163
|
-
angleDeg={measurement.labelAngleDeg}
|
|
158
|
+
{measurements.map((measurement) =>
|
|
159
|
+
(() => {
|
|
160
|
+
const screenLabelAngleDeg = normalizeReadableScreenAngle(
|
|
161
|
+
measurement.labelAngleDeg + sceneRotationDeg,
|
|
162
|
+
)
|
|
163
|
+
const localLabelAngleDeg = screenLabelAngleDeg - sceneRotationDeg
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<g
|
|
167
|
+
className={className}
|
|
168
|
+
key={measurement.id}
|
|
169
|
+
pointerEvents="none"
|
|
170
|
+
style={{ userSelect: 'none' }}
|
|
171
|
+
>
|
|
172
|
+
<FloorplanMeasurementLine
|
|
173
|
+
dashed={measurement.dashedExtensions ?? true}
|
|
174
|
+
isSelected={measurement.isSelected}
|
|
175
|
+
palette={palette}
|
|
176
|
+
segment={measurement.extensionStart}
|
|
177
|
+
stroke={measurement.extensionStroke}
|
|
178
|
+
/>
|
|
179
|
+
<FloorplanMeasurementLine
|
|
164
180
|
isSelected={measurement.isSelected}
|
|
165
181
|
palette={palette}
|
|
182
|
+
path={measurement.dimensionPathStart}
|
|
183
|
+
segment={measurement.dimensionLineStart}
|
|
166
184
|
stroke={measurement.stroke}
|
|
167
|
-
x={measurement.dimensionLineStart.x1}
|
|
168
|
-
y={measurement.dimensionLineStart.y1}
|
|
169
185
|
/>
|
|
170
|
-
<
|
|
171
|
-
angleDeg={measurement.labelAngleDeg}
|
|
186
|
+
<FloorplanMeasurementLine
|
|
172
187
|
isSelected={measurement.isSelected}
|
|
173
188
|
palette={palette}
|
|
189
|
+
path={measurement.dimensionPathEnd}
|
|
190
|
+
segment={measurement.dimensionLineEnd}
|
|
174
191
|
stroke={measurement.stroke}
|
|
175
|
-
x={measurement.dimensionLineEnd.x2}
|
|
176
|
-
y={measurement.dimensionLineEnd.y2}
|
|
177
192
|
/>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
<FloorplanMeasurementLine
|
|
194
|
+
dashed={measurement.dashedExtensions ?? true}
|
|
195
|
+
isSelected={measurement.isSelected}
|
|
196
|
+
palette={palette}
|
|
197
|
+
segment={measurement.extensionEnd}
|
|
198
|
+
stroke={measurement.extensionStroke}
|
|
199
|
+
/>
|
|
200
|
+
{measurement.showTicks !== false ? (
|
|
201
|
+
<>
|
|
202
|
+
<FloorplanMeasurementTick
|
|
203
|
+
angleDeg={localLabelAngleDeg}
|
|
204
|
+
isSelected={measurement.isSelected}
|
|
205
|
+
palette={palette}
|
|
206
|
+
stroke={measurement.stroke}
|
|
207
|
+
x={measurement.dimensionLineStart.x1}
|
|
208
|
+
y={measurement.dimensionLineStart.y1}
|
|
209
|
+
/>
|
|
210
|
+
<FloorplanMeasurementTick
|
|
211
|
+
angleDeg={localLabelAngleDeg}
|
|
212
|
+
isSelected={measurement.isSelected}
|
|
213
|
+
palette={palette}
|
|
214
|
+
stroke={measurement.stroke}
|
|
215
|
+
x={measurement.dimensionLineEnd.x2}
|
|
216
|
+
y={measurement.dimensionLineEnd.y2}
|
|
217
|
+
/>
|
|
218
|
+
</>
|
|
219
|
+
) : null}
|
|
220
|
+
<text
|
|
221
|
+
dominantBaseline="central"
|
|
222
|
+
fill={measurement.labelFill ?? palette.measurementStroke}
|
|
223
|
+
fillOpacity={
|
|
224
|
+
measurement.isSelected
|
|
225
|
+
? FLOORPLAN_MEASUREMENT_LABEL_OPACITY
|
|
226
|
+
: FLOORPLAN_MEASUREMENT_LABEL_OPACITY * 0.4
|
|
227
|
+
}
|
|
228
|
+
fontFamily="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
|
|
229
|
+
fontSize={FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE}
|
|
230
|
+
fontWeight="600"
|
|
231
|
+
textAnchor="middle"
|
|
232
|
+
transform={`rotate(${localLabelAngleDeg} ${measurement.labelX} ${measurement.labelY}) translate(0, -0.04)`}
|
|
233
|
+
x={measurement.labelX}
|
|
234
|
+
y={measurement.labelY}
|
|
235
|
+
>
|
|
236
|
+
{measurement.label}
|
|
237
|
+
</text>
|
|
238
|
+
</g>
|
|
239
|
+
)
|
|
240
|
+
})(),
|
|
241
|
+
)}
|
|
200
242
|
</>
|
|
201
243
|
)
|
|
202
244
|
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
type FloorplanGeometry,
|
|
7
|
+
type GeometryContext,
|
|
8
|
+
nodeRegistry,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { memo } from 'react'
|
|
12
|
+
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
13
|
+
import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Renders a faint, non-interactive ghost of the node being placed by a
|
|
17
|
+
* registry placement tool (e.g. column), following the cursor in the floor
|
|
18
|
+
* plan. The 3D view shows a translucent mesh preview; in 2D that mesh is
|
|
19
|
+
* hidden (canvas `display:none`), so without this the user only saw the grid
|
|
20
|
+
* cursor dot + alignment guides — no sense of the footprint they were about
|
|
21
|
+
* to drop. The placement tool publishes a transient, already-positioned +
|
|
22
|
+
* aligned node to `usePlacementPreview`; we build its `def.floorplan`
|
|
23
|
+
* footprint with a minimal (unselected) context and render it.
|
|
24
|
+
*
|
|
25
|
+
* Mounted inside the floor-plan scene `<g>` so the geometry's level-local
|
|
26
|
+
* meters get the same world→SVG transform every other entry does.
|
|
27
|
+
*/
|
|
28
|
+
export const FloorplanPlacementPreviewLayer = memo(function FloorplanPlacementPreviewLayer() {
|
|
29
|
+
const node = usePlacementPreview((s) => s.node)
|
|
30
|
+
if (!node) return null
|
|
31
|
+
|
|
32
|
+
const builder = nodeRegistry.get(node.type)?.floorplan
|
|
33
|
+
if (!builder) return null
|
|
34
|
+
|
|
35
|
+
// Minimal, unselected context — preview never shows selection chrome
|
|
36
|
+
// (move handles / resize arrows / hatch live behind `viewState.selected`).
|
|
37
|
+
// `resolve` reads the scene lazily (a builder rarely calls it for a ghost,
|
|
38
|
+
// and `parent: null` short-circuits the elevator's level walk) so the layer
|
|
39
|
+
// never subscribes to / bulk-reads the nodes map during render.
|
|
40
|
+
const ctx = {
|
|
41
|
+
resolve: (id: AnyNodeId) => useScene.getState().nodes[id],
|
|
42
|
+
children: [],
|
|
43
|
+
siblings: [],
|
|
44
|
+
parent: null,
|
|
45
|
+
viewState: undefined,
|
|
46
|
+
} as unknown as GeometryContext
|
|
47
|
+
|
|
48
|
+
const geometry = (builder as (n: AnyNode, c: GeometryContext) => FloorplanGeometry | null)(
|
|
49
|
+
node,
|
|
50
|
+
ctx,
|
|
51
|
+
)
|
|
52
|
+
if (!geometry) return null
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<g data-floorplan-placement-preview opacity={0.5} pointerEvents="none">
|
|
56
|
+
<FloorplanGeometryRenderer geometry={geometry} />
|
|
57
|
+
</g>
|
|
58
|
+
)
|
|
59
|
+
})
|