@pascal-app/editor 0.5.1 → 0.7.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 +12 -7
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +29 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +281 -83
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +10442 -3275
- package/src/components/editor/index.tsx +270 -20
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +766 -12
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +350 -157
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +377 -58
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- 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-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +19 -0
- package/src/components/tools/door/move-door-tool.tsx +38 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +27 -8
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
- package/src/components/tools/fence/move-fence-tool.tsx +102 -27
- package/src/components/tools/item/move-tool.tsx +19 -1
- package/src/components/tools/item/placement-math.ts +44 -7
- package/src/components/tools/item/placement-strategies.ts +111 -33
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
- package/src/components/tools/roof/move-roof-tool.tsx +111 -43
- package/src/components/tools/shared/polygon-editor.tsx +244 -29
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +30 -3
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +348 -17
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +28 -0
- package/src/components/tools/window/window-tool.tsx +17 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +37 -5
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +27 -5
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +189 -169
- package/src/components/ui/controls/slider-control.tsx +88 -26
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +47 -27
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +986 -294
- package/src/components/ui/panels/fence-panel.tsx +55 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +241 -30
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +243 -9
- package/src/components/ui/panels/roof-panel.tsx +30 -62
- package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
- package/src/components/ui/panels/slab-panel.tsx +46 -24
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +117 -69
- package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
- package/src/components/ui/panels/wall-panel.tsx +71 -17
- package/src/components/ui/panels/window-panel.tsx +665 -146
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- 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 +138 -56
- 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 +9 -5
- 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/spawn-tree-node.tsx +82 -0
- 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 +12 -6
- 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 +15 -8
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +96 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +67 -9
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +2 -1
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/history.ts +20 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +279 -15
|
@@ -2,19 +2,57 @@ import {
|
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
4
|
type BuildingNode,
|
|
5
|
+
type CeilingNode,
|
|
6
|
+
type ColumnNode,
|
|
5
7
|
emitter,
|
|
8
|
+
type FenceNode,
|
|
9
|
+
getMaterialPresetByRef,
|
|
6
10
|
type ItemNode,
|
|
7
11
|
type NodeEvent,
|
|
12
|
+
type RoofEvent,
|
|
13
|
+
type RoofNode,
|
|
14
|
+
type RoofSegmentEvent,
|
|
8
15
|
resolveLevelId,
|
|
16
|
+
resolveMaterial,
|
|
17
|
+
type SlabNode,
|
|
18
|
+
type StairEvent,
|
|
19
|
+
type StairNode,
|
|
20
|
+
type StairSegmentEvent,
|
|
21
|
+
type StairSurfaceMaterialRole,
|
|
9
22
|
sceneRegistry,
|
|
10
23
|
useScene,
|
|
24
|
+
type WallEvent,
|
|
25
|
+
type WallNode,
|
|
26
|
+
type WallSurfaceSide,
|
|
11
27
|
} from '@pascal-app/core'
|
|
12
28
|
|
|
13
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
applyMaterialPresetToMaterials,
|
|
31
|
+
createMaterial,
|
|
32
|
+
createMaterialFromPresetRef,
|
|
33
|
+
getRoofMaterialArray,
|
|
34
|
+
getStairBodyMaterials,
|
|
35
|
+
getStairRailingMaterial,
|
|
36
|
+
getVisibleWallMaterials,
|
|
37
|
+
useViewer,
|
|
38
|
+
} from '@pascal-app/viewer'
|
|
14
39
|
import { useCallback, useEffect, useRef } from 'react'
|
|
15
|
-
import { Color, type Material, type Mesh, type Object3D } from 'three'
|
|
40
|
+
import { type BufferGeometry, Color, type Material, type Mesh, type Object3D } from 'three'
|
|
41
|
+
import {
|
|
42
|
+
type ActivePaintMaterial,
|
|
43
|
+
buildRoofSurfaceMaterialPatch,
|
|
44
|
+
buildSingleSurfaceMaterialPatch,
|
|
45
|
+
buildStairSurfaceMaterialPatch,
|
|
46
|
+
buildWallSurfaceMaterialPatch,
|
|
47
|
+
hasActivePaintMaterial,
|
|
48
|
+
resolveActivePaintMaterialFromSelection,
|
|
49
|
+
} from '../../lib/material-paint'
|
|
16
50
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
17
|
-
import useEditor, {
|
|
51
|
+
import useEditor, {
|
|
52
|
+
type MaterialTargetRole,
|
|
53
|
+
type Phase,
|
|
54
|
+
type StructureLayer,
|
|
55
|
+
} from './../../store/use-editor'
|
|
18
56
|
import { boxSelectHandled } from '../tools/select/box-select-tool'
|
|
19
57
|
|
|
20
58
|
const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
|
@@ -28,6 +66,7 @@ type SelectableNodeType =
|
|
|
28
66
|
| 'wall'
|
|
29
67
|
| 'fence'
|
|
30
68
|
| 'item'
|
|
69
|
+
| 'column'
|
|
31
70
|
| 'building'
|
|
32
71
|
| 'zone'
|
|
33
72
|
| 'slab'
|
|
@@ -36,6 +75,7 @@ type SelectableNodeType =
|
|
|
36
75
|
| 'roof-segment'
|
|
37
76
|
| 'stair'
|
|
38
77
|
| 'stair-segment'
|
|
78
|
+
| 'spawn'
|
|
39
79
|
| 'window'
|
|
40
80
|
| 'door'
|
|
41
81
|
|
|
@@ -44,6 +84,16 @@ type ModifierKeys = {
|
|
|
44
84
|
ctrl: boolean
|
|
45
85
|
}
|
|
46
86
|
|
|
87
|
+
type PaintPreviewCleanup = () => void
|
|
88
|
+
|
|
89
|
+
type PaintInteraction = {
|
|
90
|
+
key: string
|
|
91
|
+
apply: (() => void) | null
|
|
92
|
+
hoverMode: HoverHighlightMode
|
|
93
|
+
hoveredId: AnyNodeId
|
|
94
|
+
preview: (() => PaintPreviewCleanup | null) | null
|
|
95
|
+
}
|
|
96
|
+
|
|
47
97
|
interface SelectionStrategy {
|
|
48
98
|
types: SelectableNodeType[]
|
|
49
99
|
handleSelect: (node: AnyNode, nativeEvent?: any, modifierKeys?: ModifierKeys) => void
|
|
@@ -68,6 +118,330 @@ export const resolveBuildingId = (
|
|
|
68
118
|
return null
|
|
69
119
|
}
|
|
70
120
|
|
|
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
|
+
function resolveStairMaterialTarget(
|
|
149
|
+
event: StairEvent | StairSegmentEvent,
|
|
150
|
+
): StairSurfaceMaterialRole | null {
|
|
151
|
+
const hitObjectName = event.nativeEvent.object?.name ?? ''
|
|
152
|
+
const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
|
|
153
|
+
|
|
154
|
+
if (hitObjectName.startsWith('stair-railing')) {
|
|
155
|
+
return 'railing'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (hitObjectName.startsWith('stair-side')) {
|
|
159
|
+
return 'side'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (materialIndex === 0) {
|
|
163
|
+
return 'tread'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (materialIndex === 1) {
|
|
167
|
+
return 'side'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalY = event.normal?.[1]
|
|
171
|
+
if (normalY !== undefined && normalY > 0.75) {
|
|
172
|
+
return 'tread'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (normalY !== undefined && Math.abs(normalY) <= 0.75) {
|
|
176
|
+
return 'side'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveRoofMaterialTarget(
|
|
183
|
+
event: RoofEvent | RoofSegmentEvent,
|
|
184
|
+
): 'top' | 'edge' | 'wall' | null {
|
|
185
|
+
const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
|
|
186
|
+
if (materialIndex === 3) return 'top'
|
|
187
|
+
if (materialIndex === 0) return 'edge'
|
|
188
|
+
if (materialIndex === 1 || materialIndex === 2) return 'wall'
|
|
189
|
+
|
|
190
|
+
const normalY = event.normal?.[1]
|
|
191
|
+
if (normalY !== undefined && normalY > 0.35) return 'top'
|
|
192
|
+
if (normalY !== undefined && Math.abs(normalY) <= 0.35) return 'edge'
|
|
193
|
+
if (normalY !== undefined && normalY < -0.35) return 'wall'
|
|
194
|
+
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getEventObject(event: NodeEvent): Object3D {
|
|
199
|
+
const eventWithObject = event as NodeEvent & { object?: Object3D }
|
|
200
|
+
return eventWithObject.object ?? event.nativeEvent.object
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getIntersectionMaterialIndex(
|
|
204
|
+
object: Object3D,
|
|
205
|
+
faceIndex: number | undefined,
|
|
206
|
+
): number | undefined {
|
|
207
|
+
if (faceIndex === undefined) return undefined
|
|
208
|
+
|
|
209
|
+
const geometry = (object as Mesh).geometry as BufferGeometry | undefined
|
|
210
|
+
if (!geometry || geometry.groups.length === 0) return undefined
|
|
211
|
+
|
|
212
|
+
const triangleStart = faceIndex * 3
|
|
213
|
+
const group = geometry.groups.find(
|
|
214
|
+
(entry) => triangleStart >= entry.start && triangleStart < entry.start + entry.count,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return group?.materialIndex
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getRegisteredNodeObject(nodeId: string): Object3D | null {
|
|
221
|
+
return sceneRegistry.nodes.get(nodeId) ?? null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getRegisteredMesh(nodeId: string): Mesh | null {
|
|
225
|
+
const object = getRegisteredNodeObject(nodeId)
|
|
226
|
+
return object && (object as Mesh).isMesh ? (object as Mesh) : null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function previewMeshMaterial(mesh: Mesh, material: Material | Material[]): PaintPreviewCleanup {
|
|
230
|
+
const previousMaterial = mesh.material
|
|
231
|
+
mesh.material = material
|
|
232
|
+
return () => {
|
|
233
|
+
mesh.material = previousMaterial
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function previewCursor(cursor: string): PaintPreviewCleanup {
|
|
238
|
+
const previousCursor = document.body.style.cursor
|
|
239
|
+
document.body.style.cursor = cursor
|
|
240
|
+
return () => {
|
|
241
|
+
document.body.style.cursor = previousCursor
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getSingleSurfacePreviewMaterial(material: ActivePaintMaterial): Material | null {
|
|
246
|
+
if (material.materialPreset) {
|
|
247
|
+
return createMaterialFromPresetRef(material.materialPreset)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (material.material) {
|
|
251
|
+
return createMaterial(material.material)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function applyWallPaintPreview(
|
|
258
|
+
node: WallNode,
|
|
259
|
+
role: WallSurfaceSide,
|
|
260
|
+
material: ActivePaintMaterial,
|
|
261
|
+
): PaintPreviewCleanup | null {
|
|
262
|
+
const mesh = getRegisteredMesh(node.id)
|
|
263
|
+
if (!mesh) return null
|
|
264
|
+
|
|
265
|
+
const previewNode = {
|
|
266
|
+
...node,
|
|
267
|
+
...buildWallSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return previewMeshMaterial(mesh, getVisibleWallMaterials(previewNode))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function applyRoofPaintPreview(
|
|
274
|
+
node: RoofNode,
|
|
275
|
+
role: 'top' | 'edge' | 'wall',
|
|
276
|
+
material: ActivePaintMaterial,
|
|
277
|
+
): PaintPreviewCleanup | null {
|
|
278
|
+
const root = getRegisteredNodeObject(node.id)
|
|
279
|
+
const mesh = root?.getObjectByName('merged-roof') as Mesh | undefined
|
|
280
|
+
if (!mesh) return null
|
|
281
|
+
|
|
282
|
+
const previewNode = {
|
|
283
|
+
...node,
|
|
284
|
+
...buildRoofSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
|
285
|
+
}
|
|
286
|
+
const previewMaterial = getRoofMaterialArray(previewNode)
|
|
287
|
+
if (!previewMaterial) return null
|
|
288
|
+
|
|
289
|
+
return previewMeshMaterial(mesh, previewMaterial)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function applyStairPaintPreview(
|
|
293
|
+
node: StairNode,
|
|
294
|
+
role: StairSurfaceMaterialRole,
|
|
295
|
+
material: ActivePaintMaterial,
|
|
296
|
+
): PaintPreviewCleanup | null {
|
|
297
|
+
const root = getRegisteredNodeObject(node.id)
|
|
298
|
+
if (!root) return null
|
|
299
|
+
|
|
300
|
+
const previewNode = {
|
|
301
|
+
...node,
|
|
302
|
+
...buildStairSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
|
303
|
+
}
|
|
304
|
+
const bodyMaterials = getStairBodyMaterials(previewNode)
|
|
305
|
+
const railingMaterial = getStairRailingMaterial(previewNode)
|
|
306
|
+
const restores: PaintPreviewCleanup[] = []
|
|
307
|
+
|
|
308
|
+
root.traverse((object) => {
|
|
309
|
+
if (!(object as Mesh).isMesh) return
|
|
310
|
+
const mesh = object as Mesh
|
|
311
|
+
if (mesh.name.startsWith('stair-railing')) {
|
|
312
|
+
restores.push(previewMeshMaterial(mesh, railingMaterial))
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
if (Array.isArray(mesh.material) && mesh.material.length === 2) {
|
|
316
|
+
restores.push(previewMeshMaterial(mesh, bodyMaterials))
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
if (mesh.name === 'merged-stair') {
|
|
320
|
+
restores.push(previewMeshMaterial(mesh, bodyMaterials))
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
if (mesh.name.startsWith('stair-side')) {
|
|
324
|
+
restores.push(previewMeshMaterial(mesh, bodyMaterials[1]))
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
if (restores.length === 0) return null
|
|
329
|
+
|
|
330
|
+
return () => {
|
|
331
|
+
for (let index = restores.length - 1; index >= 0; index -= 1) {
|
|
332
|
+
restores[index]?.()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function applySingleSurfacePaintPreview(
|
|
338
|
+
node: FenceNode | ColumnNode | SlabNode | CeilingNode,
|
|
339
|
+
material: ActivePaintMaterial,
|
|
340
|
+
): PaintPreviewCleanup | null {
|
|
341
|
+
if (node.type === 'ceiling') {
|
|
342
|
+
const root = getRegisteredMesh(node.id)
|
|
343
|
+
const overlay = root?.getObjectByName('ceiling-grid') as Mesh | undefined
|
|
344
|
+
if (!root || !overlay) return null
|
|
345
|
+
|
|
346
|
+
const previewColor =
|
|
347
|
+
getMaterialPresetByRef(material.materialPreset)?.mapProperties.color ??
|
|
348
|
+
resolveMaterial(material.material).color ??
|
|
349
|
+
'#999999'
|
|
350
|
+
|
|
351
|
+
const previousRootMaterial = root.material
|
|
352
|
+
const previousOverlayMaterial = overlay.material
|
|
353
|
+
const rootPreviewMaterial = Array.isArray(previousRootMaterial)
|
|
354
|
+
? previousRootMaterial.map((entry) => entry.clone())
|
|
355
|
+
: previousRootMaterial.clone()
|
|
356
|
+
const overlayPreviewMaterial = Array.isArray(previousOverlayMaterial)
|
|
357
|
+
? previousOverlayMaterial.map((entry) => entry.clone())
|
|
358
|
+
: previousOverlayMaterial.clone()
|
|
359
|
+
|
|
360
|
+
const applyColor = (input: Material | Material[]) => {
|
|
361
|
+
const materials = Array.isArray(input) ? input : [input]
|
|
362
|
+
for (const entry of materials) {
|
|
363
|
+
const materialWithColor = entry as Material & { color?: Color; needsUpdate?: boolean }
|
|
364
|
+
if (materialWithColor.color instanceof Color) {
|
|
365
|
+
materialWithColor.color = new Color(previewColor)
|
|
366
|
+
}
|
|
367
|
+
materialWithColor.needsUpdate = true
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
applyColor(rootPreviewMaterial)
|
|
372
|
+
applyColor(overlayPreviewMaterial)
|
|
373
|
+
root.material = rootPreviewMaterial
|
|
374
|
+
overlay.material = overlayPreviewMaterial
|
|
375
|
+
|
|
376
|
+
return () => {
|
|
377
|
+
root.material = previousRootMaterial
|
|
378
|
+
overlay.material = previousOverlayMaterial
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const registeredObject = getRegisteredNodeObject(node.id)
|
|
383
|
+
const mesh =
|
|
384
|
+
registeredObject && (registeredObject as Mesh).isMesh ? (registeredObject as Mesh) : null
|
|
385
|
+
|
|
386
|
+
const previewMaterial = getSingleSurfacePreviewMaterial(material)
|
|
387
|
+
if (!previewMaterial) return null
|
|
388
|
+
|
|
389
|
+
if (node.type === 'column') {
|
|
390
|
+
if (!registeredObject) return null
|
|
391
|
+
const restores: PaintPreviewCleanup[] = []
|
|
392
|
+
|
|
393
|
+
registeredObject.traverse((object) => {
|
|
394
|
+
if (!(object as Mesh).isMesh) return
|
|
395
|
+
restores.push(previewMeshMaterial(object as Mesh, previewMaterial))
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
if (restores.length === 0) return null
|
|
399
|
+
return () => {
|
|
400
|
+
for (let index = restores.length - 1; index >= 0; index -= 1) {
|
|
401
|
+
restores[index]?.()
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!mesh) return null
|
|
407
|
+
|
|
408
|
+
if (node.type === 'slab') {
|
|
409
|
+
const slabMaterial = previewMaterial.clone()
|
|
410
|
+
applyMaterialPresetToMaterials(slabMaterial, getMaterialPresetByRef(material.materialPreset))
|
|
411
|
+
const previewMeshMaterialInput = slabMaterial as Material & {
|
|
412
|
+
alphaMap?: unknown
|
|
413
|
+
depthWrite?: boolean
|
|
414
|
+
needsUpdate?: boolean
|
|
415
|
+
opacity?: number
|
|
416
|
+
side?: number
|
|
417
|
+
transparent?: boolean
|
|
418
|
+
}
|
|
419
|
+
previewMeshMaterialInput.transparent = false
|
|
420
|
+
previewMeshMaterialInput.opacity = 1
|
|
421
|
+
previewMeshMaterialInput.alphaMap = null
|
|
422
|
+
previewMeshMaterialInput.depthWrite = true
|
|
423
|
+
previewMeshMaterialInput.needsUpdate = true
|
|
424
|
+
return previewMeshMaterial(mesh, slabMaterial)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return previewMeshMaterial(mesh, previewMaterial)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function setSelectedMaterialTargetForNode(node: AnyNode, role: MaterialTargetRole | null) {
|
|
431
|
+
if (!role) {
|
|
432
|
+
const currentTarget = useEditor.getState().selectedMaterialTarget
|
|
433
|
+
if (currentTarget?.nodeId !== node.id) {
|
|
434
|
+
useEditor.getState().setSelectedMaterialTarget(null)
|
|
435
|
+
}
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
useEditor.getState().setSelectedMaterialTarget({
|
|
440
|
+
nodeId: node.id as AnyNodeId,
|
|
441
|
+
role,
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
71
445
|
const HIGHLIGHT_PROFILES = {
|
|
72
446
|
delete: {
|
|
73
447
|
color: new Color('#dc2626'),
|
|
@@ -84,6 +458,7 @@ const HIGHLIGHT_PROFILES = {
|
|
|
84
458
|
} as const
|
|
85
459
|
|
|
86
460
|
type HighlightKind = keyof typeof HIGHLIGHT_PROFILES
|
|
461
|
+
type HoverHighlightMode = 'default' | 'delete' | 'paint-ready' | 'paint-disabled'
|
|
87
462
|
|
|
88
463
|
type HighlightableMaterial = Material & {
|
|
89
464
|
color?: Color
|
|
@@ -189,6 +564,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
189
564
|
'wall',
|
|
190
565
|
'fence',
|
|
191
566
|
'item',
|
|
567
|
+
'column',
|
|
192
568
|
'zone',
|
|
193
569
|
'slab',
|
|
194
570
|
'ceiling',
|
|
@@ -196,6 +572,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
196
572
|
'roof-segment',
|
|
197
573
|
'stair',
|
|
198
574
|
'stair-segment',
|
|
575
|
+
'spawn',
|
|
199
576
|
'window',
|
|
200
577
|
'door',
|
|
201
578
|
],
|
|
@@ -241,12 +618,14 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
241
618
|
if (
|
|
242
619
|
node.type === 'wall' ||
|
|
243
620
|
node.type === 'fence' ||
|
|
621
|
+
node.type === 'column' ||
|
|
244
622
|
node.type === 'slab' ||
|
|
245
623
|
node.type === 'ceiling' ||
|
|
246
624
|
node.type === 'roof' ||
|
|
247
625
|
node.type === 'roof-segment' ||
|
|
248
626
|
node.type === 'stair' ||
|
|
249
|
-
node.type === 'stair-segment'
|
|
627
|
+
node.type === 'stair-segment' ||
|
|
628
|
+
node.type === 'spawn'
|
|
250
629
|
)
|
|
251
630
|
return true
|
|
252
631
|
if (node.type === 'item') {
|
|
@@ -303,12 +682,14 @@ const getSelectionTarget = (node: AnyNode): SelectionTarget | null => {
|
|
|
303
682
|
if (
|
|
304
683
|
node.type === 'wall' ||
|
|
305
684
|
node.type === 'fence' ||
|
|
685
|
+
node.type === 'column' ||
|
|
306
686
|
node.type === 'slab' ||
|
|
307
687
|
node.type === 'ceiling' ||
|
|
308
688
|
node.type === 'roof' ||
|
|
309
689
|
node.type === 'roof-segment' ||
|
|
310
690
|
node.type === 'stair' ||
|
|
311
691
|
node.type === 'stair-segment' ||
|
|
692
|
+
node.type === 'spawn' ||
|
|
312
693
|
node.type === 'window' ||
|
|
313
694
|
node.type === 'door'
|
|
314
695
|
) {
|
|
@@ -346,15 +727,302 @@ export const SelectionManager = () => {
|
|
|
346
727
|
const clickHandledRef = useRef(false)
|
|
347
728
|
|
|
348
729
|
const movingNode = useEditor((s) => s.movingNode)
|
|
730
|
+
const curvingWall = useEditor((s) => s.curvingWall)
|
|
731
|
+
const curvingFence = useEditor((s) => s.curvingFence)
|
|
349
732
|
|
|
350
733
|
useEffect(() => {
|
|
351
|
-
|
|
734
|
+
const nextHoverMode: HoverHighlightMode = mode === 'delete' ? 'delete' : 'default'
|
|
735
|
+
setHoverHighlightMode(nextHoverMode)
|
|
352
736
|
|
|
353
737
|
return () => {
|
|
354
738
|
setHoverHighlightMode('default')
|
|
355
739
|
}
|
|
356
740
|
}, [mode, setHoverHighlightMode])
|
|
357
741
|
|
|
742
|
+
useEffect(() => {
|
|
743
|
+
if (mode !== 'material-paint') return
|
|
744
|
+
if (movingNode || curvingWall) return
|
|
745
|
+
|
|
746
|
+
let activePreview: { key: string; restore: PaintPreviewCleanup } | null = null
|
|
747
|
+
|
|
748
|
+
const clearActivePreview = () => {
|
|
749
|
+
activePreview?.restore()
|
|
750
|
+
activePreview = null
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const resolveActivePaintMaterial = () =>
|
|
754
|
+
useEditor.getState().activePaintMaterial ??
|
|
755
|
+
resolveActivePaintMaterialFromSelection({
|
|
756
|
+
nodes: useScene.getState().nodes,
|
|
757
|
+
selectedId:
|
|
758
|
+
useViewer.getState().selection.selectedIds.length === 1
|
|
759
|
+
? (useViewer.getState().selection.selectedIds[0] ?? null)
|
|
760
|
+
: null,
|
|
761
|
+
selectedMaterialTarget: useEditor.getState().selectedMaterialTarget,
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
const getPaintInteraction = (event: NodeEvent): PaintInteraction | null => {
|
|
765
|
+
const activePaintMaterial = resolveActivePaintMaterial()
|
|
766
|
+
const node = event.node
|
|
767
|
+
|
|
768
|
+
if (!isNodeInCurrentLevel(node)) return null
|
|
769
|
+
|
|
770
|
+
if (node.type === 'wall') {
|
|
771
|
+
const role = resolveWallMaterialTarget(event as WallEvent)
|
|
772
|
+
const compatible = role !== null && hasActivePaintMaterial(activePaintMaterial)
|
|
773
|
+
return {
|
|
774
|
+
key: `wall:${node.id}:${role ?? 'unsupported'}`,
|
|
775
|
+
hoveredId: node.id as AnyNodeId,
|
|
776
|
+
hoverMode:
|
|
777
|
+
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
|
778
|
+
? 'paint-ready'
|
|
779
|
+
: 'paint-disabled',
|
|
780
|
+
apply:
|
|
781
|
+
compatible && hasActivePaintMaterial(activePaintMaterial)
|
|
782
|
+
? () => {
|
|
783
|
+
useScene
|
|
784
|
+
.getState()
|
|
785
|
+
.updateNode(
|
|
786
|
+
node.id as AnyNodeId,
|
|
787
|
+
buildWallSurfaceMaterialPatch(
|
|
788
|
+
node as WallNode,
|
|
789
|
+
role!,
|
|
790
|
+
activePaintMaterial.material,
|
|
791
|
+
activePaintMaterial.materialPreset,
|
|
792
|
+
),
|
|
793
|
+
)
|
|
794
|
+
}
|
|
795
|
+
: null,
|
|
796
|
+
preview:
|
|
797
|
+
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
|
798
|
+
? () => applyWallPaintPreview(node as WallNode, role, activePaintMaterial)
|
|
799
|
+
: () => previewCursor('not-allowed'),
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (node.type === 'roof' || node.type === 'roof-segment') {
|
|
804
|
+
const roofNode =
|
|
805
|
+
node.type === 'roof'
|
|
806
|
+
? node
|
|
807
|
+
: node.parentId
|
|
808
|
+
? useScene.getState().nodes[node.parentId as AnyNodeId]
|
|
809
|
+
: null
|
|
810
|
+
if (!roofNode || roofNode.type !== 'roof') return null
|
|
811
|
+
|
|
812
|
+
const role = resolveRoofMaterialTarget(event as RoofEvent | RoofSegmentEvent)
|
|
813
|
+
const compatible = role !== null && hasActivePaintMaterial(activePaintMaterial)
|
|
814
|
+
return {
|
|
815
|
+
key: `roof:${roofNode.id}:${role ?? 'unsupported'}`,
|
|
816
|
+
hoveredId: roofNode.id as AnyNodeId,
|
|
817
|
+
hoverMode:
|
|
818
|
+
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
|
819
|
+
? 'paint-ready'
|
|
820
|
+
: 'paint-disabled',
|
|
821
|
+
apply:
|
|
822
|
+
compatible && hasActivePaintMaterial(activePaintMaterial)
|
|
823
|
+
? () => {
|
|
824
|
+
useScene
|
|
825
|
+
.getState()
|
|
826
|
+
.updateNode(
|
|
827
|
+
roofNode.id as AnyNodeId,
|
|
828
|
+
buildRoofSurfaceMaterialPatch(
|
|
829
|
+
roofNode as RoofNode,
|
|
830
|
+
role!,
|
|
831
|
+
activePaintMaterial.material,
|
|
832
|
+
activePaintMaterial.materialPreset,
|
|
833
|
+
),
|
|
834
|
+
)
|
|
835
|
+
}
|
|
836
|
+
: null,
|
|
837
|
+
preview:
|
|
838
|
+
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
|
839
|
+
? () => applyRoofPaintPreview(roofNode as RoofNode, role, activePaintMaterial)
|
|
840
|
+
: () => previewCursor('not-allowed'),
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (node.type === 'stair' || node.type === 'stair-segment') {
|
|
845
|
+
const stairNode =
|
|
846
|
+
node.type === 'stair'
|
|
847
|
+
? node
|
|
848
|
+
: node.parentId
|
|
849
|
+
? useScene.getState().nodes[node.parentId as AnyNodeId]
|
|
850
|
+
: null
|
|
851
|
+
if (!stairNode || stairNode.type !== 'stair') return null
|
|
852
|
+
|
|
853
|
+
const role = resolveStairMaterialTarget(event as StairEvent | StairSegmentEvent)
|
|
854
|
+
const compatible = role !== null && hasActivePaintMaterial(activePaintMaterial)
|
|
855
|
+
return {
|
|
856
|
+
key: `stair:${stairNode.id}:${role ?? 'unsupported'}`,
|
|
857
|
+
hoveredId: stairNode.id as AnyNodeId,
|
|
858
|
+
hoverMode:
|
|
859
|
+
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
|
860
|
+
? 'paint-ready'
|
|
861
|
+
: 'paint-disabled',
|
|
862
|
+
apply:
|
|
863
|
+
compatible && hasActivePaintMaterial(activePaintMaterial)
|
|
864
|
+
? () => {
|
|
865
|
+
useScene
|
|
866
|
+
.getState()
|
|
867
|
+
.updateNode(
|
|
868
|
+
stairNode.id as AnyNodeId,
|
|
869
|
+
buildStairSurfaceMaterialPatch(
|
|
870
|
+
stairNode as StairNode,
|
|
871
|
+
role!,
|
|
872
|
+
activePaintMaterial.material,
|
|
873
|
+
activePaintMaterial.materialPreset,
|
|
874
|
+
),
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
: null,
|
|
878
|
+
preview:
|
|
879
|
+
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
|
880
|
+
? () => applyStairPaintPreview(stairNode as StairNode, role, activePaintMaterial)
|
|
881
|
+
: () => previewCursor('not-allowed'),
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (
|
|
886
|
+
node.type === 'fence' ||
|
|
887
|
+
node.type === 'column' ||
|
|
888
|
+
node.type === 'slab' ||
|
|
889
|
+
node.type === 'ceiling'
|
|
890
|
+
) {
|
|
891
|
+
const compatible = hasActivePaintMaterial(activePaintMaterial)
|
|
892
|
+
|
|
893
|
+
return {
|
|
894
|
+
key: `${node.type}:${node.id}:surface`,
|
|
895
|
+
hoveredId: node.id as AnyNodeId,
|
|
896
|
+
hoverMode: compatible ? 'paint-ready' : 'paint-disabled',
|
|
897
|
+
apply: compatible
|
|
898
|
+
? () => {
|
|
899
|
+
useScene
|
|
900
|
+
.getState()
|
|
901
|
+
.updateNode(
|
|
902
|
+
node.id as AnyNodeId,
|
|
903
|
+
buildSingleSurfaceMaterialPatch<
|
|
904
|
+
FenceNode | ColumnNode | SlabNode | CeilingNode
|
|
905
|
+
>(activePaintMaterial.material, activePaintMaterial.materialPreset),
|
|
906
|
+
)
|
|
907
|
+
}
|
|
908
|
+
: null,
|
|
909
|
+
preview: compatible
|
|
910
|
+
? () =>
|
|
911
|
+
applySingleSurfacePaintPreview(
|
|
912
|
+
node as FenceNode | ColumnNode | SlabNode | CeilingNode,
|
|
913
|
+
activePaintMaterial,
|
|
914
|
+
)
|
|
915
|
+
: () => previewCursor('not-allowed'),
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const disabledNodeTypes = ['item', 'window', 'door', 'zone']
|
|
920
|
+
if (disabledNodeTypes.includes(node.type)) {
|
|
921
|
+
return {
|
|
922
|
+
key: `${node.type}:${node.id}:unsupported`,
|
|
923
|
+
hoveredId: node.id as AnyNodeId,
|
|
924
|
+
hoverMode: 'paint-disabled',
|
|
925
|
+
apply: null,
|
|
926
|
+
preview: () => previewCursor('not-allowed'),
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return null
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const onEnter = (event: NodeEvent) => {
|
|
934
|
+
if (boxSelectHandled) return
|
|
935
|
+
|
|
936
|
+
const interaction = getPaintInteraction(event)
|
|
937
|
+
if (!interaction) return
|
|
938
|
+
|
|
939
|
+
event.stopPropagation()
|
|
940
|
+
|
|
941
|
+
if (activePreview?.key === interaction.key) {
|
|
942
|
+
return
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
clearActivePreview()
|
|
946
|
+
useViewer.setState({ hoveredId: interaction.hoveredId })
|
|
947
|
+
setHoverHighlightMode(interaction.hoverMode)
|
|
948
|
+
|
|
949
|
+
const restore = interaction.preview?.()
|
|
950
|
+
if (restore) {
|
|
951
|
+
activePreview = { key: interaction.key, restore }
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const onLeave = (event: NodeEvent) => {
|
|
956
|
+
const interaction = getPaintInteraction(event)
|
|
957
|
+
if (!interaction) return
|
|
958
|
+
|
|
959
|
+
if (activePreview?.key !== interaction.key) {
|
|
960
|
+
return
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
clearActivePreview()
|
|
964
|
+
if (useViewer.getState().hoveredId === interaction.hoveredId) {
|
|
965
|
+
useViewer.setState({ hoveredId: null })
|
|
966
|
+
}
|
|
967
|
+
setHoverHighlightMode('default')
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const onClick = (event: NodeEvent) => {
|
|
971
|
+
if (boxSelectHandled) return
|
|
972
|
+
|
|
973
|
+
const interaction = getPaintInteraction(event)
|
|
974
|
+
if (!interaction) return
|
|
975
|
+
|
|
976
|
+
event.stopPropagation()
|
|
977
|
+
|
|
978
|
+
if (!interaction.apply) {
|
|
979
|
+
return
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
interaction.apply()
|
|
983
|
+
if (activePreview?.key === interaction.key) {
|
|
984
|
+
activePreview = null
|
|
985
|
+
} else {
|
|
986
|
+
clearActivePreview()
|
|
987
|
+
}
|
|
988
|
+
setHoverHighlightMode(interaction.hoverMode)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const allTypes = [
|
|
992
|
+
'wall',
|
|
993
|
+
'fence',
|
|
994
|
+
'item',
|
|
995
|
+
'column',
|
|
996
|
+
'slab',
|
|
997
|
+
'ceiling',
|
|
998
|
+
'roof',
|
|
999
|
+
'roof-segment',
|
|
1000
|
+
'stair',
|
|
1001
|
+
'stair-segment',
|
|
1002
|
+
'spawn',
|
|
1003
|
+
'window',
|
|
1004
|
+
'door',
|
|
1005
|
+
'zone',
|
|
1006
|
+
] as const
|
|
1007
|
+
|
|
1008
|
+
for (const type of allTypes) {
|
|
1009
|
+
emitter.on(`${type}:enter` as any, onEnter as any)
|
|
1010
|
+
emitter.on(`${type}:leave` as any, onLeave as any)
|
|
1011
|
+
emitter.on(`${type}:click` as any, onClick as any)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return () => {
|
|
1015
|
+
for (const type of allTypes) {
|
|
1016
|
+
emitter.off(`${type}:enter` as any, onEnter as any)
|
|
1017
|
+
emitter.off(`${type}:leave` as any, onLeave as any)
|
|
1018
|
+
emitter.off(`${type}:click` as any, onClick as any)
|
|
1019
|
+
}
|
|
1020
|
+
clearActivePreview()
|
|
1021
|
+
useViewer.setState({ hoveredId: null })
|
|
1022
|
+
setHoverHighlightMode('default')
|
|
1023
|
+
}
|
|
1024
|
+
}, [curvingWall, mode, movingNode, setHoverHighlightMode])
|
|
1025
|
+
|
|
358
1026
|
useEffect(() => {
|
|
359
1027
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
360
1028
|
if (event.key === 'Meta') modifierKeysRef.current.meta = true
|
|
@@ -384,7 +1052,7 @@ export const SelectionManager = () => {
|
|
|
384
1052
|
|
|
385
1053
|
useEffect(() => {
|
|
386
1054
|
if (mode !== 'select') return
|
|
387
|
-
if (movingNode) return
|
|
1055
|
+
if (movingNode || curvingWall || curvingFence) return
|
|
388
1056
|
|
|
389
1057
|
const onClick = (event: NodeEvent) => {
|
|
390
1058
|
// Skip if box-select just completed (drag ended over a node)
|
|
@@ -438,6 +1106,50 @@ export const SelectionManager = () => {
|
|
|
438
1106
|
|
|
439
1107
|
activeStrategy.handleSelect(nodeToSelect, event.nativeEvent, modifierKeysRef.current)
|
|
440
1108
|
|
|
1109
|
+
let nextMaterialTargetHandled = false
|
|
1110
|
+
|
|
1111
|
+
if (node.type === 'wall' && nodeToSelect.type === 'wall') {
|
|
1112
|
+
setSelectedMaterialTargetForNode(
|
|
1113
|
+
nodeToSelect,
|
|
1114
|
+
resolveWallMaterialTarget(event as WallEvent),
|
|
1115
|
+
)
|
|
1116
|
+
nextMaterialTargetHandled = true
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (
|
|
1120
|
+
(node.type === 'stair' || node.type === 'stair-segment') &&
|
|
1121
|
+
nodeToSelect.type === 'stair'
|
|
1122
|
+
) {
|
|
1123
|
+
setSelectedMaterialTargetForNode(
|
|
1124
|
+
nodeToSelect,
|
|
1125
|
+
resolveStairMaterialTarget(event as StairEvent | StairSegmentEvent),
|
|
1126
|
+
)
|
|
1127
|
+
nextMaterialTargetHandled = true
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (
|
|
1131
|
+
(node.type === 'roof' || node.type === 'roof-segment') &&
|
|
1132
|
+
nodeToSelect.type === 'roof'
|
|
1133
|
+
) {
|
|
1134
|
+
setSelectedMaterialTargetForNode(
|
|
1135
|
+
nodeToSelect,
|
|
1136
|
+
resolveRoofMaterialTarget(event as RoofEvent | RoofSegmentEvent),
|
|
1137
|
+
)
|
|
1138
|
+
nextMaterialTargetHandled = true
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (
|
|
1142
|
+
(node.type === 'fence' || node.type === 'slab' || node.type === 'ceiling') &&
|
|
1143
|
+
nodeToSelect.type === node.type
|
|
1144
|
+
) {
|
|
1145
|
+
setSelectedMaterialTargetForNode(nodeToSelect, 'surface')
|
|
1146
|
+
nextMaterialTargetHandled = true
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (!nextMaterialTargetHandled && useEditor.getState().selectedMaterialTarget) {
|
|
1150
|
+
useEditor.getState().setSelectedMaterialTarget(null)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
441
1153
|
// Reset the handled flag after a short delay to allow grid:click to be ignored
|
|
442
1154
|
setTimeout(() => {
|
|
443
1155
|
clickHandledRef.current = false
|
|
@@ -449,6 +1161,7 @@ export const SelectionManager = () => {
|
|
|
449
1161
|
'wall',
|
|
450
1162
|
'fence',
|
|
451
1163
|
'item',
|
|
1164
|
+
'column',
|
|
452
1165
|
'building',
|
|
453
1166
|
'zone',
|
|
454
1167
|
'slab',
|
|
@@ -457,6 +1170,7 @@ export const SelectionManager = () => {
|
|
|
457
1170
|
'roof-segment',
|
|
458
1171
|
'stair',
|
|
459
1172
|
'stair-segment',
|
|
1173
|
+
'spawn',
|
|
460
1174
|
'window',
|
|
461
1175
|
'door',
|
|
462
1176
|
]
|
|
@@ -470,6 +1184,7 @@ export const SelectionManager = () => {
|
|
|
470
1184
|
const { phase, structureLayer } = useEditor.getState()
|
|
471
1185
|
const activeStrategy = SELECTION_STRATEGIES[phase]
|
|
472
1186
|
if (activeStrategy) activeStrategy.handleDeselect()
|
|
1187
|
+
useEditor.getState().setSelectedMaterialTarget(null)
|
|
473
1188
|
|
|
474
1189
|
// When deselecting from zone mode, return to structure select
|
|
475
1190
|
if (phase === 'structure' && structureLayer === 'zones') {
|
|
@@ -485,12 +1200,12 @@ export const SelectionManager = () => {
|
|
|
485
1200
|
})
|
|
486
1201
|
emitter.off('grid:click', onGridClick)
|
|
487
1202
|
}
|
|
488
|
-
}, [mode, movingNode])
|
|
1203
|
+
}, [curvingFence, curvingWall, mode, movingNode])
|
|
489
1204
|
|
|
490
1205
|
// Global double-click handler for auto-switching phases and cross-phase hover
|
|
491
1206
|
useEffect(() => {
|
|
492
1207
|
if (mode !== 'select') return
|
|
493
|
-
if (movingNode) return
|
|
1208
|
+
if (movingNode || curvingWall || curvingFence) return
|
|
494
1209
|
|
|
495
1210
|
const onEnter = (event: NodeEvent) => {
|
|
496
1211
|
const node = event.node
|
|
@@ -543,12 +1258,14 @@ export const SelectionManager = () => {
|
|
|
543
1258
|
} else if (
|
|
544
1259
|
node.type === 'wall' ||
|
|
545
1260
|
node.type === 'fence' ||
|
|
1261
|
+
node.type === 'column' ||
|
|
546
1262
|
node.type === 'slab' ||
|
|
547
1263
|
node.type === 'ceiling' ||
|
|
548
1264
|
node.type === 'roof' ||
|
|
549
1265
|
node.type === 'roof-segment' ||
|
|
550
1266
|
node.type === 'stair' ||
|
|
551
1267
|
node.type === 'stair-segment' ||
|
|
1268
|
+
node.type === 'spawn' ||
|
|
552
1269
|
node.type === 'window' ||
|
|
553
1270
|
node.type === 'door'
|
|
554
1271
|
) {
|
|
@@ -594,6 +1311,7 @@ export const SelectionManager = () => {
|
|
|
594
1311
|
'wall',
|
|
595
1312
|
'fence',
|
|
596
1313
|
'item',
|
|
1314
|
+
'column',
|
|
597
1315
|
'building',
|
|
598
1316
|
'slab',
|
|
599
1317
|
'ceiling',
|
|
@@ -601,6 +1319,7 @@ export const SelectionManager = () => {
|
|
|
601
1319
|
'roof-segment',
|
|
602
1320
|
'stair',
|
|
603
1321
|
'stair-segment',
|
|
1322
|
+
'spawn',
|
|
604
1323
|
'window',
|
|
605
1324
|
'door',
|
|
606
1325
|
'zone',
|
|
@@ -619,7 +1338,7 @@ export const SelectionManager = () => {
|
|
|
619
1338
|
emitter.off(`${type}:double-click` as any, onDoubleClick as any)
|
|
620
1339
|
})
|
|
621
1340
|
}
|
|
622
|
-
}, [mode, movingNode])
|
|
1341
|
+
}, [curvingFence, curvingWall, mode, movingNode])
|
|
623
1342
|
|
|
624
1343
|
// Delete mode: click-to-delete (sledgehammer tool)
|
|
625
1344
|
useEffect(() => {
|
|
@@ -666,6 +1385,7 @@ export const SelectionManager = () => {
|
|
|
666
1385
|
'wall',
|
|
667
1386
|
'fence',
|
|
668
1387
|
'item',
|
|
1388
|
+
'column',
|
|
669
1389
|
'slab',
|
|
670
1390
|
'ceiling',
|
|
671
1391
|
'roof',
|
|
@@ -703,6 +1423,12 @@ export const SelectionManager = () => {
|
|
|
703
1423
|
}
|
|
704
1424
|
|
|
705
1425
|
const SelectionStateSync = () => {
|
|
1426
|
+
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
1427
|
+
const setSelectedMaterialTarget = useEditor((s) => s.setSelectedMaterialTarget)
|
|
1428
|
+
const singleSelectedId = useViewer((s) =>
|
|
1429
|
+
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
|
|
1430
|
+
)
|
|
1431
|
+
|
|
706
1432
|
useEffect(() => {
|
|
707
1433
|
return useScene.subscribe((state) => {
|
|
708
1434
|
const { buildingId, levelId, zoneId, selectedIds } = useViewer.getState().selection
|
|
@@ -731,6 +1457,33 @@ const SelectionStateSync = () => {
|
|
|
731
1457
|
})
|
|
732
1458
|
}, [])
|
|
733
1459
|
|
|
1460
|
+
useEffect(() => {
|
|
1461
|
+
if (!selectedMaterialTarget) return
|
|
1462
|
+
|
|
1463
|
+
if (!singleSelectedId) {
|
|
1464
|
+
setSelectedMaterialTarget(null)
|
|
1465
|
+
return
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const selectedNode = useScene.getState().nodes[singleSelectedId as AnyNodeId]
|
|
1469
|
+
if (
|
|
1470
|
+
!selectedNode ||
|
|
1471
|
+
(selectedNode.type !== 'wall' &&
|
|
1472
|
+
selectedNode.type !== 'fence' &&
|
|
1473
|
+
selectedNode.type !== 'slab' &&
|
|
1474
|
+
selectedNode.type !== 'ceiling' &&
|
|
1475
|
+
selectedNode.type !== 'stair' &&
|
|
1476
|
+
selectedNode.type !== 'roof')
|
|
1477
|
+
) {
|
|
1478
|
+
setSelectedMaterialTarget(null)
|
|
1479
|
+
return
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (selectedMaterialTarget.nodeId !== selectedNode.id) {
|
|
1483
|
+
setSelectedMaterialTarget(null)
|
|
1484
|
+
}
|
|
1485
|
+
}, [selectedMaterialTarget, setSelectedMaterialTarget, singleSelectedId])
|
|
1486
|
+
|
|
734
1487
|
return null
|
|
735
1488
|
}
|
|
736
1489
|
|
|
@@ -830,7 +1583,8 @@ const SelectionMaterialSync = () => {
|
|
|
830
1583
|
}, [hoverHighlightMode, hoveredId, previewSelectedIds, selectedIds, syncSelectionMaterials])
|
|
831
1584
|
|
|
832
1585
|
useEffect(() => {
|
|
833
|
-
return useScene.subscribe(() => {
|
|
1586
|
+
return useScene.subscribe((state, prevState) => {
|
|
1587
|
+
if (state.nodes === prevState.nodes) return
|
|
834
1588
|
syncSelectionMaterials()
|
|
835
1589
|
})
|
|
836
1590
|
}, [syncSelectionMaterials])
|
|
@@ -919,13 +1673,13 @@ const EditorOutlinerSync = () => {
|
|
|
919
1673
|
outliner.selectedObjects.length = 0
|
|
920
1674
|
for (const id of idsToHighlight) {
|
|
921
1675
|
const obj = sceneRegistry.nodes.get(id)
|
|
922
|
-
if (obj) outliner.selectedObjects.push(obj)
|
|
1676
|
+
if (obj?.parent) outliner.selectedObjects.push(obj)
|
|
923
1677
|
}
|
|
924
1678
|
|
|
925
1679
|
outliner.hoveredObjects.length = 0
|
|
926
1680
|
if (hoveredId) {
|
|
927
1681
|
const obj = sceneRegistry.nodes.get(hoveredId)
|
|
928
|
-
if (obj) outliner.hoveredObjects.push(obj)
|
|
1682
|
+
if (obj?.parent) outliner.hoveredObjects.push(obj)
|
|
929
1683
|
}
|
|
930
1684
|
}, [phase, previewSelectedIds, selection, hoveredId, outliner])
|
|
931
1685
|
|