@pascal-app/editor 0.8.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- 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 +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- 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 +5829 -12005
- 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 +178 -149
- 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 +433 -156
- 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 +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- 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-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- 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 +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- 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 +1 -1
- 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 +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- 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 -538
- 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 +56 -0
- 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 +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- 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 +4 -6
- 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 +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -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 +10 -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 +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- 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.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- 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 +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- 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 +320 -37
- 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/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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 -264
- 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 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- 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 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- 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 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- 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 -280
- 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 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
|
+
collectAlignmentAnchors,
|
|
4
|
+
createSurfaceOpeningPreviewController,
|
|
5
|
+
type EventSuffix,
|
|
3
6
|
emitter,
|
|
4
7
|
type GridEvent,
|
|
5
8
|
type LevelNode,
|
|
9
|
+
movingAlignmentAnchors,
|
|
10
|
+
type NodeEvent,
|
|
11
|
+
resolveAlignment,
|
|
6
12
|
StairNode,
|
|
7
13
|
StairSegmentNode,
|
|
14
|
+
syncAutoStairOpenings,
|
|
8
15
|
useScene,
|
|
9
16
|
} from '@pascal-app/core'
|
|
17
|
+
import { useAlignmentGuides } from '@pascal-app/editor'
|
|
10
18
|
import { useViewer } from '@pascal-app/viewer'
|
|
11
19
|
import { useEffect, useMemo, useRef } from 'react'
|
|
12
20
|
import * as THREE from 'three'
|
|
13
21
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
22
|
+
import {
|
|
23
|
+
resolveStairDestinationLevel,
|
|
24
|
+
resolveStairPlacementLevelId,
|
|
25
|
+
} from '../../../lib/stair-levels'
|
|
14
26
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
27
|
+
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
15
28
|
import {
|
|
16
29
|
DEFAULT_CURVED_STAIR_INNER_RADIUS,
|
|
17
30
|
DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
|
|
@@ -32,6 +45,23 @@ import {
|
|
|
32
45
|
} from './stair-defaults'
|
|
33
46
|
|
|
34
47
|
const GRID_OFFSET = 0.02
|
|
48
|
+
/** Figma-style alignment-snap threshold (meters), matching the move tools. */
|
|
49
|
+
const ALIGNMENT_THRESHOLD_M = 0.08
|
|
50
|
+
type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
|
|
51
|
+
|
|
52
|
+
const CLICK_TRIGGER_KINDS = [
|
|
53
|
+
'shelf',
|
|
54
|
+
'item',
|
|
55
|
+
'slab',
|
|
56
|
+
'ceiling',
|
|
57
|
+
'wall',
|
|
58
|
+
'fence',
|
|
59
|
+
'column',
|
|
60
|
+
'roof',
|
|
61
|
+
'roof-segment',
|
|
62
|
+
'stair',
|
|
63
|
+
'stair-segment',
|
|
64
|
+
] as const
|
|
35
65
|
|
|
36
66
|
/**
|
|
37
67
|
* Generates the step-profile geometry for the ghost preview.
|
|
@@ -69,19 +99,10 @@ function createStairPreviewGeometry(): THREE.BufferGeometry {
|
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
/**
|
|
72
|
-
* Creates a
|
|
102
|
+
* Creates a default straight stair segment.
|
|
73
103
|
*/
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
position: [number, number, number],
|
|
77
|
-
rotation: number,
|
|
78
|
-
): void {
|
|
79
|
-
const { createNodes, nodes } = useScene.getState()
|
|
80
|
-
|
|
81
|
-
const stairCount = Object.values(nodes).filter((n) => n.type === 'stair').length
|
|
82
|
-
const name = `Staircase ${stairCount + 1}`
|
|
83
|
-
|
|
84
|
-
const segment = StairSegmentNode.parse({
|
|
104
|
+
function createDefaultStairSegment() {
|
|
105
|
+
return StairSegmentNode.parse({
|
|
85
106
|
segmentType: 'stair',
|
|
86
107
|
width: DEFAULT_STAIR_WIDTH,
|
|
87
108
|
length: DEFAULT_STAIR_LENGTH,
|
|
@@ -92,14 +113,24 @@ function commitStairPlacement(
|
|
|
92
113
|
thickness: DEFAULT_STAIR_THICKNESS,
|
|
93
114
|
position: [0, 0, 0],
|
|
94
115
|
})
|
|
116
|
+
}
|
|
95
117
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
function createDefaultStairNode({
|
|
119
|
+
name,
|
|
120
|
+
levelId,
|
|
121
|
+
nextLevelId,
|
|
122
|
+
position,
|
|
123
|
+
rotation,
|
|
124
|
+
segmentId,
|
|
125
|
+
}: {
|
|
126
|
+
name: string
|
|
127
|
+
levelId: LevelNode['id']
|
|
128
|
+
nextLevelId: LevelNode['id']
|
|
129
|
+
position: [number, number, number]
|
|
130
|
+
rotation: number
|
|
131
|
+
segmentId: StairSegmentNode['id']
|
|
132
|
+
}) {
|
|
133
|
+
return StairNode.parse({
|
|
103
134
|
name,
|
|
104
135
|
position,
|
|
105
136
|
rotation,
|
|
@@ -121,11 +152,55 @@ function commitStairPlacement(
|
|
|
121
152
|
showStepSupports: DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS,
|
|
122
153
|
railingHeight: DEFAULT_STAIR_RAILING_HEIGHT,
|
|
123
154
|
railingMode: DEFAULT_STAIR_RAILING_MODE,
|
|
124
|
-
children: [
|
|
155
|
+
children: [segmentId],
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Creates a stair group with one default stair segment at the given position/rotation.
|
|
161
|
+
*/
|
|
162
|
+
function commitStairPlacement(
|
|
163
|
+
levelId: LevelNode['id'],
|
|
164
|
+
position: [number, number, number],
|
|
165
|
+
rotation: number,
|
|
166
|
+
): void {
|
|
167
|
+
const { createNodes, nodes } = useScene.getState()
|
|
168
|
+
const placementLevelId = resolveStairPlacementLevelId(
|
|
169
|
+
nodes,
|
|
170
|
+
levelId,
|
|
171
|
+
useViewer.getState().selection.buildingId,
|
|
172
|
+
)
|
|
173
|
+
if (!placementLevelId) return
|
|
174
|
+
|
|
175
|
+
const stairCount = Object.values(nodes).filter((n) => n.type === 'stair').length
|
|
176
|
+
const name = `Staircase ${stairCount + 1}`
|
|
177
|
+
const segment = createDefaultStairSegment()
|
|
178
|
+
|
|
179
|
+
const destinationPlan = resolveStairDestinationLevel({
|
|
180
|
+
createMissing: true,
|
|
181
|
+
fromLevelId: placementLevelId,
|
|
182
|
+
nodes,
|
|
183
|
+
})
|
|
184
|
+
const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
|
|
185
|
+
|
|
186
|
+
const stair = createDefaultStairNode({
|
|
187
|
+
name,
|
|
188
|
+
levelId: placementLevelId,
|
|
189
|
+
nextLevelId,
|
|
190
|
+
position,
|
|
191
|
+
rotation,
|
|
192
|
+
segmentId: segment.id,
|
|
125
193
|
})
|
|
126
194
|
|
|
195
|
+
const createdLevel = destinationPlan?.createdLevel
|
|
196
|
+
const levelCreateOps =
|
|
197
|
+
createdLevel && destinationPlan.buildingId
|
|
198
|
+
? [{ node: createdLevel, parentId: destinationPlan.buildingId }]
|
|
199
|
+
: []
|
|
200
|
+
|
|
127
201
|
createNodes([
|
|
128
|
-
|
|
202
|
+
...levelCreateOps,
|
|
203
|
+
{ node: stair, parentId: placementLevelId },
|
|
129
204
|
{ node: segment, parentId: stair.id },
|
|
130
205
|
])
|
|
131
206
|
|
|
@@ -137,6 +212,7 @@ export const StairTool: React.FC = () => {
|
|
|
137
212
|
const previewRef = useRef<THREE.Group>(null)
|
|
138
213
|
const rotationRef = useRef(0)
|
|
139
214
|
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
215
|
+
const lastCanonicalPositionRef = useRef<[number, number, number] | null>(null)
|
|
140
216
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
141
217
|
|
|
142
218
|
const previewGeometry = useMemo(() => createStairPreviewGeometry(), [])
|
|
@@ -144,22 +220,144 @@ export const StairTool: React.FC = () => {
|
|
|
144
220
|
useEffect(() => {
|
|
145
221
|
if (!currentLevelId) return
|
|
146
222
|
|
|
223
|
+
const openingPreview = createSurfaceOpeningPreviewController()
|
|
224
|
+
|
|
147
225
|
// Reset rotation when tool activates
|
|
148
226
|
rotationRef.current = 0
|
|
149
227
|
if (previewRef.current) previewRef.current.rotation.y = 0
|
|
228
|
+
lastCanonicalPositionRef.current = null
|
|
229
|
+
|
|
230
|
+
const buildPreviewScene = (position: [number, number, number], rotation: number) => {
|
|
231
|
+
const nodes = useScene.getState().nodes
|
|
232
|
+
const placementLevelId = resolveStairPlacementLevelId(
|
|
233
|
+
nodes,
|
|
234
|
+
currentLevelId,
|
|
235
|
+
useViewer.getState().selection.buildingId,
|
|
236
|
+
)
|
|
237
|
+
if (!placementLevelId) return null
|
|
238
|
+
|
|
239
|
+
const destinationPlan = resolveStairDestinationLevel({
|
|
240
|
+
createMissing: true,
|
|
241
|
+
fromLevelId: placementLevelId,
|
|
242
|
+
nodes,
|
|
243
|
+
})
|
|
244
|
+
const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
|
|
245
|
+
const segment = createDefaultStairSegment()
|
|
246
|
+
const stair = createDefaultStairNode({
|
|
247
|
+
name: 'Staircase Preview',
|
|
248
|
+
levelId: placementLevelId,
|
|
249
|
+
nextLevelId,
|
|
250
|
+
position,
|
|
251
|
+
rotation,
|
|
252
|
+
segmentId: segment.id,
|
|
253
|
+
})
|
|
254
|
+
const previewNodes = {
|
|
255
|
+
...nodes,
|
|
256
|
+
...(destinationPlan?.createdLevel
|
|
257
|
+
? { [destinationPlan.createdLevel.id]: destinationPlan.createdLevel }
|
|
258
|
+
: {}),
|
|
259
|
+
[stair.id]: { ...stair, parentId: placementLevelId },
|
|
260
|
+
[segment.id]: { ...segment, parentId: stair.id },
|
|
261
|
+
} as Record<string, AnyNode>
|
|
262
|
+
|
|
263
|
+
return { placementLevelId, previewNodes, stair }
|
|
264
|
+
}
|
|
150
265
|
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
266
|
+
const applyDraftPreview = (position: [number, number, number], rotation: number) => {
|
|
267
|
+
const preview = buildPreviewScene(position, rotation)
|
|
268
|
+
const visualPosition = preview
|
|
269
|
+
? getFloorStackPreviewPosition({
|
|
270
|
+
node: preview.stair,
|
|
271
|
+
position,
|
|
272
|
+
rotation,
|
|
273
|
+
levelId: preview.placementLevelId,
|
|
274
|
+
nodes: preview.previewNodes,
|
|
275
|
+
})
|
|
276
|
+
: position
|
|
156
277
|
if (cursorRef.current) {
|
|
157
|
-
cursorRef.current.position.set(
|
|
278
|
+
cursorRef.current.position.set(
|
|
279
|
+
visualPosition[0],
|
|
280
|
+
visualPosition[1] + GRID_OFFSET,
|
|
281
|
+
visualPosition[2],
|
|
282
|
+
)
|
|
158
283
|
}
|
|
159
284
|
|
|
160
285
|
if (previewRef.current) {
|
|
161
|
-
previewRef.current.position.set(
|
|
286
|
+
previewRef.current.position.set(...visualPosition)
|
|
287
|
+
previewRef.current.rotation.y = rotation
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!preview) {
|
|
291
|
+
openingPreview.clear()
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
openingPreview.apply(syncAutoStairOpenings(preview.previewNodes))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Alignment candidates — anchors of every alignable object; refreshed
|
|
299
|
+
// after each placement. The moving stair aligns by its footprint edges so
|
|
300
|
+
// users can snap the run side against walls, slabs, elevators, or another
|
|
301
|
+
// stair instead of only lining up the invisible origin point.
|
|
302
|
+
let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
|
|
303
|
+
const resolveStairFootprintAlignment = (
|
|
304
|
+
x: number,
|
|
305
|
+
z: number,
|
|
306
|
+
rotation: number,
|
|
307
|
+
): ReturnType<typeof resolveAlignment> | null => {
|
|
308
|
+
const preview = buildPreviewScene([x, 0, z], rotation)
|
|
309
|
+
const moving = preview
|
|
310
|
+
? movingAlignmentAnchors(preview.stair, preview.previewNodes, x, z, rotation)
|
|
311
|
+
: []
|
|
312
|
+
if (moving.length === 0) return null
|
|
313
|
+
return resolveAlignment({
|
|
314
|
+
moving,
|
|
315
|
+
candidates: alignmentCandidates,
|
|
316
|
+
threshold: ALIGNMENT_THRESHOLD_M,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
// The probe is the RAW cursor, not the grid-snapped point: resolving
|
|
320
|
+
// against the grid point would only catch anchors that happen to sit near
|
|
321
|
+
// a grid line. Matched axes use the raw probe + snap delta; unmatched axes
|
|
322
|
+
// keep the normal grid snap. Alt bypasses.
|
|
323
|
+
const alignPoint = (
|
|
324
|
+
gridX: number,
|
|
325
|
+
gridZ: number,
|
|
326
|
+
rawX: number,
|
|
327
|
+
rawZ: number,
|
|
328
|
+
bypass: boolean,
|
|
329
|
+
): [number, number] => {
|
|
330
|
+
if (bypass || alignmentCandidates.length === 0) {
|
|
331
|
+
useAlignmentGuides.getState().clear()
|
|
332
|
+
return [gridX, gridZ]
|
|
162
333
|
}
|
|
334
|
+
const ar = resolveStairFootprintAlignment(rawX, rawZ, rotationRef.current)
|
|
335
|
+
if (!ar || ar.guides.length === 0) {
|
|
336
|
+
useAlignmentGuides.getState().clear()
|
|
337
|
+
return [gridX, gridZ]
|
|
338
|
+
}
|
|
339
|
+
let x = gridX
|
|
340
|
+
let z = gridZ
|
|
341
|
+
if (ar.snap) {
|
|
342
|
+
if (ar.guides.some((guide) => guide.axis === 'x')) x = rawX + ar.snap.dx
|
|
343
|
+
if (ar.guides.some((guide) => guide.axis === 'z')) z = rawZ + ar.snap.dz
|
|
344
|
+
}
|
|
345
|
+
const finalAlignment = resolveStairFootprintAlignment(x, z, rotationRef.current)
|
|
346
|
+
useAlignmentGuides.getState().set(finalAlignment?.guides ?? ar.guides)
|
|
347
|
+
return [x, z]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const onGridMove = (event: GridEvent) => {
|
|
351
|
+
const [gridX, gridZ] = alignPoint(
|
|
352
|
+
Math.round(event.localPosition[0] * 2) / 2,
|
|
353
|
+
Math.round(event.localPosition[2] * 2) / 2,
|
|
354
|
+
event.localPosition[0],
|
|
355
|
+
event.localPosition[2],
|
|
356
|
+
event.nativeEvent?.altKey === true,
|
|
357
|
+
)
|
|
358
|
+
const position: [number, number, number] = [gridX, 0, gridZ]
|
|
359
|
+
lastCanonicalPositionRef.current = position
|
|
360
|
+
applyDraftPreview(position, rotationRef.current)
|
|
163
361
|
|
|
164
362
|
if (
|
|
165
363
|
previousGridPosRef.current &&
|
|
@@ -171,12 +369,34 @@ export const StairTool: React.FC = () => {
|
|
|
171
369
|
previousGridPosRef.current = [gridX, gridZ]
|
|
172
370
|
}
|
|
173
371
|
|
|
174
|
-
const
|
|
372
|
+
const getAlignedGridPosition = (event: GridEvent): [number, number, number] => {
|
|
373
|
+
const [gridX, gridZ] = alignPoint(
|
|
374
|
+
Math.round(event.localPosition[0] * 2) / 2,
|
|
375
|
+
Math.round(event.localPosition[2] * 2) / 2,
|
|
376
|
+
event.localPosition[0],
|
|
377
|
+
event.localPosition[2],
|
|
378
|
+
event.nativeEvent?.altKey === true,
|
|
379
|
+
)
|
|
380
|
+
return [gridX, 0, gridZ]
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const commitAtCursor = (event: ClickTriggerEvent) => {
|
|
175
384
|
if (!currentLevelId) return
|
|
385
|
+
const nodeEvent = 'node' in event ? (event as NodeEvent<AnyNode>) : null
|
|
386
|
+
if (nodeEvent) {
|
|
387
|
+
nodeEvent.stopPropagation()
|
|
388
|
+
nodeEvent.nativeEvent.stopPropagation()
|
|
389
|
+
}
|
|
176
390
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
391
|
+
const position = nodeEvent
|
|
392
|
+
? lastCanonicalPositionRef.current
|
|
393
|
+
: getAlignedGridPosition(event as GridEvent)
|
|
394
|
+
if (!position) return
|
|
395
|
+
|
|
396
|
+
commitStairPlacement(currentLevelId, position, rotationRef.current)
|
|
397
|
+
openingPreview.clear()
|
|
398
|
+
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
|
|
399
|
+
useAlignmentGuides.getState().clear()
|
|
180
400
|
}
|
|
181
401
|
|
|
182
402
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
@@ -193,20 +413,34 @@ export const StairTool: React.FC = () => {
|
|
|
193
413
|
event.preventDefault()
|
|
194
414
|
sfxEmitter.emit('sfx:item-rotate')
|
|
195
415
|
rotationRef.current += rotationDelta
|
|
196
|
-
if (
|
|
416
|
+
if (lastCanonicalPositionRef.current) {
|
|
417
|
+
applyDraftPreview(lastCanonicalPositionRef.current, rotationRef.current)
|
|
418
|
+
} else if (previewRef.current) {
|
|
197
419
|
previewRef.current.rotation.y = rotationRef.current
|
|
198
420
|
}
|
|
199
421
|
}
|
|
200
422
|
}
|
|
201
423
|
|
|
202
424
|
emitter.on('grid:move', onGridMove)
|
|
203
|
-
emitter.on('grid:click',
|
|
425
|
+
emitter.on('grid:click', commitAtCursor)
|
|
426
|
+
type SuffixedKey<K extends string> = `${K}:${EventSuffix}`
|
|
427
|
+
type ClickKey = SuffixedKey<(typeof CLICK_TRIGGER_KINDS)[number]>
|
|
428
|
+
for (const kind of CLICK_TRIGGER_KINDS) {
|
|
429
|
+
const key = `${kind}:click` as ClickKey
|
|
430
|
+
emitter.on(key, commitAtCursor as never)
|
|
431
|
+
}
|
|
204
432
|
window.addEventListener('keydown', onKeyDown)
|
|
205
433
|
|
|
206
434
|
return () => {
|
|
207
435
|
emitter.off('grid:move', onGridMove)
|
|
208
|
-
emitter.off('grid:click',
|
|
436
|
+
emitter.off('grid:click', commitAtCursor)
|
|
437
|
+
for (const kind of CLICK_TRIGGER_KINDS) {
|
|
438
|
+
const key = `${kind}:click` as ClickKey
|
|
439
|
+
emitter.off(key, commitAtCursor as never)
|
|
440
|
+
}
|
|
209
441
|
window.removeEventListener('keydown', onKeyDown)
|
|
442
|
+
useAlignmentGuides.getState().clear()
|
|
443
|
+
openingPreview.clear()
|
|
210
444
|
}
|
|
211
445
|
}, [currentLevelId])
|
|
212
446
|
|