@pascal-app/editor 0.7.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 +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- 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 +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- 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 +5837 -12006
- 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 +234 -190
- 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 +435 -158
- 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 +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- 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-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- 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 +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- 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 +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- 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 +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- 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 -543
- 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 +57 -1
- 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 +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- 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 +8 -12
- 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 +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -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 +24 -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 +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- 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.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- 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 +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- 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 +342 -91
- 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/tsconfig.json +2 -1
- 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/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- 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 -257
- 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 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- 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 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- 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 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- 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 -230
- 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 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -6,14 +6,22 @@ import {
|
|
|
6
6
|
type ColumnNode,
|
|
7
7
|
emitter,
|
|
8
8
|
type FenceNode,
|
|
9
|
+
getEffectiveRoofSurfaceMaterial,
|
|
10
|
+
getEffectiveSegmentSurfaceMaterial,
|
|
9
11
|
getMaterialPresetByRef,
|
|
12
|
+
getRoofSegmentSurfaceY,
|
|
13
|
+
getSelectableKinds,
|
|
10
14
|
type ItemNode,
|
|
15
|
+
isRegistrySelectable,
|
|
11
16
|
type NodeEvent,
|
|
17
|
+
nodeRegistry,
|
|
12
18
|
type RoofEvent,
|
|
13
19
|
type RoofNode,
|
|
14
20
|
type RoofSegmentEvent,
|
|
21
|
+
type RoofSegmentNode,
|
|
15
22
|
resolveLevelId,
|
|
16
23
|
resolveMaterial,
|
|
24
|
+
type ShelfNode,
|
|
17
25
|
type SlabNode,
|
|
18
26
|
type StairEvent,
|
|
19
27
|
type StairNode,
|
|
@@ -21,9 +29,6 @@ import {
|
|
|
21
29
|
type StairSurfaceMaterialRole,
|
|
22
30
|
sceneRegistry,
|
|
23
31
|
useScene,
|
|
24
|
-
type WallEvent,
|
|
25
|
-
type WallNode,
|
|
26
|
-
type WallSurfaceSide,
|
|
27
32
|
} from '@pascal-app/core'
|
|
28
33
|
|
|
29
34
|
import {
|
|
@@ -33,29 +38,30 @@ import {
|
|
|
33
38
|
getRoofMaterialArray,
|
|
34
39
|
getStairBodyMaterials,
|
|
35
40
|
getStairRailingMaterial,
|
|
36
|
-
getVisibleWallMaterials,
|
|
37
41
|
useViewer,
|
|
38
42
|
} from '@pascal-app/viewer'
|
|
39
43
|
import { useCallback, useEffect, useRef } from 'react'
|
|
40
|
-
import { type BufferGeometry, Color, type Material, type Mesh, type Object3D } from 'three'
|
|
44
|
+
import { type BufferGeometry, Color, type Material, type Mesh, type Object3D, Vector3 } from 'three'
|
|
41
45
|
import {
|
|
42
46
|
type ActivePaintMaterial,
|
|
47
|
+
buildRoofSegmentSurfaceMaterialPatch,
|
|
43
48
|
buildRoofSurfaceMaterialPatch,
|
|
44
49
|
buildSingleSurfaceMaterialPatch,
|
|
45
50
|
buildStairSurfaceMaterialPatch,
|
|
46
|
-
buildWallSurfaceMaterialPatch,
|
|
47
51
|
hasActivePaintMaterial,
|
|
48
52
|
resolveActivePaintMaterialFromSelection,
|
|
49
53
|
} from '../../lib/material-paint'
|
|
50
|
-
import {
|
|
54
|
+
import { emitDeleteSFX } from '../../lib/sfx-bus'
|
|
51
55
|
import useEditor, {
|
|
52
56
|
type MaterialTargetRole,
|
|
53
57
|
type Phase,
|
|
54
58
|
type StructureLayer,
|
|
55
59
|
} from './../../store/use-editor'
|
|
56
|
-
import { boxSelectHandled } from '../tools/select/box-select-
|
|
60
|
+
import { boxSelectHandled } from '../tools/select/box-select-state'
|
|
57
61
|
|
|
58
62
|
const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
|
63
|
+
// Elevators are building-scoped, so they stay selectable across level filters.
|
|
64
|
+
if (node.type === 'elevator') return true
|
|
59
65
|
const currentLevelId = useViewer.getState().selection.levelId
|
|
60
66
|
if (!currentLevelId) return true // No level selected, allow all
|
|
61
67
|
const nodeLevelId = resolveLevelId(node, useScene.getState().nodes)
|
|
@@ -68,6 +74,7 @@ type SelectableNodeType =
|
|
|
68
74
|
| 'item'
|
|
69
75
|
| 'column'
|
|
70
76
|
| 'building'
|
|
77
|
+
| 'elevator'
|
|
71
78
|
| 'zone'
|
|
72
79
|
| 'slab'
|
|
73
80
|
| 'ceiling'
|
|
@@ -118,33 +125,6 @@ export const resolveBuildingId = (
|
|
|
118
125
|
return null
|
|
119
126
|
}
|
|
120
127
|
|
|
121
|
-
function resolveWallMaterialTarget(event: WallEvent): WallSurfaceSide | null {
|
|
122
|
-
const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
|
|
123
|
-
if (materialIndex === 1) return 'interior'
|
|
124
|
-
if (materialIndex === 2) return 'exterior'
|
|
125
|
-
|
|
126
|
-
const normalZ = event.normal?.[2]
|
|
127
|
-
const localZ = event.localPosition[2]
|
|
128
|
-
const thickness = event.node.thickness ?? 0.1
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
normalZ === undefined ||
|
|
132
|
-
Math.abs(normalZ) < 0.65 ||
|
|
133
|
-
Math.abs(localZ) < Math.max(thickness * 0.2, 0.01)
|
|
134
|
-
) {
|
|
135
|
-
return null
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const hitFace = localZ >= 0 ? 'front' : 'back'
|
|
139
|
-
const semantic = hitFace === 'front' ? event.node.frontSide : event.node.backSide
|
|
140
|
-
|
|
141
|
-
if (semantic === 'interior' || semantic === 'exterior') {
|
|
142
|
-
return semantic
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return hitFace === 'front' ? 'interior' : 'exterior'
|
|
146
|
-
}
|
|
147
|
-
|
|
148
128
|
function resolveStairMaterialTarget(
|
|
149
129
|
event: StairEvent | StairSegmentEvent,
|
|
150
130
|
): StairSurfaceMaterialRole | null {
|
|
@@ -226,6 +206,59 @@ function getRegisteredMesh(nodeId: string): Mesh | null {
|
|
|
226
206
|
return object && (object as Mesh).isMesh ? (object as Mesh) : null
|
|
227
207
|
}
|
|
228
208
|
|
|
209
|
+
const roofSelectionWorldPoint = new Vector3()
|
|
210
|
+
|
|
211
|
+
function resolveRoofSegmentSelectionTarget(event: NodeEvent): RoofSegmentNode | null {
|
|
212
|
+
const roof = event.node
|
|
213
|
+
if (roof.type !== 'roof') return null
|
|
214
|
+
|
|
215
|
+
roofSelectionWorldPoint.set(...event.position)
|
|
216
|
+
const nodes = useScene.getState().nodes
|
|
217
|
+
let firstSegment: RoofSegmentNode | null = null
|
|
218
|
+
let bestSegment: { node: RoofSegmentNode; score: number } | null = null
|
|
219
|
+
|
|
220
|
+
for (const childId of roof.children ?? []) {
|
|
221
|
+
const segment = nodes[childId as AnyNodeId] as RoofSegmentNode | undefined
|
|
222
|
+
if (segment?.type !== 'roof-segment') continue
|
|
223
|
+
|
|
224
|
+
const object = getRegisteredNodeObject(segment.id)
|
|
225
|
+
if (!object) continue
|
|
226
|
+
|
|
227
|
+
if (!firstSegment) firstSegment = segment
|
|
228
|
+
|
|
229
|
+
object.updateWorldMatrix(true, false)
|
|
230
|
+
const local = object.worldToLocal(roofSelectionWorldPoint.clone())
|
|
231
|
+
const overhang = segment.overhang ?? 0
|
|
232
|
+
const halfWidth = segment.width / 2 + overhang
|
|
233
|
+
const halfDepth = segment.depth / 2 + overhang
|
|
234
|
+
|
|
235
|
+
if (Math.abs(local.x) > halfWidth || Math.abs(local.z) > halfDepth) {
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const score = Math.abs(local.y - getRoofSegmentSurfaceY(segment, local.x, local.z))
|
|
240
|
+
if (!bestSegment || score < bestSegment.score) {
|
|
241
|
+
bestSegment = { node: segment, score }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return bestSegment?.node ?? firstSegment
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isInActiveRoofContext(
|
|
249
|
+
segment: RoofSegmentNode,
|
|
250
|
+
selectedIds: readonly string[],
|
|
251
|
+
nodes: Record<string, AnyNode>,
|
|
252
|
+
): boolean {
|
|
253
|
+
if (!segment.parentId) return false
|
|
254
|
+
if (selectedIds.includes(segment.id) || selectedIds.includes(segment.parentId)) return true
|
|
255
|
+
|
|
256
|
+
return selectedIds.some((selectedId) => {
|
|
257
|
+
const selectedNode = nodes[selectedId]
|
|
258
|
+
return selectedNode?.type === 'roof-segment' && selectedNode.parentId === segment.parentId
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
229
262
|
function previewMeshMaterial(mesh: Mesh, material: Material | Material[]): PaintPreviewCleanup {
|
|
230
263
|
const previousMaterial = mesh.material
|
|
231
264
|
mesh.material = material
|
|
@@ -243,50 +276,82 @@ function previewCursor(cursor: string): PaintPreviewCleanup {
|
|
|
243
276
|
}
|
|
244
277
|
|
|
245
278
|
function getSingleSurfacePreviewMaterial(material: ActivePaintMaterial): Material | null {
|
|
279
|
+
const shading = useViewer.getState().shading
|
|
280
|
+
|
|
246
281
|
if (material.materialPreset) {
|
|
247
|
-
return createMaterialFromPresetRef(material.materialPreset)
|
|
282
|
+
return createMaterialFromPresetRef(material.materialPreset, shading)
|
|
248
283
|
}
|
|
249
284
|
|
|
250
285
|
if (material.material) {
|
|
251
|
-
return createMaterial(material.material)
|
|
286
|
+
return createMaterial(material.material, shading)
|
|
252
287
|
}
|
|
253
288
|
|
|
254
289
|
return null
|
|
255
290
|
}
|
|
256
291
|
|
|
257
|
-
function
|
|
258
|
-
node:
|
|
259
|
-
role:
|
|
292
|
+
function applyRoofPaintPreview(
|
|
293
|
+
node: RoofNode,
|
|
294
|
+
role: 'top' | 'edge' | 'wall',
|
|
260
295
|
material: ActivePaintMaterial,
|
|
261
296
|
): PaintPreviewCleanup | null {
|
|
262
|
-
const
|
|
297
|
+
const root = getRegisteredNodeObject(node.id)
|
|
298
|
+
const mesh = root?.getObjectByName('merged-roof') as Mesh | undefined
|
|
263
299
|
if (!mesh) return null
|
|
264
300
|
|
|
265
301
|
const previewNode = {
|
|
266
302
|
...node,
|
|
267
|
-
...
|
|
303
|
+
...buildRoofSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
|
268
304
|
}
|
|
305
|
+
const previewMaterial = getRoofMaterialArray(
|
|
306
|
+
previewNode,
|
|
307
|
+
useViewer.getState().shading,
|
|
308
|
+
useViewer.getState().textures,
|
|
309
|
+
useViewer.getState().colorPreset,
|
|
310
|
+
useViewer.getState().sceneTheme,
|
|
311
|
+
)
|
|
312
|
+
if (!previewMaterial) return null
|
|
269
313
|
|
|
270
|
-
return previewMeshMaterial(mesh,
|
|
314
|
+
return previewMeshMaterial(mesh, previewMaterial)
|
|
271
315
|
}
|
|
272
316
|
|
|
273
|
-
function
|
|
274
|
-
node:
|
|
317
|
+
function applyRoofSegmentPaintPreview(
|
|
318
|
+
node: RoofSegmentNode,
|
|
319
|
+
parent: RoofNode | null,
|
|
275
320
|
role: 'top' | 'edge' | 'wall',
|
|
276
321
|
material: ActivePaintMaterial,
|
|
277
322
|
): PaintPreviewCleanup | null {
|
|
278
|
-
const
|
|
279
|
-
const mesh = root?.getObjectByName('merged-roof') as Mesh | undefined
|
|
323
|
+
const mesh = getRegisteredMesh(node.id)
|
|
280
324
|
if (!mesh) return null
|
|
281
325
|
|
|
282
|
-
|
|
326
|
+
// Synthesise the segment node as if the paint had committed, then build
|
|
327
|
+
// the same 4-slot array the renderer would. Mirrors getRoofMaterialArray
|
|
328
|
+
// layout (slot 0 ← edge, 1 ← wall, 2 ← wall, 3 ← top) so the preview
|
|
329
|
+
// material lands on the matching CSG groups.
|
|
330
|
+
const previewNode: RoofSegmentNode = {
|
|
283
331
|
...node,
|
|
284
|
-
...
|
|
332
|
+
...buildRoofSegmentSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
|
285
333
|
}
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
334
|
+
const resolveSlot = (r: 'top' | 'edge' | 'wall'): Material | null => {
|
|
335
|
+
const parentSpec = parent ? getEffectiveRoofSurfaceMaterial(parent, r) : undefined
|
|
336
|
+
const spec = getEffectiveSegmentSurfaceMaterial(previewNode, r, parentSpec)
|
|
337
|
+
if (typeof spec.materialPreset === 'string') {
|
|
338
|
+
const resolved = createMaterialFromPresetRef(spec.materialPreset)
|
|
339
|
+
if (resolved) return resolved
|
|
340
|
+
}
|
|
341
|
+
if (spec.material !== undefined) return createMaterial(spec.material)
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
const edge = resolveSlot('edge')
|
|
345
|
+
const wall = resolveSlot('wall')
|
|
346
|
+
const top = resolveSlot('top')
|
|
347
|
+
if (!(edge || wall || top)) return null
|
|
348
|
+
const fallback = parent ? getRoofMaterialArray(parent) : null
|
|
349
|
+
const fb = (n: number) => fallback?.[n] ?? null
|
|
350
|
+
// Per-role only, then the parent's themed slot — matches the renderer so the
|
|
351
|
+
// preview never bleeds a painted surface onto the segment's other surfaces.
|
|
352
|
+
const arr: Material[] = [edge ?? fb(0)!, wall ?? fb(1)!, wall ?? fb(2)!, top ?? fb(3)!]
|
|
353
|
+
if (arr.some((m) => !m)) return null
|
|
354
|
+
return previewMeshMaterial(mesh, arr)
|
|
290
355
|
}
|
|
291
356
|
|
|
292
357
|
function applyStairPaintPreview(
|
|
@@ -301,8 +366,9 @@ function applyStairPaintPreview(
|
|
|
301
366
|
...node,
|
|
302
367
|
...buildStairSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
|
303
368
|
}
|
|
304
|
-
const
|
|
305
|
-
const
|
|
369
|
+
const shading = useViewer.getState().shading
|
|
370
|
+
const bodyMaterials = getStairBodyMaterials(previewNode, shading)
|
|
371
|
+
const railingMaterial = getStairRailingMaterial(previewNode, shading)
|
|
306
372
|
const restores: PaintPreviewCleanup[] = []
|
|
307
373
|
|
|
308
374
|
root.traverse((object) => {
|
|
@@ -335,13 +401,13 @@ function applyStairPaintPreview(
|
|
|
335
401
|
}
|
|
336
402
|
|
|
337
403
|
function applySingleSurfacePaintPreview(
|
|
338
|
-
node: FenceNode | ColumnNode | SlabNode | CeilingNode,
|
|
404
|
+
node: FenceNode | ColumnNode | SlabNode | CeilingNode | ShelfNode,
|
|
339
405
|
material: ActivePaintMaterial,
|
|
340
406
|
): PaintPreviewCleanup | null {
|
|
341
407
|
if (node.type === 'ceiling') {
|
|
342
408
|
const root = getRegisteredMesh(node.id)
|
|
343
409
|
const overlay = root?.getObjectByName('ceiling-grid') as Mesh | undefined
|
|
344
|
-
if (!root
|
|
410
|
+
if (!(root && overlay)) return null
|
|
345
411
|
|
|
346
412
|
const previewColor =
|
|
347
413
|
getMaterialPresetByRef(material.materialPreset)?.mapProperties.color ??
|
|
@@ -403,6 +469,25 @@ function applySingleSurfacePaintPreview(
|
|
|
403
469
|
}
|
|
404
470
|
}
|
|
405
471
|
|
|
472
|
+
if (node.type === 'shelf') {
|
|
473
|
+
// Shelf registers a `<group>` (not a Mesh) with `useRegistry`, so we walk
|
|
474
|
+
// the subtree and preview-swap every child mesh — same approach `column`
|
|
475
|
+
// uses. (The roof vents previously shared this arm; they now route through
|
|
476
|
+
// their `capabilities.paint` dispatcher.)
|
|
477
|
+
if (!registeredObject) return null
|
|
478
|
+
const restores: PaintPreviewCleanup[] = []
|
|
479
|
+
registeredObject.traverse((object) => {
|
|
480
|
+
if (!(object as Mesh).isMesh) return
|
|
481
|
+
restores.push(previewMeshMaterial(object as Mesh, previewMaterial))
|
|
482
|
+
})
|
|
483
|
+
if (restores.length === 0) return null
|
|
484
|
+
return () => {
|
|
485
|
+
for (let index = restores.length - 1; index >= 0; index -= 1) {
|
|
486
|
+
restores[index]?.()
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
406
491
|
if (!mesh) return null
|
|
407
492
|
|
|
408
493
|
if (node.type === 'slab') {
|
|
@@ -427,6 +512,11 @@ function applySingleSurfacePaintPreview(
|
|
|
427
512
|
return previewMeshMaterial(mesh, previewMaterial)
|
|
428
513
|
}
|
|
429
514
|
|
|
515
|
+
// Chimney + dormer paint dispatch lives on their NodeDefinition's
|
|
516
|
+
// `capabilities.paint` (see packages/nodes/src/{chimney,dormer}/
|
|
517
|
+
// paint.ts). The generic registry-driven arm in this file consults
|
|
518
|
+
// those entries — no per-kind helpers needed here.
|
|
519
|
+
|
|
430
520
|
function setSelectedMaterialTargetForNode(node: AnyNode, role: MaterialTargetRole | null) {
|
|
431
521
|
if (!role) {
|
|
432
522
|
const currentTarget = useEditor.getState().selectedMaterialTarget
|
|
@@ -565,6 +655,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
565
655
|
'fence',
|
|
566
656
|
'item',
|
|
567
657
|
'column',
|
|
658
|
+
'elevator',
|
|
568
659
|
'zone',
|
|
569
660
|
'slab',
|
|
570
661
|
'ceiling',
|
|
@@ -579,11 +670,18 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
579
670
|
handleSelect: (node, nativeEvent, modifierKeys) => {
|
|
580
671
|
const { selection, setSelection } = useViewer.getState()
|
|
581
672
|
const nodes = useScene.getState().nodes
|
|
582
|
-
const nodeLevelId = resolveLevelId(node, nodes)
|
|
583
|
-
const buildingId =
|
|
673
|
+
const nodeLevelId = node.type === 'elevator' ? null : resolveLevelId(node, nodes)
|
|
674
|
+
const buildingId =
|
|
675
|
+
node.type === 'elevator' &&
|
|
676
|
+
node.parentId &&
|
|
677
|
+
nodes[node.parentId as AnyNodeId]?.type === 'building'
|
|
678
|
+
? node.parentId
|
|
679
|
+
: nodeLevelId
|
|
680
|
+
? resolveBuildingId(nodeLevelId, nodes)
|
|
681
|
+
: null
|
|
584
682
|
|
|
585
683
|
const updates: any = {}
|
|
586
|
-
if (nodeLevelId !== 'default' && nodeLevelId !== selection.levelId) {
|
|
684
|
+
if (nodeLevelId && nodeLevelId !== 'default' && nodeLevelId !== selection.levelId) {
|
|
587
685
|
updates.levelId = nodeLevelId
|
|
588
686
|
}
|
|
589
687
|
if (buildingId && buildingId !== selection.buildingId) {
|
|
@@ -619,6 +717,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
619
717
|
node.type === 'wall' ||
|
|
620
718
|
node.type === 'fence' ||
|
|
621
719
|
node.type === 'column' ||
|
|
720
|
+
node.type === 'elevator' ||
|
|
622
721
|
node.type === 'slab' ||
|
|
623
722
|
node.type === 'ceiling' ||
|
|
624
723
|
node.type === 'roof' ||
|
|
@@ -636,6 +735,11 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
636
735
|
}
|
|
637
736
|
if (node.type === 'window' || node.type === 'door') return true
|
|
638
737
|
|
|
738
|
+
// Registry-driven: any kind whose NodeDefinition declares the
|
|
739
|
+
// `selectable` capability is also selectable in structure phase. Phase 4
|
|
740
|
+
// makes this the only path and deletes the hardcoded chain above.
|
|
741
|
+
if (isRegistrySelectable(node.type)) return true
|
|
742
|
+
|
|
639
743
|
return false
|
|
640
744
|
},
|
|
641
745
|
},
|
|
@@ -664,14 +768,43 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
664
768
|
},
|
|
665
769
|
isValid: (node) => {
|
|
666
770
|
if (!isNodeInCurrentLevel(node)) return false
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
771
|
+
// Item: door/window-category items belong to structure phase, not furnish.
|
|
772
|
+
if (node.type === 'item') {
|
|
773
|
+
const item = node as ItemNode
|
|
774
|
+
return item.asset.category !== 'door' && item.asset.category !== 'window'
|
|
775
|
+
}
|
|
776
|
+
// Registry-driven kinds with `category: 'furnish'` (shelf today,
|
|
777
|
+
// future furniture kinds): selectable in furnish phase if their
|
|
778
|
+
// definition declares the `selectable` capability. Without this
|
|
779
|
+
// branch, shelf clicks routed to furnish phase via getSelectionTarget
|
|
780
|
+
// would be rejected here — single-click selection broken.
|
|
781
|
+
const def = nodeRegistry.get(node.type)
|
|
782
|
+
if (def && def.category === 'furnish' && def.capabilities.selectable) return true
|
|
783
|
+
return false
|
|
670
784
|
},
|
|
671
785
|
},
|
|
672
786
|
}
|
|
673
787
|
|
|
674
788
|
const getSelectionTarget = (node: AnyNode): SelectionTarget | null => {
|
|
789
|
+
// Item is checked FIRST so its asset.category-driven routing (door/
|
|
790
|
+
// window items land in structure phase, everything else in furnish)
|
|
791
|
+
// beats the generic registry fallback below. Without this, registering
|
|
792
|
+
// `item` (Phase 5) made isRegistrySelectable('item') match the
|
|
793
|
+
// structure branch first, breaking single-click selection: first click
|
|
794
|
+
// switched the editor to structure phase, second click selected.
|
|
795
|
+
if (node.type === 'item') {
|
|
796
|
+
const item = node as ItemNode
|
|
797
|
+
if (item.asset.category === 'door' || item.asset.category === 'window') {
|
|
798
|
+
return {
|
|
799
|
+
phase: 'structure',
|
|
800
|
+
structureLayer: 'elements',
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
phase: 'furnish',
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
675
808
|
if (node.type === 'zone') {
|
|
676
809
|
return {
|
|
677
810
|
phase: 'structure',
|
|
@@ -683,6 +816,7 @@ const getSelectionTarget = (node: AnyNode): SelectionTarget | null => {
|
|
|
683
816
|
node.type === 'wall' ||
|
|
684
817
|
node.type === 'fence' ||
|
|
685
818
|
node.type === 'column' ||
|
|
819
|
+
node.type === 'elevator' ||
|
|
686
820
|
node.type === 'slab' ||
|
|
687
821
|
node.type === 'ceiling' ||
|
|
688
822
|
node.type === 'roof' ||
|
|
@@ -699,18 +833,16 @@ const getSelectionTarget = (node: AnyNode): SelectionTarget | null => {
|
|
|
699
833
|
}
|
|
700
834
|
}
|
|
701
835
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return {
|
|
712
|
-
phase: 'furnish',
|
|
836
|
+
// Registry-driven kinds (Phase 5+): route by `def.category`. Built-ins
|
|
837
|
+
// above match before this fallback. Furnish-category kinds (shelf,
|
|
838
|
+
// item — already handled above) land on the furnish phase; structure-
|
|
839
|
+
// category kinds (everything else) on structure/elements.
|
|
840
|
+
const def = nodeRegistry.get(node.type)
|
|
841
|
+
if (def) {
|
|
842
|
+
if (def.category === 'furnish') {
|
|
843
|
+
return { phase: 'furnish' }
|
|
713
844
|
}
|
|
845
|
+
return { phase: 'structure', structureLayer: 'elements' }
|
|
714
846
|
}
|
|
715
847
|
|
|
716
848
|
return null
|
|
@@ -762,45 +894,84 @@ export const SelectionManager = () => {
|
|
|
762
894
|
})
|
|
763
895
|
|
|
764
896
|
const getPaintInteraction = (event: NodeEvent): PaintInteraction | null => {
|
|
897
|
+
const eraser = useEditor.getState().paintEraser
|
|
765
898
|
const activePaintMaterial = resolveActivePaintMaterial()
|
|
766
899
|
const node = event.node
|
|
767
900
|
|
|
768
901
|
if (!isNodeInCurrentLevel(node)) return null
|
|
769
902
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
903
|
+
// The eraser clears a surface back to its default by painting with an
|
|
904
|
+
// empty material — every `build*SurfaceMaterialPatch` interprets
|
|
905
|
+
// `undefined` material/preset as "reset this role". So a single spec
|
|
906
|
+
// with both fields undefined drives the same apply/preview paths as a
|
|
907
|
+
// real material; only the enabled-gate differs (no material required).
|
|
908
|
+
const paintEnabled = eraser || hasActivePaintMaterial(activePaintMaterial)
|
|
909
|
+
const paintSpec: ActivePaintMaterial = eraser
|
|
910
|
+
? {
|
|
911
|
+
material: undefined,
|
|
912
|
+
materialPreset: undefined,
|
|
913
|
+
sourceTarget:
|
|
914
|
+
activePaintMaterial?.sourceTarget ?? useEditor.getState().activePaintTarget,
|
|
915
|
+
}
|
|
916
|
+
: (activePaintMaterial ?? {
|
|
917
|
+
material: undefined,
|
|
918
|
+
materialPreset: undefined,
|
|
919
|
+
sourceTarget: useEditor.getState().activePaintTarget,
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
// Registry-driven paint dispatch — kinds that declare
|
|
923
|
+
// `capabilities.paint` route hover / click / preview through
|
|
924
|
+
// their definition. Wall, chimney, and dormer use this; legacy
|
|
925
|
+
// roof / stair / single-surface arms below stay until they
|
|
926
|
+
// migrate too.
|
|
927
|
+
const paintCap = nodeRegistry.get(node.type)?.capabilities?.paint
|
|
928
|
+
if (paintCap) {
|
|
929
|
+
const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
|
|
930
|
+
const role = paintCap.resolveRole({
|
|
931
|
+
node,
|
|
932
|
+
materialIndex: materialIndex ?? null,
|
|
933
|
+
normal: event.normal,
|
|
934
|
+
localPosition: event.localPosition as readonly [number, number, number] | undefined,
|
|
935
|
+
hitObjectName: event.nativeEvent.object?.name,
|
|
936
|
+
})
|
|
937
|
+
const compatible = role !== null && paintEnabled
|
|
773
938
|
return {
|
|
774
|
-
key:
|
|
939
|
+
key: `${node.type}:${node.id}:${role ?? 'unsupported'}:${eraser ? 'erase' : 'paint'}`,
|
|
775
940
|
hoveredId: node.id as AnyNodeId,
|
|
776
|
-
hoverMode:
|
|
777
|
-
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
|
778
|
-
? 'paint-ready'
|
|
779
|
-
: 'paint-disabled',
|
|
941
|
+
hoverMode: compatible ? 'paint-ready' : 'paint-disabled',
|
|
780
942
|
apply:
|
|
781
|
-
compatible &&
|
|
943
|
+
compatible && role
|
|
782
944
|
? () => {
|
|
783
|
-
useScene
|
|
784
|
-
.
|
|
785
|
-
.
|
|
786
|
-
node
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
),
|
|
793
|
-
)
|
|
945
|
+
useScene.getState().updateNode(
|
|
946
|
+
node.id as AnyNodeId,
|
|
947
|
+
paintCap.buildPatch({
|
|
948
|
+
node,
|
|
949
|
+
role,
|
|
950
|
+
material: paintSpec.material,
|
|
951
|
+
materialPreset: paintSpec.materialPreset,
|
|
952
|
+
}) as Partial<AnyNode>,
|
|
953
|
+
)
|
|
794
954
|
}
|
|
795
955
|
: null,
|
|
796
956
|
preview:
|
|
797
|
-
compatible &&
|
|
798
|
-
? () =>
|
|
957
|
+
compatible && role
|
|
958
|
+
? () => {
|
|
959
|
+
const root = getRegisteredNodeObject(node.id)
|
|
960
|
+
if (!root) return null
|
|
961
|
+
return paintCap.applyPreview({
|
|
962
|
+
node,
|
|
963
|
+
role,
|
|
964
|
+
material: paintSpec.material,
|
|
965
|
+
materialPreset: paintSpec.materialPreset,
|
|
966
|
+
root,
|
|
967
|
+
})
|
|
968
|
+
}
|
|
799
969
|
: () => previewCursor('not-allowed'),
|
|
800
970
|
}
|
|
801
971
|
}
|
|
802
972
|
|
|
803
973
|
if (node.type === 'roof' || node.type === 'roof-segment') {
|
|
974
|
+
const isSegmentHit = node.type === 'roof-segment'
|
|
804
975
|
const roofNode =
|
|
805
976
|
node.type === 'roof'
|
|
806
977
|
? node
|
|
@@ -810,33 +981,56 @@ export const SelectionManager = () => {
|
|
|
810
981
|
if (!roofNode || roofNode.type !== 'roof') return null
|
|
811
982
|
|
|
812
983
|
const role = resolveRoofMaterialTarget(event as RoofEvent | RoofSegmentEvent)
|
|
813
|
-
const compatible = role !== null &&
|
|
984
|
+
const compatible = role !== null && paintEnabled
|
|
985
|
+
// Painting directly on a segment (only possible in segment edit
|
|
986
|
+
// mode, where the per-segment mesh is visible) writes to the
|
|
987
|
+
// segment's own role-specific fields. Painting the merged shell
|
|
988
|
+
// — or a roof node directly — keeps fanning to the parent roof.
|
|
989
|
+
const segmentTarget = isSegmentHit ? (node as RoofSegmentNode) : null
|
|
814
990
|
return {
|
|
815
|
-
key:
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
: 'paint-disabled',
|
|
991
|
+
key: `${segmentTarget ? 'roof-segment' : 'roof'}:${
|
|
992
|
+
segmentTarget ? segmentTarget.id : roofNode.id
|
|
993
|
+
}:${role ?? 'unsupported'}:${eraser ? 'erase' : 'paint'}`,
|
|
994
|
+
hoveredId: (segmentTarget ? segmentTarget.id : roofNode.id) as AnyNodeId,
|
|
995
|
+
hoverMode: compatible ? 'paint-ready' : 'paint-disabled',
|
|
821
996
|
apply:
|
|
822
|
-
compatible &&
|
|
997
|
+
compatible && role
|
|
823
998
|
? () => {
|
|
824
|
-
useScene
|
|
825
|
-
|
|
826
|
-
.updateNode(
|
|
999
|
+
const sceneState = useScene.getState()
|
|
1000
|
+
if (segmentTarget) {
|
|
1001
|
+
sceneState.updateNode(
|
|
1002
|
+
segmentTarget.id as AnyNodeId,
|
|
1003
|
+
buildRoofSegmentSurfaceMaterialPatch(
|
|
1004
|
+
segmentTarget,
|
|
1005
|
+
role,
|
|
1006
|
+
paintSpec.material,
|
|
1007
|
+
paintSpec.materialPreset,
|
|
1008
|
+
),
|
|
1009
|
+
)
|
|
1010
|
+
} else {
|
|
1011
|
+
sceneState.updateNode(
|
|
827
1012
|
roofNode.id as AnyNodeId,
|
|
828
1013
|
buildRoofSurfaceMaterialPatch(
|
|
829
1014
|
roofNode as RoofNode,
|
|
830
|
-
role
|
|
831
|
-
|
|
832
|
-
|
|
1015
|
+
role,
|
|
1016
|
+
paintSpec.material,
|
|
1017
|
+
paintSpec.materialPreset,
|
|
833
1018
|
),
|
|
834
1019
|
)
|
|
1020
|
+
}
|
|
835
1021
|
}
|
|
836
1022
|
: null,
|
|
837
1023
|
preview:
|
|
838
|
-
compatible &&
|
|
839
|
-
? () =>
|
|
1024
|
+
compatible && role
|
|
1025
|
+
? () =>
|
|
1026
|
+
segmentTarget
|
|
1027
|
+
? applyRoofSegmentPaintPreview(
|
|
1028
|
+
segmentTarget,
|
|
1029
|
+
roofNode as RoofNode,
|
|
1030
|
+
role,
|
|
1031
|
+
paintSpec,
|
|
1032
|
+
)
|
|
1033
|
+
: applyRoofPaintPreview(roofNode as RoofNode, role, paintSpec)
|
|
840
1034
|
: () => previewCursor('not-allowed'),
|
|
841
1035
|
}
|
|
842
1036
|
}
|
|
@@ -851,16 +1045,13 @@ export const SelectionManager = () => {
|
|
|
851
1045
|
if (!stairNode || stairNode.type !== 'stair') return null
|
|
852
1046
|
|
|
853
1047
|
const role = resolveStairMaterialTarget(event as StairEvent | StairSegmentEvent)
|
|
854
|
-
const compatible = role !== null &&
|
|
1048
|
+
const compatible = role !== null && paintEnabled
|
|
855
1049
|
return {
|
|
856
|
-
key: `stair:${stairNode.id}:${role ?? 'unsupported'}`,
|
|
1050
|
+
key: `stair:${stairNode.id}:${role ?? 'unsupported'}:${eraser ? 'erase' : 'paint'}`,
|
|
857
1051
|
hoveredId: stairNode.id as AnyNodeId,
|
|
858
|
-
hoverMode:
|
|
859
|
-
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
|
860
|
-
? 'paint-ready'
|
|
861
|
-
: 'paint-disabled',
|
|
1052
|
+
hoverMode: compatible ? 'paint-ready' : 'paint-disabled',
|
|
862
1053
|
apply:
|
|
863
|
-
compatible &&
|
|
1054
|
+
compatible && role
|
|
864
1055
|
? () => {
|
|
865
1056
|
useScene
|
|
866
1057
|
.getState()
|
|
@@ -868,30 +1059,36 @@ export const SelectionManager = () => {
|
|
|
868
1059
|
stairNode.id as AnyNodeId,
|
|
869
1060
|
buildStairSurfaceMaterialPatch(
|
|
870
1061
|
stairNode as StairNode,
|
|
871
|
-
role
|
|
872
|
-
|
|
873
|
-
|
|
1062
|
+
role,
|
|
1063
|
+
paintSpec.material,
|
|
1064
|
+
paintSpec.materialPreset,
|
|
874
1065
|
),
|
|
875
1066
|
)
|
|
876
1067
|
}
|
|
877
1068
|
: null,
|
|
878
1069
|
preview:
|
|
879
|
-
compatible &&
|
|
880
|
-
? () => applyStairPaintPreview(stairNode as StairNode, role,
|
|
1070
|
+
compatible && role
|
|
1071
|
+
? () => applyStairPaintPreview(stairNode as StairNode, role, paintSpec)
|
|
881
1072
|
: () => previewCursor('not-allowed'),
|
|
882
1073
|
}
|
|
883
1074
|
}
|
|
884
1075
|
|
|
1076
|
+
// Registry-driven paint dispatch handled at the top of this
|
|
1077
|
+
// function — kinds declaring `capabilities.paint` return there
|
|
1078
|
+
// before any of the legacy roof / stair / single-surface arms
|
|
1079
|
+
// below run.
|
|
1080
|
+
|
|
885
1081
|
if (
|
|
886
1082
|
node.type === 'fence' ||
|
|
887
1083
|
node.type === 'column' ||
|
|
888
1084
|
node.type === 'slab' ||
|
|
889
|
-
node.type === 'ceiling'
|
|
1085
|
+
node.type === 'ceiling' ||
|
|
1086
|
+
node.type === 'shelf'
|
|
890
1087
|
) {
|
|
891
|
-
const compatible =
|
|
1088
|
+
const compatible = paintEnabled
|
|
892
1089
|
|
|
893
1090
|
return {
|
|
894
|
-
key: `${node.type}:${node.id}:surface`,
|
|
1091
|
+
key: `${node.type}:${node.id}:surface:${eraser ? 'erase' : 'paint'}`,
|
|
895
1092
|
hoveredId: node.id as AnyNodeId,
|
|
896
1093
|
hoverMode: compatible ? 'paint-ready' : 'paint-disabled',
|
|
897
1094
|
apply: compatible
|
|
@@ -901,16 +1098,16 @@ export const SelectionManager = () => {
|
|
|
901
1098
|
.updateNode(
|
|
902
1099
|
node.id as AnyNodeId,
|
|
903
1100
|
buildSingleSurfaceMaterialPatch<
|
|
904
|
-
FenceNode | ColumnNode | SlabNode | CeilingNode
|
|
905
|
-
>(
|
|
1101
|
+
FenceNode | ColumnNode | SlabNode | CeilingNode | ShelfNode
|
|
1102
|
+
>(paintSpec.material, paintSpec.materialPreset),
|
|
906
1103
|
)
|
|
907
1104
|
}
|
|
908
1105
|
: null,
|
|
909
1106
|
preview: compatible
|
|
910
1107
|
? () =>
|
|
911
1108
|
applySingleSurfacePaintPreview(
|
|
912
|
-
node as FenceNode | ColumnNode | SlabNode | CeilingNode,
|
|
913
|
-
|
|
1109
|
+
node as FenceNode | ColumnNode | SlabNode | CeilingNode | ShelfNode,
|
|
1110
|
+
paintSpec,
|
|
914
1111
|
)
|
|
915
1112
|
: () => previewCursor('not-allowed'),
|
|
916
1113
|
}
|
|
@@ -999,20 +1196,26 @@ export const SelectionManager = () => {
|
|
|
999
1196
|
'roof-segment',
|
|
1000
1197
|
'stair',
|
|
1001
1198
|
'stair-segment',
|
|
1002
|
-
'spawn',
|
|
1003
1199
|
'window',
|
|
1004
1200
|
'door',
|
|
1005
1201
|
'zone',
|
|
1006
1202
|
] as const
|
|
1007
1203
|
|
|
1008
|
-
|
|
1204
|
+
// Registry-driven kinds get the same subscriptions as the hardcoded list,
|
|
1205
|
+
// so future built-in nodes don't need to edit allTypes per migration.
|
|
1206
|
+
const registryKinds = getSelectableKinds().filter(
|
|
1207
|
+
(k) => !(allTypes as readonly string[]).includes(k),
|
|
1208
|
+
)
|
|
1209
|
+
const subscribedKinds = [...(allTypes as readonly string[]), ...registryKinds]
|
|
1210
|
+
|
|
1211
|
+
for (const type of subscribedKinds) {
|
|
1009
1212
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
|
1010
1213
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
|
1011
1214
|
emitter.on(`${type}:click` as any, onClick as any)
|
|
1012
1215
|
}
|
|
1013
1216
|
|
|
1014
1217
|
return () => {
|
|
1015
|
-
for (const type of
|
|
1218
|
+
for (const type of subscribedKinds) {
|
|
1016
1219
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
|
1017
1220
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
|
1018
1221
|
emitter.off(`${type}:click` as any, onClick as any)
|
|
@@ -1059,6 +1262,16 @@ export const SelectionManager = () => {
|
|
|
1059
1262
|
if (boxSelectHandled) return
|
|
1060
1263
|
|
|
1061
1264
|
const node = event.node
|
|
1265
|
+
|
|
1266
|
+
// A ceiling is selectable only through its corner handles, never via
|
|
1267
|
+
// the `ceiling-grid` body mesh. When the grid is revealed (ceiling
|
|
1268
|
+
// selected, or an item placed beneath it) a top-down click hits the
|
|
1269
|
+
// grid first; selecting the ceiling there both re-selects it as a
|
|
1270
|
+
// no-op and stops propagation, blocking the hosted item below. By
|
|
1271
|
+
// ignoring non-handle ceiling clicks (without stopping propagation)
|
|
1272
|
+
// the click falls through to the item underneath.
|
|
1273
|
+
if (node.type === 'ceiling' && !event.viaHandle) return
|
|
1274
|
+
|
|
1062
1275
|
let currentPhase = useEditor.getState().phase
|
|
1063
1276
|
let currentStructureLayer = useEditor.getState().structureLayer
|
|
1064
1277
|
|
|
@@ -1092,8 +1305,14 @@ export const SelectionManager = () => {
|
|
|
1092
1305
|
|
|
1093
1306
|
let nodeToSelect = node
|
|
1094
1307
|
if (node.type === 'roof-segment' && node.parentId) {
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1308
|
+
const nodes = useScene.getState().nodes
|
|
1309
|
+
const parentNode = nodes[node.parentId as AnyNodeId]
|
|
1310
|
+
const selectedIds = useViewer.getState().selection.selectedIds
|
|
1311
|
+
if (
|
|
1312
|
+
parentNode &&
|
|
1313
|
+
parentNode.type === 'roof' &&
|
|
1314
|
+
!isInActiveRoofContext(node, selectedIds, nodes)
|
|
1315
|
+
) {
|
|
1097
1316
|
nodeToSelect = parentNode
|
|
1098
1317
|
}
|
|
1099
1318
|
}
|
|
@@ -1104,19 +1323,44 @@ export const SelectionManager = () => {
|
|
|
1104
1323
|
}
|
|
1105
1324
|
}
|
|
1106
1325
|
|
|
1326
|
+
// Clicking any node (e.g. the slab surface outside a hole) exits slab
|
|
1327
|
+
// hole-edit mode. The hole handles + hit mesh stopPropagation, so a
|
|
1328
|
+
// click reaching here means the user clicked outside the hole.
|
|
1329
|
+
if (useEditor.getState().editingHole) {
|
|
1330
|
+
useEditor.getState().setEditingHole(null)
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1107
1333
|
activeStrategy.handleSelect(nodeToSelect, event.nativeEvent, modifierKeysRef.current)
|
|
1108
1334
|
|
|
1109
1335
|
let nextMaterialTargetHandled = false
|
|
1110
1336
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1337
|
+
// Registry-driven paint-target resolve on click. Kinds with
|
|
1338
|
+
// `capabilities.paint` route through this entry — wall,
|
|
1339
|
+
// chimney, dormer use it today. The legacy stair / roof /
|
|
1340
|
+
// single-surface arms below stay until they migrate too.
|
|
1341
|
+
if (nodeToSelect.type === node.type) {
|
|
1342
|
+
const paintCap = nodeRegistry.get(node.type)?.capabilities?.paint
|
|
1343
|
+
if (paintCap) {
|
|
1344
|
+
const materialIndex = getIntersectionMaterialIndex(
|
|
1345
|
+
getEventObject(event),
|
|
1346
|
+
event.faceIndex,
|
|
1347
|
+
)
|
|
1348
|
+
const role = paintCap.resolveRole({
|
|
1349
|
+
node,
|
|
1350
|
+
materialIndex: materialIndex ?? null,
|
|
1351
|
+
normal: event.normal,
|
|
1352
|
+
localPosition: event.localPosition as readonly [number, number, number] | undefined,
|
|
1353
|
+
hitObjectName: event.nativeEvent.object?.name,
|
|
1354
|
+
})
|
|
1355
|
+
if (role) {
|
|
1356
|
+
setSelectedMaterialTargetForNode(nodeToSelect, role as MaterialTargetRole)
|
|
1357
|
+
nextMaterialTargetHandled = true
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1117
1360
|
}
|
|
1118
1361
|
|
|
1119
1362
|
if (
|
|
1363
|
+
!nextMaterialTargetHandled &&
|
|
1120
1364
|
(node.type === 'stair' || node.type === 'stair-segment') &&
|
|
1121
1365
|
nodeToSelect.type === 'stair'
|
|
1122
1366
|
) {
|
|
@@ -1128,6 +1372,7 @@ export const SelectionManager = () => {
|
|
|
1128
1372
|
}
|
|
1129
1373
|
|
|
1130
1374
|
if (
|
|
1375
|
+
!nextMaterialTargetHandled &&
|
|
1131
1376
|
(node.type === 'roof' || node.type === 'roof-segment') &&
|
|
1132
1377
|
nodeToSelect.type === 'roof'
|
|
1133
1378
|
) {
|
|
@@ -1139,7 +1384,11 @@ export const SelectionManager = () => {
|
|
|
1139
1384
|
}
|
|
1140
1385
|
|
|
1141
1386
|
if (
|
|
1142
|
-
|
|
1387
|
+
!nextMaterialTargetHandled &&
|
|
1388
|
+
(node.type === 'fence' ||
|
|
1389
|
+
node.type === 'slab' ||
|
|
1390
|
+
node.type === 'ceiling' ||
|
|
1391
|
+
node.type === 'shelf') &&
|
|
1143
1392
|
nodeToSelect.type === node.type
|
|
1144
1393
|
) {
|
|
1145
1394
|
setSelectedMaterialTargetForNode(nodeToSelect, 'surface')
|
|
@@ -1163,6 +1412,7 @@ export const SelectionManager = () => {
|
|
|
1163
1412
|
'item',
|
|
1164
1413
|
'column',
|
|
1165
1414
|
'building',
|
|
1415
|
+
'elevator',
|
|
1166
1416
|
'zone',
|
|
1167
1417
|
'slab',
|
|
1168
1418
|
'ceiling',
|
|
@@ -1174,7 +1424,14 @@ export const SelectionManager = () => {
|
|
|
1174
1424
|
'window',
|
|
1175
1425
|
'door',
|
|
1176
1426
|
]
|
|
1177
|
-
|
|
1427
|
+
// Registry-driven kinds get the same subscriptions as the hardcoded list,
|
|
1428
|
+
// so future built-in nodes don't need to edit allTypes per migration.
|
|
1429
|
+
const registryKinds = getSelectableKinds().filter(
|
|
1430
|
+
(k) => !(allTypes as readonly string[]).includes(k),
|
|
1431
|
+
)
|
|
1432
|
+
const subscribedKinds = [...(allTypes as readonly string[]), ...registryKinds]
|
|
1433
|
+
|
|
1434
|
+
subscribedKinds.forEach((type) => {
|
|
1178
1435
|
emitter.on(`${type}:click` as any, onClick as any)
|
|
1179
1436
|
})
|
|
1180
1437
|
|
|
@@ -1195,7 +1452,7 @@ export const SelectionManager = () => {
|
|
|
1195
1452
|
emitter.on('grid:click', onGridClick)
|
|
1196
1453
|
|
|
1197
1454
|
return () => {
|
|
1198
|
-
|
|
1455
|
+
subscribedKinds.forEach((type) => {
|
|
1199
1456
|
emitter.off(`${type}:click` as any, onClick as any)
|
|
1200
1457
|
})
|
|
1201
1458
|
emitter.off('grid:click', onGridClick)
|
|
@@ -1242,7 +1499,11 @@ export const SelectionManager = () => {
|
|
|
1242
1499
|
}
|
|
1243
1500
|
|
|
1244
1501
|
const onDoubleClick = (event: NodeEvent) => {
|
|
1245
|
-
|
|
1502
|
+
let node = event.node
|
|
1503
|
+
if (node.type === 'roof') {
|
|
1504
|
+
node = resolveRoofSegmentSelectionTarget(event) ?? node
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1246
1507
|
const currentPhase = useEditor.getState().phase
|
|
1247
1508
|
|
|
1248
1509
|
let targetPhase: 'site' | 'structure' | 'furnish' | null = null
|
|
@@ -1259,6 +1520,7 @@ export const SelectionManager = () => {
|
|
|
1259
1520
|
node.type === 'wall' ||
|
|
1260
1521
|
node.type === 'fence' ||
|
|
1261
1522
|
node.type === 'column' ||
|
|
1523
|
+
node.type === 'elevator' ||
|
|
1262
1524
|
node.type === 'slab' ||
|
|
1263
1525
|
node.type === 'ceiling' ||
|
|
1264
1526
|
node.type === 'roof' ||
|
|
@@ -1313,6 +1575,7 @@ export const SelectionManager = () => {
|
|
|
1313
1575
|
'item',
|
|
1314
1576
|
'column',
|
|
1315
1577
|
'building',
|
|
1578
|
+
'elevator',
|
|
1316
1579
|
'slab',
|
|
1317
1580
|
'ceiling',
|
|
1318
1581
|
'roof',
|
|
@@ -1325,14 +1588,19 @@ export const SelectionManager = () => {
|
|
|
1325
1588
|
'zone',
|
|
1326
1589
|
'site',
|
|
1327
1590
|
]
|
|
1328
|
-
|
|
1591
|
+
const registryKinds = getSelectableKinds().filter(
|
|
1592
|
+
(k) => !(allTypes as readonly string[]).includes(k),
|
|
1593
|
+
)
|
|
1594
|
+
const subscribedKinds = [...(allTypes as readonly string[]), ...registryKinds]
|
|
1595
|
+
|
|
1596
|
+
subscribedKinds.forEach((type) => {
|
|
1329
1597
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
|
1330
1598
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
|
1331
1599
|
emitter.on(`${type}:double-click` as any, onDoubleClick as any)
|
|
1332
1600
|
})
|
|
1333
1601
|
|
|
1334
1602
|
return () => {
|
|
1335
|
-
|
|
1603
|
+
subscribedKinds.forEach((type) => {
|
|
1336
1604
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
|
1337
1605
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
|
1338
1606
|
emitter.off(`${type}:double-click` as any, onDoubleClick as any)
|
|
@@ -1351,11 +1619,7 @@ export const SelectionManager = () => {
|
|
|
1351
1619
|
event.stopPropagation()
|
|
1352
1620
|
|
|
1353
1621
|
// Play appropriate SFX
|
|
1354
|
-
|
|
1355
|
-
sfxEmitter.emit('sfx:item-delete')
|
|
1356
|
-
} else {
|
|
1357
|
-
sfxEmitter.emit('sfx:structure-delete')
|
|
1358
|
-
}
|
|
1622
|
+
emitDeleteSFX(node.type)
|
|
1359
1623
|
|
|
1360
1624
|
useScene.getState().deleteNode(node.id as AnyNodeId)
|
|
1361
1625
|
if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
|
|
@@ -1386,25 +1650,32 @@ export const SelectionManager = () => {
|
|
|
1386
1650
|
'fence',
|
|
1387
1651
|
'item',
|
|
1388
1652
|
'column',
|
|
1653
|
+
'elevator',
|
|
1389
1654
|
'slab',
|
|
1390
1655
|
'ceiling',
|
|
1391
1656
|
'roof',
|
|
1392
1657
|
'roof-segment',
|
|
1393
1658
|
'stair',
|
|
1394
1659
|
'stair-segment',
|
|
1660
|
+
'spawn',
|
|
1395
1661
|
'window',
|
|
1396
1662
|
'door',
|
|
1397
1663
|
'zone',
|
|
1398
1664
|
] as const
|
|
1399
1665
|
|
|
1400
|
-
|
|
1666
|
+
const registryKinds = getSelectableKinds().filter(
|
|
1667
|
+
(k) => !(allTypes as readonly string[]).includes(k),
|
|
1668
|
+
)
|
|
1669
|
+
const subscribedKinds = [...(allTypes as readonly string[]), ...registryKinds]
|
|
1670
|
+
|
|
1671
|
+
for (const type of subscribedKinds) {
|
|
1401
1672
|
emitter.on(`${type}:click` as any, onClick as any)
|
|
1402
1673
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
|
1403
1674
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
|
1404
1675
|
}
|
|
1405
1676
|
|
|
1406
1677
|
return () => {
|
|
1407
|
-
for (const type of
|
|
1678
|
+
for (const type of subscribedKinds) {
|
|
1408
1679
|
emitter.off(`${type}:click` as any, onClick as any)
|
|
1409
1680
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
|
1410
1681
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
|
@@ -1636,6 +1907,7 @@ const EditorOutlinerSync = () => {
|
|
|
1636
1907
|
const previewSelectedIds = useViewer((s) => s.previewSelectedIds)
|
|
1637
1908
|
const hoveredId = useViewer((s) => s.hoveredId)
|
|
1638
1909
|
const outliner = useViewer((s) => s.outliner)
|
|
1910
|
+
const nodes = useScene((s) => s.nodes)
|
|
1639
1911
|
|
|
1640
1912
|
useEffect(() => {
|
|
1641
1913
|
let idsToHighlight: string[] = []
|
|
@@ -1672,16 +1944,21 @@ const EditorOutlinerSync = () => {
|
|
|
1672
1944
|
// 2. Sync with the imperative outliner arrays (mutate in place to keep references)
|
|
1673
1945
|
outliner.selectedObjects.length = 0
|
|
1674
1946
|
for (const id of idsToHighlight) {
|
|
1947
|
+
if (!nodes[id as AnyNodeId]) continue
|
|
1675
1948
|
const obj = sceneRegistry.nodes.get(id)
|
|
1676
1949
|
if (obj?.parent) outliner.selectedObjects.push(obj)
|
|
1677
1950
|
}
|
|
1678
1951
|
|
|
1679
1952
|
outliner.hoveredObjects.length = 0
|
|
1680
1953
|
if (hoveredId) {
|
|
1681
|
-
|
|
1682
|
-
|
|
1954
|
+
if (!nodes[hoveredId as AnyNodeId]) {
|
|
1955
|
+
useViewer.setState({ hoveredId: null })
|
|
1956
|
+
} else {
|
|
1957
|
+
const obj = sceneRegistry.nodes.get(hoveredId)
|
|
1958
|
+
if (obj?.parent) outliner.hoveredObjects.push(obj)
|
|
1959
|
+
}
|
|
1683
1960
|
}
|
|
1684
|
-
}, [phase, previewSelectedIds, selection, hoveredId, outliner])
|
|
1961
|
+
}, [phase, previewSelectedIds, selection, hoveredId, outliner, nodes])
|
|
1685
1962
|
|
|
1686
1963
|
return null
|
|
1687
1964
|
}
|