@pascal-app/editor 0.4.0 → 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 +341 -48
- package/src/components/editor/floating-building-action-menu.tsx +70 -0
- package/src/components/editor/floorplan-panel.tsx +1350 -722
- package/src/components/editor/index.tsx +221 -167
- package/src/components/editor/node-action-menu.tsx +40 -11
- package/src/components/editor/selection-manager.tsx +238 -10
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +422 -79
- 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/systems/stair/stair-edit-system.tsx +27 -5
- package/src/components/tools/building/move-building-tool.tsx +157 -0
- 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-math.ts +1 -1
- package/src/components/tools/door/door-tool.tsx +31 -7
- package/src/components/tools/door/move-door-tool.tsx +27 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +137 -0
- package/src/components/tools/fence/fence-tool.tsx +190 -0
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
- package/src/components/tools/fence/move-fence-tool.tsx +231 -0
- package/src/components/tools/item/item-tool.tsx +3 -3
- package/src/components/tools/item/move-tool.tsx +16 -0
- package/src/components/tools/item/placement-math.ts +14 -6
- package/src/components/tools/item/placement-strategies.ts +17 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +123 -16
- package/src/components/tools/roof/move-roof-tool.tsx +90 -26
- package/src/components/tools/roof/roof-tool.tsx +6 -6
- package/src/components/tools/select/box-select-tool.tsx +2 -2
- 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/slab/slab-tool.tsx +4 -4
- package/src/components/tools/stair/stair-defaults.ts +10 -0
- package/src/components/tools/stair/stair-tool.tsx +39 -8
- package/src/components/tools/tool-manager.tsx +54 -14
- 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/wall/wall-tool.tsx +19 -29
- package/src/components/tools/window/move-window-tool.tsx +27 -8
- package/src/components/tools/window/window-math.ts +1 -1
- package/src/components/tools/window/window-tool.tsx +31 -7
- package/src/components/tools/zone/zone-tool.tsx +7 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -4
- package/src/components/ui/action-menu/structure-tools.tsx +1 -0
- 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/helpers/building-helper.tsx +32 -0
- package/src/components/ui/helpers/helper-manager.tsx +2 -0
- 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 +269 -0
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/panel-manager.tsx +32 -27
- 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 +377 -50
- 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 +28 -17
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +65 -53
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +40 -25
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +69 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +88 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +14 -13
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +64 -53
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +32 -23
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +72 -51
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +40 -37
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +72 -51
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +13 -13
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -17
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +62 -54
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +40 -25
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +27 -28
- package/src/components/ui/viewer-toolbar.tsx +55 -2
- package/src/components/viewer-overlay.tsx +26 -19
- package/src/hooks/use-auto-save.ts +3 -6
- package/src/hooks/use-contextual-tools.ts +25 -16
- package/src/hooks/use-grid-events.ts +13 -1
- package/src/hooks/use-keyboard.ts +7 -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 +125 -10
|
@@ -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 => {
|
|
@@ -26,6 +34,7 @@ const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
|
|
26
34
|
|
|
27
35
|
type SelectableNodeType =
|
|
28
36
|
| 'wall'
|
|
37
|
+
| 'fence'
|
|
29
38
|
| 'item'
|
|
30
39
|
| 'building'
|
|
31
40
|
| 'zone'
|
|
@@ -67,6 +76,123 @@ export const resolveBuildingId = (
|
|
|
67
76
|
return null
|
|
68
77
|
}
|
|
69
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
|
+
|
|
70
196
|
const HIGHLIGHT_PROFILES = {
|
|
71
197
|
delete: {
|
|
72
198
|
color: new Color('#dc2626'),
|
|
@@ -142,7 +268,9 @@ function createHighlightedMaterials(
|
|
|
142
268
|
|
|
143
269
|
function disposeHighlightedMaterials(material: Material | Material[]) {
|
|
144
270
|
if (Array.isArray(material)) {
|
|
145
|
-
material.forEach((entry) =>
|
|
271
|
+
material.forEach((entry) => {
|
|
272
|
+
entry.dispose()
|
|
273
|
+
})
|
|
146
274
|
return
|
|
147
275
|
}
|
|
148
276
|
|
|
@@ -184,6 +312,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
184
312
|
structure: {
|
|
185
313
|
types: [
|
|
186
314
|
'wall',
|
|
315
|
+
'fence',
|
|
187
316
|
'item',
|
|
188
317
|
'zone',
|
|
189
318
|
'slab',
|
|
@@ -236,6 +365,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
236
365
|
}
|
|
237
366
|
if (
|
|
238
367
|
node.type === 'wall' ||
|
|
368
|
+
node.type === 'fence' ||
|
|
239
369
|
node.type === 'slab' ||
|
|
240
370
|
node.type === 'ceiling' ||
|
|
241
371
|
node.type === 'roof' ||
|
|
@@ -297,6 +427,7 @@ const getSelectionTarget = (node: AnyNode): SelectionTarget | null => {
|
|
|
297
427
|
|
|
298
428
|
if (
|
|
299
429
|
node.type === 'wall' ||
|
|
430
|
+
node.type === 'fence' ||
|
|
300
431
|
node.type === 'slab' ||
|
|
301
432
|
node.type === 'ceiling' ||
|
|
302
433
|
node.type === 'roof' ||
|
|
@@ -340,6 +471,8 @@ export const SelectionManager = () => {
|
|
|
340
471
|
const clickHandledRef = useRef(false)
|
|
341
472
|
|
|
342
473
|
const movingNode = useEditor((s) => s.movingNode)
|
|
474
|
+
const curvingWall = useEditor((s) => s.curvingWall)
|
|
475
|
+
const curvingFence = useEditor((s) => s.curvingFence)
|
|
343
476
|
|
|
344
477
|
useEffect(() => {
|
|
345
478
|
setHoverHighlightMode(mode === 'delete' ? 'delete' : 'default')
|
|
@@ -378,7 +511,7 @@ export const SelectionManager = () => {
|
|
|
378
511
|
|
|
379
512
|
useEffect(() => {
|
|
380
513
|
if (mode !== 'select') return
|
|
381
|
-
if (movingNode) return
|
|
514
|
+
if (movingNode || curvingWall || curvingFence) return
|
|
382
515
|
|
|
383
516
|
const onClick = (event: NodeEvent) => {
|
|
384
517
|
// Skip if box-select just completed (drag ended over a node)
|
|
@@ -389,7 +522,8 @@ export const SelectionManager = () => {
|
|
|
389
522
|
let currentStructureLayer = useEditor.getState().structureLayer
|
|
390
523
|
|
|
391
524
|
// Auto-switch between zones, structure, and furnish when clicking elements on the same level.
|
|
392
|
-
|
|
525
|
+
// Also auto-switch from site phase when clicking structural/furnish elements (e.g. 2D floorplan).
|
|
526
|
+
if (currentPhase === 'structure' || currentPhase === 'furnish' || currentPhase === 'site') {
|
|
393
527
|
if (isNodeInCurrentLevel(node)) {
|
|
394
528
|
const target = getSelectionTarget(node)
|
|
395
529
|
if (target) {
|
|
@@ -431,6 +565,42 @@ export const SelectionManager = () => {
|
|
|
431
565
|
|
|
432
566
|
activeStrategy.handleSelect(nodeToSelect, event.nativeEvent, modifierKeysRef.current)
|
|
433
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
|
+
|
|
434
604
|
// Reset the handled flag after a short delay to allow grid:click to be ignored
|
|
435
605
|
setTimeout(() => {
|
|
436
606
|
clickHandledRef.current = false
|
|
@@ -440,6 +610,7 @@ export const SelectionManager = () => {
|
|
|
440
610
|
|
|
441
611
|
const allTypes = [
|
|
442
612
|
'wall',
|
|
613
|
+
'fence',
|
|
443
614
|
'item',
|
|
444
615
|
'building',
|
|
445
616
|
'zone',
|
|
@@ -462,6 +633,7 @@ export const SelectionManager = () => {
|
|
|
462
633
|
const { phase, structureLayer } = useEditor.getState()
|
|
463
634
|
const activeStrategy = SELECTION_STRATEGIES[phase]
|
|
464
635
|
if (activeStrategy) activeStrategy.handleDeselect()
|
|
636
|
+
useEditor.getState().setSelectedMaterialTarget(null)
|
|
465
637
|
|
|
466
638
|
// When deselecting from zone mode, return to structure select
|
|
467
639
|
if (phase === 'structure' && structureLayer === 'zones') {
|
|
@@ -477,12 +649,12 @@ export const SelectionManager = () => {
|
|
|
477
649
|
})
|
|
478
650
|
emitter.off('grid:click', onGridClick)
|
|
479
651
|
}
|
|
480
|
-
}, [mode, movingNode])
|
|
652
|
+
}, [curvingFence, curvingWall, mode, movingNode])
|
|
481
653
|
|
|
482
654
|
// Global double-click handler for auto-switching phases and cross-phase hover
|
|
483
655
|
useEffect(() => {
|
|
484
656
|
if (mode !== 'select') return
|
|
485
|
-
if (movingNode) return
|
|
657
|
+
if (movingNode || curvingWall || curvingFence) return
|
|
486
658
|
|
|
487
659
|
const onEnter = (event: NodeEvent) => {
|
|
488
660
|
const node = event.node
|
|
@@ -534,6 +706,7 @@ export const SelectionManager = () => {
|
|
|
534
706
|
}
|
|
535
707
|
} else if (
|
|
536
708
|
node.type === 'wall' ||
|
|
709
|
+
node.type === 'fence' ||
|
|
537
710
|
node.type === 'slab' ||
|
|
538
711
|
node.type === 'ceiling' ||
|
|
539
712
|
node.type === 'roof' ||
|
|
@@ -583,6 +756,7 @@ export const SelectionManager = () => {
|
|
|
583
756
|
|
|
584
757
|
const allTypes = [
|
|
585
758
|
'wall',
|
|
759
|
+
'fence',
|
|
586
760
|
'item',
|
|
587
761
|
'building',
|
|
588
762
|
'slab',
|
|
@@ -609,7 +783,7 @@ export const SelectionManager = () => {
|
|
|
609
783
|
emitter.off(`${type}:double-click` as any, onDoubleClick as any)
|
|
610
784
|
})
|
|
611
785
|
}
|
|
612
|
-
}, [mode, movingNode])
|
|
786
|
+
}, [curvingFence, curvingWall, mode, movingNode])
|
|
613
787
|
|
|
614
788
|
// Delete mode: click-to-delete (sledgehammer tool)
|
|
615
789
|
useEffect(() => {
|
|
@@ -654,6 +828,7 @@ export const SelectionManager = () => {
|
|
|
654
828
|
|
|
655
829
|
const allTypes = [
|
|
656
830
|
'wall',
|
|
831
|
+
'fence',
|
|
657
832
|
'item',
|
|
658
833
|
'slab',
|
|
659
834
|
'ceiling',
|
|
@@ -692,6 +867,12 @@ export const SelectionManager = () => {
|
|
|
692
867
|
}
|
|
693
868
|
|
|
694
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
|
+
|
|
695
876
|
useEffect(() => {
|
|
696
877
|
return useScene.subscribe((state) => {
|
|
697
878
|
const { buildingId, levelId, zoneId, selectedIds } = useViewer.getState().selection
|
|
@@ -720,6 +901,28 @@ const SelectionStateSync = () => {
|
|
|
720
901
|
})
|
|
721
902
|
}, [])
|
|
722
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
|
+
|
|
723
926
|
return null
|
|
724
927
|
}
|
|
725
928
|
|
|
@@ -824,6 +1027,31 @@ const SelectionMaterialSync = () => {
|
|
|
824
1027
|
})
|
|
825
1028
|
}, [syncSelectionMaterials])
|
|
826
1029
|
|
|
1030
|
+
useEffect(() => {
|
|
1031
|
+
const restoreForCapture = () => {
|
|
1032
|
+
for (const [mesh, entry] of highlightedMaterialsRef.current.entries()) {
|
|
1033
|
+
if (mesh.material === entry.highlightedMaterial) {
|
|
1034
|
+
mesh.material = entry.originalMaterial
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const reapplyAfterCapture = () => {
|
|
1040
|
+
for (const [mesh, entry] of highlightedMaterialsRef.current.entries()) {
|
|
1041
|
+
if (mesh.material === entry.originalMaterial) {
|
|
1042
|
+
mesh.material = entry.highlightedMaterial
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
emitter.on('thumbnail:before-capture', restoreForCapture)
|
|
1048
|
+
emitter.on('thumbnail:after-capture', reapplyAfterCapture)
|
|
1049
|
+
return () => {
|
|
1050
|
+
emitter.off('thumbnail:before-capture', restoreForCapture)
|
|
1051
|
+
emitter.off('thumbnail:after-capture', reapplyAfterCapture)
|
|
1052
|
+
}
|
|
1053
|
+
}, [])
|
|
1054
|
+
|
|
827
1055
|
useEffect(() => {
|
|
828
1056
|
return () => {
|
|
829
1057
|
for (const [mesh, entry] of highlightedMaterialsRef.current.entries()) {
|
|
@@ -883,13 +1111,13 @@ const EditorOutlinerSync = () => {
|
|
|
883
1111
|
outliner.selectedObjects.length = 0
|
|
884
1112
|
for (const id of idsToHighlight) {
|
|
885
1113
|
const obj = sceneRegistry.nodes.get(id)
|
|
886
|
-
if (obj) outliner.selectedObjects.push(obj)
|
|
1114
|
+
if (obj?.parent) outliner.selectedObjects.push(obj)
|
|
887
1115
|
}
|
|
888
1116
|
|
|
889
1117
|
outliner.hoveredObjects.length = 0
|
|
890
1118
|
if (hoveredId) {
|
|
891
1119
|
const obj = sceneRegistry.nodes.get(hoveredId)
|
|
892
|
-
if (obj) outliner.hoveredObjects.push(obj)
|
|
1120
|
+
if (obj?.parent) outliner.hoveredObjects.push(obj)
|
|
893
1121
|
}
|
|
894
1122
|
}, [phase, previewSelectedIds, selection, hoveredId, outliner])
|
|
895
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'
|