@pascal-app/editor 0.5.1 → 0.6.0
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 -7
- package/src/components/editor/editor-layout-v2.tsx +9 -0
- package/src/components/editor/floating-action-menu.tsx +255 -34
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-panel.tsx +1323 -713
- package/src/components/editor/index.tsx +2 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +200 -8
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +319 -157
- package/src/components/editor/wall-measurement-label.tsx +120 -32
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/door/door-tool.tsx +12 -0
- package/src/components/tools/door/move-door-tool.tsx +10 -0
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +19 -7
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
- package/src/components/tools/fence/move-fence-tool.tsx +8 -0
- package/src/components/tools/item/move-tool.tsx +9 -0
- package/src/components/tools/item/placement-math.ts +14 -6
- package/src/components/tools/item/placement-strategies.ts +2 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +42 -10
- package/src/components/tools/roof/move-roof-tool.tsx +89 -28
- package/src/components/tools/shared/polygon-editor.tsx +98 -8
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +12 -0
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +322 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +331 -9
- package/src/components/tools/window/move-window-tool.tsx +10 -0
- package/src/components/tools/window/window-tool.tsx +12 -0
- package/src/components/ui/action-menu/control-modes.tsx +9 -4
- package/src/components/ui/command-palette/editor-commands.tsx +9 -4
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +127 -94
- package/src/components/ui/controls/slider-control.tsx +28 -14
- package/src/components/ui/item-catalog/catalog-items.tsx +5 -0
- package/src/components/ui/panels/ceiling-panel.tsx +61 -17
- package/src/components/ui/panels/door-panel.tsx +5 -5
- package/src/components/ui/panels/fence-panel.tsx +97 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/panel-manager.tsx +31 -29
- package/src/components/ui/panels/reference-panel.tsx +5 -4
- package/src/components/ui/panels/roof-panel.tsx +91 -22
- package/src/components/ui/panels/roof-segment-panel.tsx +23 -13
- package/src/components/ui/panels/slab-panel.tsx +63 -15
- package/src/components/ui/panels/stair-panel.tsx +173 -19
- package/src/components/ui/panels/stair-segment-panel.tsx +28 -17
- package/src/components/ui/panels/wall-panel.tsx +159 -11
- package/src/components/ui/panels/window-panel.tsx +5 -7
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +29 -32
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +7 -3
- package/src/components/ui/viewer-toolbar.tsx +55 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +3 -2
- package/src/index.tsx +2 -1
- package/src/lib/history.ts +20 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/store/use-editor.tsx +118 -10
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { initSFXBus } from '../../lib/sfx-bus'
|
|
24
24
|
import useEditor from '../../store/use-editor'
|
|
25
25
|
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
26
|
+
import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
|
|
26
27
|
import { RoofEditSystem } from '../systems/roof/roof-edit-system'
|
|
27
28
|
import { StairEditSystem } from '../systems/stair/stair-edit-system'
|
|
28
29
|
import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
|
|
@@ -523,6 +524,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
523
524
|
<ExportManager />
|
|
524
525
|
{isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
|
|
525
526
|
<CeilingSystem />
|
|
527
|
+
<CeilingSelectionAffordanceSystem />
|
|
526
528
|
<RoofEditSystem />
|
|
527
529
|
<StairEditSystem />
|
|
528
530
|
{!isLoading && !isFirstPersonMode && (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
|
-
import { Copy, Move, Trash2 } from 'lucide-react'
|
|
4
|
+
import { Copy, Move, Spline, Trash2 } from 'lucide-react'
|
|
5
5
|
import type { MouseEventHandler, PointerEventHandler } from 'react'
|
|
6
6
|
|
|
7
7
|
type NodeActionMenuProps = {
|
|
@@ -9,6 +9,7 @@ type NodeActionMenuProps = {
|
|
|
9
9
|
onDelete?: MouseEventHandler<HTMLButtonElement>
|
|
10
10
|
onDuplicate?: MouseEventHandler<HTMLButtonElement>
|
|
11
11
|
onMove?: MouseEventHandler<HTMLButtonElement>
|
|
12
|
+
onCurve?: MouseEventHandler<HTMLButtonElement>
|
|
12
13
|
onPointerDown?: PointerEventHandler<HTMLDivElement>
|
|
13
14
|
onPointerUp?: PointerEventHandler<HTMLDivElement>
|
|
14
15
|
onPointerEnter?: PointerEventHandler<HTMLDivElement>
|
|
@@ -20,6 +21,7 @@ export function NodeActionMenu({
|
|
|
20
21
|
onDelete,
|
|
21
22
|
onDuplicate,
|
|
22
23
|
onMove,
|
|
24
|
+
onCurve,
|
|
23
25
|
onPointerDown,
|
|
24
26
|
onPointerUp,
|
|
25
27
|
onPointerEnter,
|
|
@@ -44,6 +46,17 @@ export function NodeActionMenu({
|
|
|
44
46
|
<Move className="h-4 w-4" />
|
|
45
47
|
</button>
|
|
46
48
|
)}
|
|
49
|
+
{onCurve && (
|
|
50
|
+
<button
|
|
51
|
+
aria-label="Curve"
|
|
52
|
+
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
53
|
+
onClick={onCurve}
|
|
54
|
+
title="Curve"
|
|
55
|
+
type="button"
|
|
56
|
+
>
|
|
57
|
+
<Spline className="h-4 w-4" />
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
47
60
|
{onDuplicate && (
|
|
48
61
|
<button
|
|
49
62
|
aria-label="Duplicate"
|
|
@@ -5,16 +5,24 @@ import {
|
|
|
5
5
|
emitter,
|
|
6
6
|
type ItemNode,
|
|
7
7
|
type NodeEvent,
|
|
8
|
+
type RoofEvent,
|
|
9
|
+
type RoofSegmentEvent,
|
|
8
10
|
resolveLevelId,
|
|
9
11
|
sceneRegistry,
|
|
12
|
+
type StairEvent,
|
|
13
|
+
type StairNode,
|
|
14
|
+
type StairSurfaceMaterialRole,
|
|
15
|
+
type StairSegmentEvent,
|
|
10
16
|
useScene,
|
|
17
|
+
type WallEvent,
|
|
18
|
+
type WallSurfaceSide,
|
|
11
19
|
} from '@pascal-app/core'
|
|
12
20
|
|
|
13
21
|
import { useViewer } from '@pascal-app/viewer'
|
|
14
22
|
import { useCallback, useEffect, useRef } from 'react'
|
|
15
|
-
import { Color, type Material, type Mesh, type Object3D } from 'three'
|
|
23
|
+
import { Color, type BufferGeometry, type Material, type Mesh, type Object3D } from 'three'
|
|
16
24
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
17
|
-
import useEditor, { type Phase, type StructureLayer } from './../../store/use-editor'
|
|
25
|
+
import useEditor, { type MaterialTargetRole, type Phase, type StructureLayer } from './../../store/use-editor'
|
|
18
26
|
import { boxSelectHandled } from '../tools/select/box-select-tool'
|
|
19
27
|
|
|
20
28
|
const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
|
@@ -68,6 +76,123 @@ export const resolveBuildingId = (
|
|
|
68
76
|
return null
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
function resolveWallMaterialTarget(event: WallEvent): WallSurfaceSide | null {
|
|
80
|
+
const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
|
|
81
|
+
if (materialIndex === 1) return 'interior'
|
|
82
|
+
if (materialIndex === 2) return 'exterior'
|
|
83
|
+
|
|
84
|
+
const normalZ = event.normal?.[2]
|
|
85
|
+
const localZ = event.localPosition[2]
|
|
86
|
+
const thickness = event.node.thickness ?? 0.1
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
normalZ === undefined ||
|
|
90
|
+
Math.abs(normalZ) < 0.65 ||
|
|
91
|
+
Math.abs(localZ) < Math.max(thickness * 0.2, 0.01)
|
|
92
|
+
) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const hitFace = localZ >= 0 ? 'front' : 'back'
|
|
97
|
+
const semantic = hitFace === 'front' ? event.node.frontSide : event.node.backSide
|
|
98
|
+
|
|
99
|
+
if (semantic === 'interior' || semantic === 'exterior') {
|
|
100
|
+
return semantic
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return hitFace === 'front' ? 'interior' : 'exterior'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveStairMaterialTarget(
|
|
107
|
+
event: StairEvent | StairSegmentEvent,
|
|
108
|
+
): StairSurfaceMaterialRole | null {
|
|
109
|
+
const hitObjectName = event.nativeEvent.object?.name ?? ''
|
|
110
|
+
const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
|
|
111
|
+
|
|
112
|
+
if (hitObjectName.startsWith('stair-railing')) {
|
|
113
|
+
return 'railing'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (hitObjectName.startsWith('stair-side')) {
|
|
117
|
+
return 'side'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (materialIndex === 0) {
|
|
121
|
+
return 'tread'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (materialIndex === 1) {
|
|
125
|
+
return 'side'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const normalY = event.normal?.[1]
|
|
129
|
+
if (normalY !== undefined && normalY > 0.75) {
|
|
130
|
+
return 'tread'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (normalY !== undefined && Math.abs(normalY) <= 0.75) {
|
|
134
|
+
return 'side'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveRoofMaterialTarget(
|
|
141
|
+
event: RoofEvent | RoofSegmentEvent,
|
|
142
|
+
): 'top' | 'edge' | 'wall' | null {
|
|
143
|
+
const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
|
|
144
|
+
if (materialIndex === 3) return 'top'
|
|
145
|
+
if (materialIndex === 0) return 'edge'
|
|
146
|
+
if (materialIndex === 1 || materialIndex === 2) return 'wall'
|
|
147
|
+
|
|
148
|
+
const normalY = event.normal?.[1]
|
|
149
|
+
if (normalY !== undefined && normalY > 0.35) return 'top'
|
|
150
|
+
if (normalY !== undefined && Math.abs(normalY) <= 0.35) return 'edge'
|
|
151
|
+
if (normalY !== undefined && normalY < -0.35) return 'wall'
|
|
152
|
+
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getEventObject(event: NodeEvent): Object3D {
|
|
157
|
+
const eventWithObject = event as NodeEvent & { object?: Object3D }
|
|
158
|
+
return eventWithObject.object ?? event.nativeEvent.object
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getIntersectionMaterialIndex(
|
|
162
|
+
object: Object3D,
|
|
163
|
+
faceIndex: number | undefined,
|
|
164
|
+
): number | undefined {
|
|
165
|
+
if (faceIndex === undefined) return undefined
|
|
166
|
+
|
|
167
|
+
const geometry = (object as Mesh).geometry as BufferGeometry | undefined
|
|
168
|
+
if (!geometry || geometry.groups.length === 0) return undefined
|
|
169
|
+
|
|
170
|
+
const triangleStart = faceIndex * 3
|
|
171
|
+
const group = geometry.groups.find(
|
|
172
|
+
(entry) => triangleStart >= entry.start && triangleStart < entry.start + entry.count,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return group?.materialIndex
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function setSelectedMaterialTargetForNode(
|
|
179
|
+
node: AnyNode,
|
|
180
|
+
role: MaterialTargetRole | null,
|
|
181
|
+
) {
|
|
182
|
+
if (!role) {
|
|
183
|
+
const currentTarget = useEditor.getState().selectedMaterialTarget
|
|
184
|
+
if (currentTarget?.nodeId !== node.id) {
|
|
185
|
+
useEditor.getState().setSelectedMaterialTarget(null)
|
|
186
|
+
}
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
useEditor.getState().setSelectedMaterialTarget({
|
|
191
|
+
nodeId: node.id as AnyNodeId,
|
|
192
|
+
role,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
71
196
|
const HIGHLIGHT_PROFILES = {
|
|
72
197
|
delete: {
|
|
73
198
|
color: new Color('#dc2626'),
|
|
@@ -346,6 +471,8 @@ export const SelectionManager = () => {
|
|
|
346
471
|
const clickHandledRef = useRef(false)
|
|
347
472
|
|
|
348
473
|
const movingNode = useEditor((s) => s.movingNode)
|
|
474
|
+
const curvingWall = useEditor((s) => s.curvingWall)
|
|
475
|
+
const curvingFence = useEditor((s) => s.curvingFence)
|
|
349
476
|
|
|
350
477
|
useEffect(() => {
|
|
351
478
|
setHoverHighlightMode(mode === 'delete' ? 'delete' : 'default')
|
|
@@ -384,7 +511,7 @@ export const SelectionManager = () => {
|
|
|
384
511
|
|
|
385
512
|
useEffect(() => {
|
|
386
513
|
if (mode !== 'select') return
|
|
387
|
-
if (movingNode) return
|
|
514
|
+
if (movingNode || curvingWall || curvingFence) return
|
|
388
515
|
|
|
389
516
|
const onClick = (event: NodeEvent) => {
|
|
390
517
|
// Skip if box-select just completed (drag ended over a node)
|
|
@@ -438,6 +565,42 @@ export const SelectionManager = () => {
|
|
|
438
565
|
|
|
439
566
|
activeStrategy.handleSelect(nodeToSelect, event.nativeEvent, modifierKeysRef.current)
|
|
440
567
|
|
|
568
|
+
let nextMaterialTargetHandled = false
|
|
569
|
+
|
|
570
|
+
if (node.type === 'wall' && nodeToSelect.type === 'wall') {
|
|
571
|
+
setSelectedMaterialTargetForNode(
|
|
572
|
+
nodeToSelect,
|
|
573
|
+
resolveWallMaterialTarget(event as WallEvent),
|
|
574
|
+
)
|
|
575
|
+
nextMaterialTargetHandled = true
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (
|
|
579
|
+
(node.type === 'stair' || node.type === 'stair-segment') &&
|
|
580
|
+
nodeToSelect.type === 'stair'
|
|
581
|
+
) {
|
|
582
|
+
setSelectedMaterialTargetForNode(
|
|
583
|
+
nodeToSelect,
|
|
584
|
+
resolveStairMaterialTarget(event as StairEvent | StairSegmentEvent),
|
|
585
|
+
)
|
|
586
|
+
nextMaterialTargetHandled = true
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (
|
|
590
|
+
(node.type === 'roof' || node.type === 'roof-segment') &&
|
|
591
|
+
nodeToSelect.type === 'roof'
|
|
592
|
+
) {
|
|
593
|
+
setSelectedMaterialTargetForNode(
|
|
594
|
+
nodeToSelect,
|
|
595
|
+
resolveRoofMaterialTarget(event as RoofEvent | RoofSegmentEvent),
|
|
596
|
+
)
|
|
597
|
+
nextMaterialTargetHandled = true
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (!nextMaterialTargetHandled && useEditor.getState().selectedMaterialTarget) {
|
|
601
|
+
useEditor.getState().setSelectedMaterialTarget(null)
|
|
602
|
+
}
|
|
603
|
+
|
|
441
604
|
// Reset the handled flag after a short delay to allow grid:click to be ignored
|
|
442
605
|
setTimeout(() => {
|
|
443
606
|
clickHandledRef.current = false
|
|
@@ -470,6 +633,7 @@ export const SelectionManager = () => {
|
|
|
470
633
|
const { phase, structureLayer } = useEditor.getState()
|
|
471
634
|
const activeStrategy = SELECTION_STRATEGIES[phase]
|
|
472
635
|
if (activeStrategy) activeStrategy.handleDeselect()
|
|
636
|
+
useEditor.getState().setSelectedMaterialTarget(null)
|
|
473
637
|
|
|
474
638
|
// When deselecting from zone mode, return to structure select
|
|
475
639
|
if (phase === 'structure' && structureLayer === 'zones') {
|
|
@@ -485,12 +649,12 @@ export const SelectionManager = () => {
|
|
|
485
649
|
})
|
|
486
650
|
emitter.off('grid:click', onGridClick)
|
|
487
651
|
}
|
|
488
|
-
}, [mode, movingNode])
|
|
652
|
+
}, [curvingFence, curvingWall, mode, movingNode])
|
|
489
653
|
|
|
490
654
|
// Global double-click handler for auto-switching phases and cross-phase hover
|
|
491
655
|
useEffect(() => {
|
|
492
656
|
if (mode !== 'select') return
|
|
493
|
-
if (movingNode) return
|
|
657
|
+
if (movingNode || curvingWall || curvingFence) return
|
|
494
658
|
|
|
495
659
|
const onEnter = (event: NodeEvent) => {
|
|
496
660
|
const node = event.node
|
|
@@ -619,7 +783,7 @@ export const SelectionManager = () => {
|
|
|
619
783
|
emitter.off(`${type}:double-click` as any, onDoubleClick as any)
|
|
620
784
|
})
|
|
621
785
|
}
|
|
622
|
-
}, [mode, movingNode])
|
|
786
|
+
}, [curvingFence, curvingWall, mode, movingNode])
|
|
623
787
|
|
|
624
788
|
// Delete mode: click-to-delete (sledgehammer tool)
|
|
625
789
|
useEffect(() => {
|
|
@@ -703,6 +867,12 @@ export const SelectionManager = () => {
|
|
|
703
867
|
}
|
|
704
868
|
|
|
705
869
|
const SelectionStateSync = () => {
|
|
870
|
+
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
871
|
+
const setSelectedMaterialTarget = useEditor((s) => s.setSelectedMaterialTarget)
|
|
872
|
+
const singleSelectedId = useViewer((s) =>
|
|
873
|
+
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
|
|
874
|
+
)
|
|
875
|
+
|
|
706
876
|
useEffect(() => {
|
|
707
877
|
return useScene.subscribe((state) => {
|
|
708
878
|
const { buildingId, levelId, zoneId, selectedIds } = useViewer.getState().selection
|
|
@@ -731,6 +901,28 @@ const SelectionStateSync = () => {
|
|
|
731
901
|
})
|
|
732
902
|
}, [])
|
|
733
903
|
|
|
904
|
+
useEffect(() => {
|
|
905
|
+
if (!selectedMaterialTarget) return
|
|
906
|
+
|
|
907
|
+
if (!singleSelectedId) {
|
|
908
|
+
setSelectedMaterialTarget(null)
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const selectedNode = useScene.getState().nodes[singleSelectedId as AnyNodeId]
|
|
913
|
+
if (
|
|
914
|
+
!selectedNode ||
|
|
915
|
+
(selectedNode.type !== 'wall' && selectedNode.type !== 'stair' && selectedNode.type !== 'roof')
|
|
916
|
+
) {
|
|
917
|
+
setSelectedMaterialTarget(null)
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (selectedMaterialTarget.nodeId !== selectedNode.id) {
|
|
922
|
+
setSelectedMaterialTarget(null)
|
|
923
|
+
}
|
|
924
|
+
}, [selectedMaterialTarget, setSelectedMaterialTarget, singleSelectedId])
|
|
925
|
+
|
|
734
926
|
return null
|
|
735
927
|
}
|
|
736
928
|
|
|
@@ -919,13 +1111,13 @@ const EditorOutlinerSync = () => {
|
|
|
919
1111
|
outliner.selectedObjects.length = 0
|
|
920
1112
|
for (const id of idsToHighlight) {
|
|
921
1113
|
const obj = sceneRegistry.nodes.get(id)
|
|
922
|
-
if (obj) outliner.selectedObjects.push(obj)
|
|
1114
|
+
if (obj?.parent) outliner.selectedObjects.push(obj)
|
|
923
1115
|
}
|
|
924
1116
|
|
|
925
1117
|
outliner.hoveredObjects.length = 0
|
|
926
1118
|
if (hoveredId) {
|
|
927
1119
|
const obj = sceneRegistry.nodes.get(hoveredId)
|
|
928
|
-
if (obj) outliner.hoveredObjects.push(obj)
|
|
1120
|
+
if (obj?.parent) outliner.hoveredObjects.push(obj)
|
|
929
1121
|
}
|
|
930
1122
|
}, [phase, previewSelectedIds, selection, hoveredId, outliner])
|
|
931
1123
|
|
|
@@ -20,12 +20,18 @@ function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function SiteEdgeLabels() {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// Narrow subscription to just the site node — subscribing to the full
|
|
24
|
+
// s.nodes dict re-rendered this on every wall/level mutation even though
|
|
25
|
+
// the site itself rarely changes.
|
|
26
|
+
const siteNode = useScene((state) => {
|
|
27
|
+
const firstRoot = state.rootNodeIds[0]
|
|
28
|
+
if (!firstRoot) return null
|
|
29
|
+
const node = state.nodes[firstRoot]
|
|
30
|
+
return node?.type === 'site' ? (node as SiteNode) : null
|
|
31
|
+
})
|
|
25
32
|
const unit = useViewer((state) => state.unit)
|
|
26
33
|
const theme = useViewer((state) => state.theme)
|
|
27
34
|
|
|
28
|
-
const siteNode = rootNodeIds[0] ? (nodes[rootNodeIds[0]] as SiteNode) : null
|
|
29
35
|
const siteNodeId = siteNode?.id
|
|
30
36
|
|
|
31
37
|
const isNight = theme === 'dark'
|