@pascal-app/editor 0.6.0 → 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 +9 -5
- 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 +20 -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 +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9855 -3298
- package/src/components/editor/index.tsx +269 -21
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/thumbnail-generator.tsx +38 -7
- 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 +267 -36
- 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/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- 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 +7 -0
- package/src/components/tools/door/move-door-tool.tsx +28 -8
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
- package/src/components/tools/fence/move-fence-tool.tsx +101 -34
- package/src/components/tools/item/move-tool.tsx +10 -1
- package/src/components/tools/item/placement-math.ts +30 -1
- package/src/components/tools/item/placement-strategies.ts +109 -31
- 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 +660 -52
- package/src/components/tools/roof/move-roof-tool.tsx +22 -15
- package/src/components/tools/shared/polygon-editor.tsx +153 -28
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -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/tool-manager.tsx +18 -3
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +28 -1
- 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 +18 -1
- package/src/components/ui/controls/material-picker.tsx +152 -165
- package/src/components/ui/controls/slider-control.tsx +66 -18
- 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 +1116 -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 +1 -25
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +981 -289
- package/src/components/ui/panels/fence-panel.tsx +3 -45
- 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 +210 -1
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +238 -5
- package/src/components/ui/panels/roof-panel.tsx +4 -105
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
- package/src/components/ui/panels/slab-panel.tsx +4 -30
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +11 -117
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +1 -95
- package/src/components/ui/panels/window-panel.tsx +660 -139
- 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 +2 -2
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +42 -1
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-keyboard.ts +64 -7
- package/src/hooks/use-mobile.ts +12 -12
- 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/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/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +164 -8
|
@@ -2,27 +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,
|
|
8
12
|
type RoofEvent,
|
|
13
|
+
type RoofNode,
|
|
9
14
|
type RoofSegmentEvent,
|
|
10
15
|
resolveLevelId,
|
|
11
|
-
|
|
16
|
+
resolveMaterial,
|
|
17
|
+
type SlabNode,
|
|
12
18
|
type StairEvent,
|
|
13
19
|
type StairNode,
|
|
14
|
-
type StairSurfaceMaterialRole,
|
|
15
20
|
type StairSegmentEvent,
|
|
21
|
+
type StairSurfaceMaterialRole,
|
|
22
|
+
sceneRegistry,
|
|
16
23
|
useScene,
|
|
17
24
|
type WallEvent,
|
|
25
|
+
type WallNode,
|
|
18
26
|
type WallSurfaceSide,
|
|
19
27
|
} from '@pascal-app/core'
|
|
20
28
|
|
|
21
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
applyMaterialPresetToMaterials,
|
|
31
|
+
createMaterial,
|
|
32
|
+
createMaterialFromPresetRef,
|
|
33
|
+
getRoofMaterialArray,
|
|
34
|
+
getStairBodyMaterials,
|
|
35
|
+
getStairRailingMaterial,
|
|
36
|
+
getVisibleWallMaterials,
|
|
37
|
+
useViewer,
|
|
38
|
+
} from '@pascal-app/viewer'
|
|
22
39
|
import { useCallback, useEffect, useRef } from 'react'
|
|
23
|
-
import {
|
|
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'
|
|
24
50
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
25
|
-
import useEditor, {
|
|
51
|
+
import useEditor, {
|
|
52
|
+
type MaterialTargetRole,
|
|
53
|
+
type Phase,
|
|
54
|
+
type StructureLayer,
|
|
55
|
+
} from './../../store/use-editor'
|
|
26
56
|
import { boxSelectHandled } from '../tools/select/box-select-tool'
|
|
27
57
|
|
|
28
58
|
const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
|
@@ -36,6 +66,7 @@ type SelectableNodeType =
|
|
|
36
66
|
| 'wall'
|
|
37
67
|
| 'fence'
|
|
38
68
|
| 'item'
|
|
69
|
+
| 'column'
|
|
39
70
|
| 'building'
|
|
40
71
|
| 'zone'
|
|
41
72
|
| 'slab'
|
|
@@ -44,6 +75,7 @@ type SelectableNodeType =
|
|
|
44
75
|
| 'roof-segment'
|
|
45
76
|
| 'stair'
|
|
46
77
|
| 'stair-segment'
|
|
78
|
+
| 'spawn'
|
|
47
79
|
| 'window'
|
|
48
80
|
| 'door'
|
|
49
81
|
|
|
@@ -52,6 +84,16 @@ type ModifierKeys = {
|
|
|
52
84
|
ctrl: boolean
|
|
53
85
|
}
|
|
54
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
|
+
|
|
55
97
|
interface SelectionStrategy {
|
|
56
98
|
types: SelectableNodeType[]
|
|
57
99
|
handleSelect: (node: AnyNode, nativeEvent?: any, modifierKeys?: ModifierKeys) => void
|
|
@@ -175,10 +217,217 @@ function getIntersectionMaterialIndex(
|
|
|
175
217
|
return group?.materialIndex
|
|
176
218
|
}
|
|
177
219
|
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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) {
|
|
182
431
|
if (!role) {
|
|
183
432
|
const currentTarget = useEditor.getState().selectedMaterialTarget
|
|
184
433
|
if (currentTarget?.nodeId !== node.id) {
|
|
@@ -209,6 +458,7 @@ const HIGHLIGHT_PROFILES = {
|
|
|
209
458
|
} as const
|
|
210
459
|
|
|
211
460
|
type HighlightKind = keyof typeof HIGHLIGHT_PROFILES
|
|
461
|
+
type HoverHighlightMode = 'default' | 'delete' | 'paint-ready' | 'paint-disabled'
|
|
212
462
|
|
|
213
463
|
type HighlightableMaterial = Material & {
|
|
214
464
|
color?: Color
|
|
@@ -314,6 +564,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
314
564
|
'wall',
|
|
315
565
|
'fence',
|
|
316
566
|
'item',
|
|
567
|
+
'column',
|
|
317
568
|
'zone',
|
|
318
569
|
'slab',
|
|
319
570
|
'ceiling',
|
|
@@ -321,6 +572,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
321
572
|
'roof-segment',
|
|
322
573
|
'stair',
|
|
323
574
|
'stair-segment',
|
|
575
|
+
'spawn',
|
|
324
576
|
'window',
|
|
325
577
|
'door',
|
|
326
578
|
],
|
|
@@ -366,12 +618,14 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
366
618
|
if (
|
|
367
619
|
node.type === 'wall' ||
|
|
368
620
|
node.type === 'fence' ||
|
|
621
|
+
node.type === 'column' ||
|
|
369
622
|
node.type === 'slab' ||
|
|
370
623
|
node.type === 'ceiling' ||
|
|
371
624
|
node.type === 'roof' ||
|
|
372
625
|
node.type === 'roof-segment' ||
|
|
373
626
|
node.type === 'stair' ||
|
|
374
|
-
node.type === 'stair-segment'
|
|
627
|
+
node.type === 'stair-segment' ||
|
|
628
|
+
node.type === 'spawn'
|
|
375
629
|
)
|
|
376
630
|
return true
|
|
377
631
|
if (node.type === 'item') {
|
|
@@ -428,12 +682,14 @@ const getSelectionTarget = (node: AnyNode): SelectionTarget | null => {
|
|
|
428
682
|
if (
|
|
429
683
|
node.type === 'wall' ||
|
|
430
684
|
node.type === 'fence' ||
|
|
685
|
+
node.type === 'column' ||
|
|
431
686
|
node.type === 'slab' ||
|
|
432
687
|
node.type === 'ceiling' ||
|
|
433
688
|
node.type === 'roof' ||
|
|
434
689
|
node.type === 'roof-segment' ||
|
|
435
690
|
node.type === 'stair' ||
|
|
436
691
|
node.type === 'stair-segment' ||
|
|
692
|
+
node.type === 'spawn' ||
|
|
437
693
|
node.type === 'window' ||
|
|
438
694
|
node.type === 'door'
|
|
439
695
|
) {
|
|
@@ -475,13 +731,298 @@ export const SelectionManager = () => {
|
|
|
475
731
|
const curvingFence = useEditor((s) => s.curvingFence)
|
|
476
732
|
|
|
477
733
|
useEffect(() => {
|
|
478
|
-
|
|
734
|
+
const nextHoverMode: HoverHighlightMode = mode === 'delete' ? 'delete' : 'default'
|
|
735
|
+
setHoverHighlightMode(nextHoverMode)
|
|
479
736
|
|
|
480
737
|
return () => {
|
|
481
738
|
setHoverHighlightMode('default')
|
|
482
739
|
}
|
|
483
740
|
}, [mode, setHoverHighlightMode])
|
|
484
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
|
+
|
|
485
1026
|
useEffect(() => {
|
|
486
1027
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
487
1028
|
if (event.key === 'Meta') modifierKeysRef.current.meta = true
|
|
@@ -597,6 +1138,14 @@ export const SelectionManager = () => {
|
|
|
597
1138
|
nextMaterialTargetHandled = true
|
|
598
1139
|
}
|
|
599
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
|
+
|
|
600
1149
|
if (!nextMaterialTargetHandled && useEditor.getState().selectedMaterialTarget) {
|
|
601
1150
|
useEditor.getState().setSelectedMaterialTarget(null)
|
|
602
1151
|
}
|
|
@@ -612,6 +1161,7 @@ export const SelectionManager = () => {
|
|
|
612
1161
|
'wall',
|
|
613
1162
|
'fence',
|
|
614
1163
|
'item',
|
|
1164
|
+
'column',
|
|
615
1165
|
'building',
|
|
616
1166
|
'zone',
|
|
617
1167
|
'slab',
|
|
@@ -620,6 +1170,7 @@ export const SelectionManager = () => {
|
|
|
620
1170
|
'roof-segment',
|
|
621
1171
|
'stair',
|
|
622
1172
|
'stair-segment',
|
|
1173
|
+
'spawn',
|
|
623
1174
|
'window',
|
|
624
1175
|
'door',
|
|
625
1176
|
]
|
|
@@ -707,12 +1258,14 @@ export const SelectionManager = () => {
|
|
|
707
1258
|
} else if (
|
|
708
1259
|
node.type === 'wall' ||
|
|
709
1260
|
node.type === 'fence' ||
|
|
1261
|
+
node.type === 'column' ||
|
|
710
1262
|
node.type === 'slab' ||
|
|
711
1263
|
node.type === 'ceiling' ||
|
|
712
1264
|
node.type === 'roof' ||
|
|
713
1265
|
node.type === 'roof-segment' ||
|
|
714
1266
|
node.type === 'stair' ||
|
|
715
1267
|
node.type === 'stair-segment' ||
|
|
1268
|
+
node.type === 'spawn' ||
|
|
716
1269
|
node.type === 'window' ||
|
|
717
1270
|
node.type === 'door'
|
|
718
1271
|
) {
|
|
@@ -758,6 +1311,7 @@ export const SelectionManager = () => {
|
|
|
758
1311
|
'wall',
|
|
759
1312
|
'fence',
|
|
760
1313
|
'item',
|
|
1314
|
+
'column',
|
|
761
1315
|
'building',
|
|
762
1316
|
'slab',
|
|
763
1317
|
'ceiling',
|
|
@@ -765,6 +1319,7 @@ export const SelectionManager = () => {
|
|
|
765
1319
|
'roof-segment',
|
|
766
1320
|
'stair',
|
|
767
1321
|
'stair-segment',
|
|
1322
|
+
'spawn',
|
|
768
1323
|
'window',
|
|
769
1324
|
'door',
|
|
770
1325
|
'zone',
|
|
@@ -830,6 +1385,7 @@ export const SelectionManager = () => {
|
|
|
830
1385
|
'wall',
|
|
831
1386
|
'fence',
|
|
832
1387
|
'item',
|
|
1388
|
+
'column',
|
|
833
1389
|
'slab',
|
|
834
1390
|
'ceiling',
|
|
835
1391
|
'roof',
|
|
@@ -912,7 +1468,12 @@ const SelectionStateSync = () => {
|
|
|
912
1468
|
const selectedNode = useScene.getState().nodes[singleSelectedId as AnyNodeId]
|
|
913
1469
|
if (
|
|
914
1470
|
!selectedNode ||
|
|
915
|
-
(selectedNode.type !== 'wall' &&
|
|
1471
|
+
(selectedNode.type !== 'wall' &&
|
|
1472
|
+
selectedNode.type !== 'fence' &&
|
|
1473
|
+
selectedNode.type !== 'slab' &&
|
|
1474
|
+
selectedNode.type !== 'ceiling' &&
|
|
1475
|
+
selectedNode.type !== 'stair' &&
|
|
1476
|
+
selectedNode.type !== 'roof')
|
|
916
1477
|
) {
|
|
917
1478
|
setSelectedMaterialTarget(null)
|
|
918
1479
|
return
|
|
@@ -1022,7 +1583,8 @@ const SelectionMaterialSync = () => {
|
|
|
1022
1583
|
}, [hoverHighlightMode, hoveredId, previewSelectedIds, selectedIds, syncSelectionMaterials])
|
|
1023
1584
|
|
|
1024
1585
|
useEffect(() => {
|
|
1025
|
-
return useScene.subscribe(() => {
|
|
1586
|
+
return useScene.subscribe((state, prevState) => {
|
|
1587
|
+
if (state.nodes === prevState.nodes) return
|
|
1026
1588
|
syncSelectionMaterials()
|
|
1027
1589
|
})
|
|
1028
1590
|
}, [syncSelectionMaterials])
|