@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
nodeRegistry,
|
|
6
|
+
registerNode,
|
|
7
|
+
useScene,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
import useEditor from '../../../store/use-editor'
|
|
12
|
+
import { collectSelectableCandidateIds } from './select-candidates'
|
|
13
|
+
|
|
14
|
+
function registerSelectableElevatorTestKind() {
|
|
15
|
+
if (nodeRegistry.has('elevator')) return
|
|
16
|
+
|
|
17
|
+
registerNode({
|
|
18
|
+
kind: 'elevator',
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
schema: z.object({ type: z.literal('elevator') }) as never,
|
|
21
|
+
category: 'structure',
|
|
22
|
+
defaults: () => ({}),
|
|
23
|
+
capabilities: { selectable: {} },
|
|
24
|
+
floorplanScope: 'building',
|
|
25
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
26
|
+
} as AnyNodeDefinition)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('selectable candidates', () => {
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
registerSelectableElevatorTestKind()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
useScene.setState({
|
|
36
|
+
nodes: {},
|
|
37
|
+
rootNodeIds: [],
|
|
38
|
+
dirtyNodes: new Set(),
|
|
39
|
+
collections: {},
|
|
40
|
+
} as never)
|
|
41
|
+
useViewer.setState({
|
|
42
|
+
selection: {
|
|
43
|
+
buildingId: 'building_test',
|
|
44
|
+
levelId: 'level_test',
|
|
45
|
+
zoneId: null,
|
|
46
|
+
selectedIds: [],
|
|
47
|
+
},
|
|
48
|
+
previewSelectedIds: [],
|
|
49
|
+
})
|
|
50
|
+
useEditor.setState({
|
|
51
|
+
phase: 'structure',
|
|
52
|
+
structureLayer: 'elements',
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('includes building-scoped elevators for the active level building', () => {
|
|
57
|
+
useScene.setState({
|
|
58
|
+
nodes: {
|
|
59
|
+
building_test: {
|
|
60
|
+
id: 'building_test',
|
|
61
|
+
type: 'building',
|
|
62
|
+
children: ['level_test', 'elevator_test'],
|
|
63
|
+
},
|
|
64
|
+
level_test: {
|
|
65
|
+
id: 'level_test',
|
|
66
|
+
type: 'level',
|
|
67
|
+
parentId: 'building_test',
|
|
68
|
+
children: [],
|
|
69
|
+
},
|
|
70
|
+
elevator_test: {
|
|
71
|
+
id: 'elevator_test',
|
|
72
|
+
type: 'elevator',
|
|
73
|
+
parentId: 'building_test',
|
|
74
|
+
position: [1, 0, 2],
|
|
75
|
+
rotation: 0,
|
|
76
|
+
},
|
|
77
|
+
} as unknown as Record<string, AnyNode>,
|
|
78
|
+
} as never)
|
|
79
|
+
|
|
80
|
+
expect(collectSelectableCandidateIds()).toContain('elevator_test')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('includes legacy level-parented elevators already loaded in the editor', () => {
|
|
84
|
+
useScene.setState({
|
|
85
|
+
nodes: {
|
|
86
|
+
building_test: {
|
|
87
|
+
id: 'building_test',
|
|
88
|
+
type: 'building',
|
|
89
|
+
children: ['level_test'],
|
|
90
|
+
},
|
|
91
|
+
level_test: {
|
|
92
|
+
id: 'level_test',
|
|
93
|
+
type: 'level',
|
|
94
|
+
parentId: 'building_test',
|
|
95
|
+
children: ['elevator_test'],
|
|
96
|
+
},
|
|
97
|
+
elevator_test: {
|
|
98
|
+
id: 'elevator_test',
|
|
99
|
+
type: 'elevator',
|
|
100
|
+
parentId: 'level_test',
|
|
101
|
+
position: [1, 0, 2],
|
|
102
|
+
rotation: 0,
|
|
103
|
+
},
|
|
104
|
+
} as unknown as Record<string, AnyNode>,
|
|
105
|
+
} as never)
|
|
106
|
+
|
|
107
|
+
expect(collectSelectableCandidateIds()).toContain('elevator_test')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
isRegistrySelectable,
|
|
5
|
+
type LevelNode,
|
|
6
|
+
nodeRegistry,
|
|
7
|
+
resolveBuildingForLevel,
|
|
8
|
+
resolveLevelId,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
12
|
+
import useEditor from '../../../store/use-editor'
|
|
13
|
+
|
|
14
|
+
function isVisibleSelectableNode(node: AnyNode): boolean {
|
|
15
|
+
if ((node as { visible?: boolean }).visible === false) return false
|
|
16
|
+
return isRegistrySelectable(node.type)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function collectSelectableCandidateIds(): string[] {
|
|
20
|
+
const { levelId } = useViewer.getState().selection
|
|
21
|
+
const { nodes } = useScene.getState()
|
|
22
|
+
const { phase, structureLayer } = useEditor.getState()
|
|
23
|
+
const result: string[] = []
|
|
24
|
+
const seen = new Set<string>()
|
|
25
|
+
const addNode = (node: AnyNode | undefined) => {
|
|
26
|
+
if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
|
|
27
|
+
seen.add(node.id)
|
|
28
|
+
result.push(node.id)
|
|
29
|
+
}
|
|
30
|
+
const visitLevelDescendant = (id: AnyNodeId) => {
|
|
31
|
+
const node = nodes[id]
|
|
32
|
+
if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
|
|
33
|
+
|
|
34
|
+
if (isRegistrySelectable(node.type)) {
|
|
35
|
+
addNode(node)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const children = 'children' in node && Array.isArray(node.children) ? node.children : []
|
|
39
|
+
for (const childId of children) {
|
|
40
|
+
visitLevelDescendant(childId as AnyNodeId)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (phase === 'site') {
|
|
45
|
+
for (const node of Object.values(nodes)) {
|
|
46
|
+
if (node.type === 'building') addNode(node)
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!levelId) return []
|
|
52
|
+
const levelNode = nodes[levelId as AnyNodeId] as LevelNode | undefined
|
|
53
|
+
if (!levelNode || levelNode.type !== 'level') return []
|
|
54
|
+
|
|
55
|
+
if (phase === 'structure' && structureLayer === 'zones') {
|
|
56
|
+
for (const childId of levelNode.children) {
|
|
57
|
+
const node = nodes[childId as AnyNodeId]
|
|
58
|
+
if (node?.type === 'zone') addNode(node)
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const childId of levelNode.children) {
|
|
64
|
+
visitLevelDescendant(childId as AnyNodeId)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const buildingId = resolveBuildingForLevel(levelId as AnyNodeId, nodes)
|
|
68
|
+
for (const node of Object.values(nodes)) {
|
|
69
|
+
if (!node || node.type === 'level' || !isVisibleSelectableNode(node)) continue
|
|
70
|
+
|
|
71
|
+
const def = nodeRegistry.get(node.type)
|
|
72
|
+
const isBuildingScoped = def?.floorplanScope === 'building'
|
|
73
|
+
const parentId = (node as { parentId?: AnyNodeId | null }).parentId
|
|
74
|
+
if (isBuildingScoped && buildingId && parentId === buildingId) {
|
|
75
|
+
addNode(node)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!isBuildingScoped && resolveLevelId(node, nodes) === levelId) {
|
|
80
|
+
addNode(node)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { nodeRegistry } from '@pascal-app/core'
|
|
2
|
+
import { type ComponentType, lazy } from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Phase 5 Stage D — runtime lazy-load of a kind's affordance tool.
|
|
6
|
+
*
|
|
7
|
+
* The editor can't statically import from `@pascal-app/nodes` (the
|
|
8
|
+
* nodes package depends on editor — static imports would cycle). The
|
|
9
|
+
* kind declares its drag-affordance components in
|
|
10
|
+
* `def.affordanceTools[<key>]: () => import('./<name>-tool')`; this
|
|
11
|
+
* helper resolves that to a `React.lazy` component at the call site.
|
|
12
|
+
*
|
|
13
|
+
* Returns null when the kind doesn't declare the affordance — callers
|
|
14
|
+
* mount the legacy fallback in that case.
|
|
15
|
+
*/
|
|
16
|
+
const lazyToolCache = new WeakMap<() => Promise<unknown>, ComponentType>()
|
|
17
|
+
|
|
18
|
+
export function getRegistryAffordanceTool(
|
|
19
|
+
kind: string,
|
|
20
|
+
affordance: string,
|
|
21
|
+
): ComponentType<any> | null {
|
|
22
|
+
const def = nodeRegistry.get(kind)
|
|
23
|
+
const loader = def?.affordanceTools?.[affordance]
|
|
24
|
+
if (!loader) return null
|
|
25
|
+
const cached = lazyToolCache.get(loader)
|
|
26
|
+
if (cached) return cached
|
|
27
|
+
const Comp = lazy(loader as () => Promise<{ default: ComponentType<any> }>)
|
|
28
|
+
lazyToolCache.set(loader, Comp as unknown as ComponentType)
|
|
29
|
+
return Comp as unknown as ComponentType<any>
|
|
30
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { sceneRegistry } from '@pascal-app/core'
|
|
4
|
+
import { useEffect, useMemo } from 'react'
|
|
5
|
+
import {
|
|
6
|
+
Box3,
|
|
7
|
+
BoxGeometry,
|
|
8
|
+
EdgesGeometry,
|
|
9
|
+
Matrix4,
|
|
10
|
+
type Mesh,
|
|
11
|
+
type Object3D,
|
|
12
|
+
PlaneGeometry,
|
|
13
|
+
Vector3,
|
|
14
|
+
} from 'three'
|
|
15
|
+
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
16
|
+
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
17
|
+
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
18
|
+
|
|
19
|
+
const NO_RAYCAST = () => null
|
|
20
|
+
|
|
21
|
+
/** green-500 — matches the item placement box's "placeable" state. */
|
|
22
|
+
const DEFAULT_COLOR = 0x22_c5_5e
|
|
23
|
+
|
|
24
|
+
type LocalBounds = { size: [number, number, number]; center: [number, number, number] }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The node's bounding box in its OWN, unrotated frame — measured from the
|
|
28
|
+
* rendered geometry so it captures the full extent (base, cap, overhang),
|
|
29
|
+
* not just the declared width/height/depth. World position + rotation are
|
|
30
|
+
* cancelled out (invert the root's world matrix), so the result is stable
|
|
31
|
+
* regardless of where the live drag has moved the node; the caller re-applies
|
|
32
|
+
* the current Y rotation on the group. Returns null when nothing measurable.
|
|
33
|
+
*/
|
|
34
|
+
function measureLocalBounds(obj: Object3D): LocalBounds | null {
|
|
35
|
+
obj.updateWorldMatrix(true, true)
|
|
36
|
+
const inverseRoot = new Matrix4().copy(obj.matrixWorld).invert()
|
|
37
|
+
const box = new Box3()
|
|
38
|
+
const meshBox = new Box3()
|
|
39
|
+
const toLocal = new Matrix4()
|
|
40
|
+
let measured = false
|
|
41
|
+
obj.traverse((child) => {
|
|
42
|
+
const mesh = child as Mesh
|
|
43
|
+
if (!mesh.isMesh || !mesh.geometry) return
|
|
44
|
+
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox()
|
|
45
|
+
const bb = mesh.geometry.boundingBox
|
|
46
|
+
if (!bb) return
|
|
47
|
+
toLocal.copy(inverseRoot).multiply(mesh.matrixWorld)
|
|
48
|
+
meshBox.copy(bb).applyMatrix4(toLocal)
|
|
49
|
+
box.union(meshBox)
|
|
50
|
+
measured = true
|
|
51
|
+
})
|
|
52
|
+
if (!measured || box.isEmpty()) return null
|
|
53
|
+
const size = box.getSize(new Vector3())
|
|
54
|
+
const center = box.getCenter(new Vector3())
|
|
55
|
+
return { size: [size.x, size.y, size.z], center: [center.x, center.y, center.z] }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface DragBoundingBoxProps {
|
|
59
|
+
/** Node whose rendered geometry is measured for the box extents. */
|
|
60
|
+
nodeId: string
|
|
61
|
+
/** Footprint origin on the floor, `[x, 0, z]` — the node's live position. */
|
|
62
|
+
position: [number, number, number]
|
|
63
|
+
/** Y rotation (radians) applied to the box, matching the dragged node. */
|
|
64
|
+
rotationY?: number
|
|
65
|
+
/** Declared `[width, height, depth]`, used until/if the mesh can't be measured. */
|
|
66
|
+
fallbackSize?: [number, number, number]
|
|
67
|
+
/**
|
|
68
|
+
* Hard override for the box extents — wins over both mesh measurement and
|
|
69
|
+
* `fallbackSize`. Use when the rendered mesh contains extras the user
|
|
70
|
+
* wouldn't read as "the thing being dragged" (e.g. an elevator whose mesh
|
|
71
|
+
* includes per-level landings outside the shaft footprint).
|
|
72
|
+
*/
|
|
73
|
+
size?: [number, number, number]
|
|
74
|
+
/** Y center of the box in the node's local frame. Defaults to `size[1] / 2`. */
|
|
75
|
+
centerY?: number
|
|
76
|
+
color?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Footprint box drawn around a node while it is being dragged — the same
|
|
81
|
+
* affordance items get during placement: a wireframe cube spanning the node's
|
|
82
|
+
* full measured extent plus a ground plane with a radial opacity gradient
|
|
83
|
+
* (transparent in the centre, opaque toward the edges). Overlay layer +
|
|
84
|
+
* `depthTest: false` keep it drawn on top of scene geometry throughout the
|
|
85
|
+
* drag, and the box visualises the bounds that drive alignment snapping.
|
|
86
|
+
*/
|
|
87
|
+
export function DragBoundingBox({
|
|
88
|
+
nodeId,
|
|
89
|
+
position,
|
|
90
|
+
rotationY = 0,
|
|
91
|
+
fallbackSize = [0, 0, 0],
|
|
92
|
+
size,
|
|
93
|
+
centerY,
|
|
94
|
+
color = DEFAULT_COLOR,
|
|
95
|
+
}: DragBoundingBoxProps) {
|
|
96
|
+
const measured = useMemo(() => {
|
|
97
|
+
if (size) return null
|
|
98
|
+
const obj = sceneRegistry.nodes.get(nodeId)
|
|
99
|
+
return obj ? measureLocalBounds(obj) : null
|
|
100
|
+
}, [nodeId, size])
|
|
101
|
+
|
|
102
|
+
const [w, h, d] = size ?? measured?.size ?? fallbackSize
|
|
103
|
+
const [cx, cy, cz] = size
|
|
104
|
+
? [0, centerY ?? size[1] / 2, 0]
|
|
105
|
+
: (measured?.center ?? [0, fallbackSize[1] / 2, 0])
|
|
106
|
+
const minY = cy - h / 2
|
|
107
|
+
|
|
108
|
+
const edgeGeometry = useMemo(() => {
|
|
109
|
+
const box = new BoxGeometry(w, h, d)
|
|
110
|
+
const edges = new EdgesGeometry(box)
|
|
111
|
+
box.dispose()
|
|
112
|
+
return edges
|
|
113
|
+
}, [w, h, d])
|
|
114
|
+
|
|
115
|
+
// Flat on the ground (XZ) at the box's base, nudged up 0.01m to avoid
|
|
116
|
+
// z-fighting with slabs.
|
|
117
|
+
const planeGeometry = useMemo(() => {
|
|
118
|
+
const plane = new PlaneGeometry(w, d)
|
|
119
|
+
plane.rotateX(-Math.PI / 2)
|
|
120
|
+
plane.translate(cx, minY + 0.01, cz)
|
|
121
|
+
return plane
|
|
122
|
+
}, [w, d, cx, minY, cz])
|
|
123
|
+
|
|
124
|
+
const edgeMaterial = useMemo(
|
|
125
|
+
() => new LineBasicNodeMaterial({ color, linewidth: 3, depthTest: false, depthWrite: false }),
|
|
126
|
+
[color],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const planeMaterial = useMemo(() => {
|
|
130
|
+
const material = new MeshBasicNodeMaterial({
|
|
131
|
+
color,
|
|
132
|
+
transparent: true,
|
|
133
|
+
depthTest: false,
|
|
134
|
+
depthWrite: false,
|
|
135
|
+
})
|
|
136
|
+
material.opacityNode = smoothstep(0, 0.7, distance(uv(), vec2(0.5, 0.5))).mul(0.6)
|
|
137
|
+
return material
|
|
138
|
+
}, [color])
|
|
139
|
+
|
|
140
|
+
useEffect(
|
|
141
|
+
() => () => {
|
|
142
|
+
edgeGeometry.dispose()
|
|
143
|
+
planeGeometry.dispose()
|
|
144
|
+
edgeMaterial.dispose()
|
|
145
|
+
planeMaterial.dispose()
|
|
146
|
+
},
|
|
147
|
+
[edgeGeometry, planeGeometry, edgeMaterial, planeMaterial],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if (w <= 0 || h <= 0 || d <= 0) return null
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<group position={position} rotation={[0, rotationY, 0]}>
|
|
154
|
+
<mesh
|
|
155
|
+
geometry={planeGeometry}
|
|
156
|
+
layers={EDITOR_LAYER}
|
|
157
|
+
material={planeMaterial}
|
|
158
|
+
raycast={NO_RAYCAST}
|
|
159
|
+
renderOrder={999}
|
|
160
|
+
/>
|
|
161
|
+
<lineSegments
|
|
162
|
+
geometry={edgeGeometry}
|
|
163
|
+
layers={EDITOR_LAYER}
|
|
164
|
+
material={edgeMaterial}
|
|
165
|
+
position={[cx, cy, cz]}
|
|
166
|
+
raycast={NO_RAYCAST}
|
|
167
|
+
renderOrder={999}
|
|
168
|
+
/>
|
|
169
|
+
</group>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type AnyNode, type AnyNodeId, getFloorStackedPosition, useScene } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
type FloorStackPreviewArgs = {
|
|
4
|
+
node: AnyNode
|
|
5
|
+
position: [number, number, number]
|
|
6
|
+
rotation?: unknown
|
|
7
|
+
levelId?: string | null
|
|
8
|
+
nodes?: Record<AnyNodeId, AnyNode>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getFloorStackPreviewPosition({
|
|
12
|
+
node,
|
|
13
|
+
position,
|
|
14
|
+
rotation,
|
|
15
|
+
levelId,
|
|
16
|
+
nodes,
|
|
17
|
+
}: FloorStackPreviewArgs): [number, number, number] {
|
|
18
|
+
return getFloorStackedPosition({
|
|
19
|
+
node,
|
|
20
|
+
nodes: nodes ?? useScene.getState().nodes,
|
|
21
|
+
position,
|
|
22
|
+
rotation,
|
|
23
|
+
levelId,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNode, type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useCallback, useRef, useState } from 'react'
|
|
5
|
+
import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
|
|
6
|
+
import useEditor from '../../../store/use-editor'
|
|
7
|
+
|
|
8
|
+
type FreshPlacementNode = Pick<AnyNode, 'id' | 'metadata'>
|
|
9
|
+
|
|
10
|
+
type FreshPlacementVisibilityArgs = {
|
|
11
|
+
node: FreshPlacementNode
|
|
12
|
+
enabled?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useFreshPlacementVisibility({
|
|
16
|
+
node,
|
|
17
|
+
enabled = true,
|
|
18
|
+
}: FreshPlacementVisibilityArgs) {
|
|
19
|
+
const isFreshPlacement = enabled && isFreshPlacementMetadata(node.metadata)
|
|
20
|
+
const useAbsoluteCursorPlacement = isFreshPlacement && !useEditor.getState().placementDragMode
|
|
21
|
+
const shouldStartHidden = useAbsoluteCursorPlacement
|
|
22
|
+
|
|
23
|
+
const [visibility, setVisibility] = useState(() => ({
|
|
24
|
+
nodeId: node.id,
|
|
25
|
+
visible: !shouldStartHidden,
|
|
26
|
+
}))
|
|
27
|
+
const visibilityRef = useRef(visibility)
|
|
28
|
+
const previewVisible = visibility.nodeId === node.id ? visibility.visible : !shouldStartHidden
|
|
29
|
+
|
|
30
|
+
const setPreviewVisibleForNode = useCallback(
|
|
31
|
+
(visible: boolean) => {
|
|
32
|
+
const current = visibilityRef.current
|
|
33
|
+
if (current.nodeId === node.id && current.visible === visible) return
|
|
34
|
+
const next = { nodeId: node.id, visible }
|
|
35
|
+
visibilityRef.current = next
|
|
36
|
+
setVisibility(next)
|
|
37
|
+
},
|
|
38
|
+
[node.id],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const revealFreshPlacement = useCallback(() => {
|
|
42
|
+
if (!isFreshPlacement) return
|
|
43
|
+
setPreviewVisibleForNode(true)
|
|
44
|
+
|
|
45
|
+
sceneRegistry.nodes.get(node.id)?.traverse((child) => {
|
|
46
|
+
child.visible = true
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const liveNode = useScene.getState().nodes[node.id as AnyNodeId]
|
|
50
|
+
if (liveNode?.visible === false) {
|
|
51
|
+
useScene.getState().updateNode(node.id as AnyNodeId, { visible: true } as Partial<AnyNode>)
|
|
52
|
+
}
|
|
53
|
+
}, [isFreshPlacement, node.id, setPreviewVisibleForNode])
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
isFreshPlacement,
|
|
57
|
+
previewVisible,
|
|
58
|
+
revealFreshPlacement,
|
|
59
|
+
useAbsoluteCursorPlacement,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BufferGeometry, Float32BufferAttribute, type LineSegments } from 'three'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Axis-aligned box description shared by every placement-cursor wireframe.
|
|
5
|
+
* `dimensions` is the box extent on each axis; `center` is its centre in the
|
|
6
|
+
* cursor group's local space (so an off-centre mesh bbox stays off-centre).
|
|
7
|
+
* `min`/`max` are kept for callers that need the explicit corners.
|
|
8
|
+
*/
|
|
9
|
+
export type PreviewBounds = {
|
|
10
|
+
min: [number, number, number]
|
|
11
|
+
max: [number, number, number]
|
|
12
|
+
dimensions: [number, number, number]
|
|
13
|
+
center: [number, number, number]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createLineGeometry(points: number[] = [0, 0, 0, 0, 0, 0]): BufferGeometry {
|
|
17
|
+
const geometry = new BufferGeometry()
|
|
18
|
+
geometry.setAttribute('position', new Float32BufferAttribute(points, 3))
|
|
19
|
+
return geometry
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Flatten a box's 12 edges into a `LineSegments` position array. */
|
|
23
|
+
export function getBoxEdgePoints(bounds: PreviewBounds): number[] {
|
|
24
|
+
const [width, height, depth] = bounds.dimensions
|
|
25
|
+
const [centerX, centerY, centerZ] = bounds.center
|
|
26
|
+
const minX = centerX - width / 2
|
|
27
|
+
const maxX = centerX + width / 2
|
|
28
|
+
const minY = centerY - height / 2
|
|
29
|
+
const maxY = centerY + height / 2
|
|
30
|
+
const minZ = centerZ - depth / 2
|
|
31
|
+
const maxZ = centerZ + depth / 2
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
minX,
|
|
35
|
+
minY,
|
|
36
|
+
minZ,
|
|
37
|
+
maxX,
|
|
38
|
+
minY,
|
|
39
|
+
minZ,
|
|
40
|
+
maxX,
|
|
41
|
+
minY,
|
|
42
|
+
minZ,
|
|
43
|
+
maxX,
|
|
44
|
+
minY,
|
|
45
|
+
maxZ,
|
|
46
|
+
maxX,
|
|
47
|
+
minY,
|
|
48
|
+
maxZ,
|
|
49
|
+
minX,
|
|
50
|
+
minY,
|
|
51
|
+
maxZ,
|
|
52
|
+
minX,
|
|
53
|
+
minY,
|
|
54
|
+
maxZ,
|
|
55
|
+
minX,
|
|
56
|
+
minY,
|
|
57
|
+
minZ,
|
|
58
|
+
|
|
59
|
+
minX,
|
|
60
|
+
maxY,
|
|
61
|
+
minZ,
|
|
62
|
+
maxX,
|
|
63
|
+
maxY,
|
|
64
|
+
minZ,
|
|
65
|
+
maxX,
|
|
66
|
+
maxY,
|
|
67
|
+
minZ,
|
|
68
|
+
maxX,
|
|
69
|
+
maxY,
|
|
70
|
+
maxZ,
|
|
71
|
+
maxX,
|
|
72
|
+
maxY,
|
|
73
|
+
maxZ,
|
|
74
|
+
minX,
|
|
75
|
+
maxY,
|
|
76
|
+
maxZ,
|
|
77
|
+
minX,
|
|
78
|
+
maxY,
|
|
79
|
+
maxZ,
|
|
80
|
+
minX,
|
|
81
|
+
maxY,
|
|
82
|
+
minZ,
|
|
83
|
+
|
|
84
|
+
minX,
|
|
85
|
+
minY,
|
|
86
|
+
minZ,
|
|
87
|
+
minX,
|
|
88
|
+
maxY,
|
|
89
|
+
minZ,
|
|
90
|
+
maxX,
|
|
91
|
+
minY,
|
|
92
|
+
minZ,
|
|
93
|
+
maxX,
|
|
94
|
+
maxY,
|
|
95
|
+
minZ,
|
|
96
|
+
maxX,
|
|
97
|
+
minY,
|
|
98
|
+
maxZ,
|
|
99
|
+
maxX,
|
|
100
|
+
maxY,
|
|
101
|
+
maxZ,
|
|
102
|
+
minX,
|
|
103
|
+
minY,
|
|
104
|
+
maxZ,
|
|
105
|
+
minX,
|
|
106
|
+
maxY,
|
|
107
|
+
maxZ,
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function updateLineGeometry(ref: React.RefObject<LineSegments>, points: number[]) {
|
|
112
|
+
const geometry = ref.current?.geometry
|
|
113
|
+
if (!geometry) return
|
|
114
|
+
|
|
115
|
+
const attribute = geometry.getAttribute('position') as Float32BufferAttribute | undefined
|
|
116
|
+
if (!attribute || attribute.array.length !== points.length) {
|
|
117
|
+
geometry.setAttribute('position', new Float32BufferAttribute(points, 3))
|
|
118
|
+
} else {
|
|
119
|
+
attribute.set(points)
|
|
120
|
+
attribute.needsUpdate = true
|
|
121
|
+
}
|
|
122
|
+
geometry.computeBoundingSphere()
|
|
123
|
+
}
|