@pascal-app/editor 0.8.0 → 0.9.2
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 +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- 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-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -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-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -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/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -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 +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- 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/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -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 +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -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 +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- 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 +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -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 +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- 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/paint-panel.tsx +0 -163
- 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,38 @@
|
|
|
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'
|
|
10
17
|
import { useViewer } from '@pascal-app/viewer'
|
|
11
18
|
import { useEffect, useMemo, useRef } from 'react'
|
|
12
19
|
import * as THREE from 'three'
|
|
13
20
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
21
|
+
import {
|
|
22
|
+
resolveStairDestinationLevel,
|
|
23
|
+
resolveStairPlacementLevelId,
|
|
24
|
+
} from '../../../lib/stair-levels'
|
|
25
|
+
|
|
26
|
+
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
27
|
+
import useEditor, {
|
|
28
|
+
isAlignmentGuideActive,
|
|
29
|
+
isGridSnapActive,
|
|
30
|
+
isMagneticSnapActive,
|
|
31
|
+
} from '../../../store/use-editor'
|
|
32
|
+
|
|
33
|
+
import useFacingPose from '../../../store/use-facing-pose'
|
|
14
34
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
35
|
+
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
15
36
|
import {
|
|
16
37
|
DEFAULT_CURVED_STAIR_INNER_RADIUS,
|
|
17
38
|
DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
|
|
@@ -23,6 +44,7 @@ import {
|
|
|
23
44
|
DEFAULT_STAIR_FILL_TO_FLOOR,
|
|
24
45
|
DEFAULT_STAIR_HEIGHT,
|
|
25
46
|
DEFAULT_STAIR_LENGTH,
|
|
47
|
+
DEFAULT_STAIR_OPENING_OFFSET,
|
|
26
48
|
DEFAULT_STAIR_RAILING_HEIGHT,
|
|
27
49
|
DEFAULT_STAIR_RAILING_MODE,
|
|
28
50
|
DEFAULT_STAIR_STEP_COUNT,
|
|
@@ -32,6 +54,23 @@ import {
|
|
|
32
54
|
} from './stair-defaults'
|
|
33
55
|
|
|
34
56
|
const GRID_OFFSET = 0.02
|
|
57
|
+
/** Figma-style alignment-snap threshold (meters), matching the move tools. */
|
|
58
|
+
const ALIGNMENT_THRESHOLD_M = 0.08
|
|
59
|
+
type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
|
|
60
|
+
|
|
61
|
+
const CLICK_TRIGGER_KINDS = [
|
|
62
|
+
'shelf',
|
|
63
|
+
'item',
|
|
64
|
+
'slab',
|
|
65
|
+
'ceiling',
|
|
66
|
+
'wall',
|
|
67
|
+
'fence',
|
|
68
|
+
'column',
|
|
69
|
+
'roof',
|
|
70
|
+
'roof-segment',
|
|
71
|
+
'stair',
|
|
72
|
+
'stair-segment',
|
|
73
|
+
] as const
|
|
35
74
|
|
|
36
75
|
/**
|
|
37
76
|
* Generates the step-profile geometry for the ghost preview.
|
|
@@ -69,19 +108,10 @@ function createStairPreviewGeometry(): THREE.BufferGeometry {
|
|
|
69
108
|
}
|
|
70
109
|
|
|
71
110
|
/**
|
|
72
|
-
* Creates a
|
|
111
|
+
* Creates a default straight stair segment.
|
|
73
112
|
*/
|
|
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({
|
|
113
|
+
function createDefaultStairSegment() {
|
|
114
|
+
return StairSegmentNode.parse({
|
|
85
115
|
segmentType: 'stair',
|
|
86
116
|
width: DEFAULT_STAIR_WIDTH,
|
|
87
117
|
length: DEFAULT_STAIR_LENGTH,
|
|
@@ -92,14 +122,24 @@ function commitStairPlacement(
|
|
|
92
122
|
thickness: DEFAULT_STAIR_THICKNESS,
|
|
93
123
|
position: [0, 0, 0],
|
|
94
124
|
})
|
|
125
|
+
}
|
|
95
126
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
function createDefaultStairNode({
|
|
128
|
+
name,
|
|
129
|
+
levelId,
|
|
130
|
+
nextLevelId,
|
|
131
|
+
position,
|
|
132
|
+
rotation,
|
|
133
|
+
segmentId,
|
|
134
|
+
}: {
|
|
135
|
+
name: string
|
|
136
|
+
levelId: LevelNode['id']
|
|
137
|
+
nextLevelId: LevelNode['id']
|
|
138
|
+
position: [number, number, number]
|
|
139
|
+
rotation: number
|
|
140
|
+
segmentId: StairSegmentNode['id']
|
|
141
|
+
}) {
|
|
142
|
+
return StairNode.parse({
|
|
103
143
|
name,
|
|
104
144
|
position,
|
|
105
145
|
rotation,
|
|
@@ -107,7 +147,7 @@ function commitStairPlacement(
|
|
|
107
147
|
fromLevelId: levelId,
|
|
108
148
|
toLevelId: nextLevelId,
|
|
109
149
|
slabOpeningMode: 'destination',
|
|
110
|
-
openingOffset:
|
|
150
|
+
openingOffset: DEFAULT_STAIR_OPENING_OFFSET,
|
|
111
151
|
width: DEFAULT_STAIR_WIDTH,
|
|
112
152
|
totalRise: DEFAULT_STAIR_HEIGHT,
|
|
113
153
|
stepCount: DEFAULT_STAIR_STEP_COUNT,
|
|
@@ -121,11 +161,55 @@ function commitStairPlacement(
|
|
|
121
161
|
showStepSupports: DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS,
|
|
122
162
|
railingHeight: DEFAULT_STAIR_RAILING_HEIGHT,
|
|
123
163
|
railingMode: DEFAULT_STAIR_RAILING_MODE,
|
|
124
|
-
children: [
|
|
164
|
+
children: [segmentId],
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Creates a stair group with one default stair segment at the given position/rotation.
|
|
170
|
+
*/
|
|
171
|
+
function commitStairPlacement(
|
|
172
|
+
levelId: LevelNode['id'],
|
|
173
|
+
position: [number, number, number],
|
|
174
|
+
rotation: number,
|
|
175
|
+
): void {
|
|
176
|
+
const { createNodes, nodes } = useScene.getState()
|
|
177
|
+
const placementLevelId = resolveStairPlacementLevelId(
|
|
178
|
+
nodes,
|
|
179
|
+
levelId,
|
|
180
|
+
useViewer.getState().selection.buildingId,
|
|
181
|
+
)
|
|
182
|
+
if (!placementLevelId) return
|
|
183
|
+
|
|
184
|
+
const stairCount = Object.values(nodes).filter((n) => n.type === 'stair').length
|
|
185
|
+
const name = `Staircase ${stairCount + 1}`
|
|
186
|
+
const segment = createDefaultStairSegment()
|
|
187
|
+
|
|
188
|
+
const destinationPlan = resolveStairDestinationLevel({
|
|
189
|
+
createMissing: true,
|
|
190
|
+
fromLevelId: placementLevelId,
|
|
191
|
+
nodes,
|
|
125
192
|
})
|
|
193
|
+
const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
|
|
194
|
+
|
|
195
|
+
const stair = createDefaultStairNode({
|
|
196
|
+
name,
|
|
197
|
+
levelId: placementLevelId,
|
|
198
|
+
nextLevelId,
|
|
199
|
+
position,
|
|
200
|
+
rotation,
|
|
201
|
+
segmentId: segment.id,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const createdLevel = destinationPlan?.createdLevel
|
|
205
|
+
const levelCreateOps =
|
|
206
|
+
createdLevel && destinationPlan.buildingId
|
|
207
|
+
? [{ node: createdLevel, parentId: destinationPlan.buildingId }]
|
|
208
|
+
: []
|
|
126
209
|
|
|
127
210
|
createNodes([
|
|
128
|
-
|
|
211
|
+
...levelCreateOps,
|
|
212
|
+
{ node: stair, parentId: placementLevelId },
|
|
129
213
|
{ node: segment, parentId: stair.id },
|
|
130
214
|
])
|
|
131
215
|
|
|
@@ -137,6 +221,7 @@ export const StairTool: React.FC = () => {
|
|
|
137
221
|
const previewRef = useRef<THREE.Group>(null)
|
|
138
222
|
const rotationRef = useRef(0)
|
|
139
223
|
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
224
|
+
const lastCanonicalPositionRef = useRef<[number, number, number] | null>(null)
|
|
140
225
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
141
226
|
|
|
142
227
|
const previewGeometry = useMemo(() => createStairPreviewGeometry(), [])
|
|
@@ -144,24 +229,187 @@ export const StairTool: React.FC = () => {
|
|
|
144
229
|
useEffect(() => {
|
|
145
230
|
if (!currentLevelId) return
|
|
146
231
|
|
|
232
|
+
const openingPreview = createSurfaceOpeningPreviewController()
|
|
233
|
+
|
|
147
234
|
// Reset rotation when tool activates
|
|
148
235
|
rotationRef.current = 0
|
|
149
236
|
if (previewRef.current) previewRef.current.rotation.y = 0
|
|
237
|
+
lastCanonicalPositionRef.current = null
|
|
238
|
+
|
|
239
|
+
const buildPreviewScene = (position: [number, number, number], rotation: number) => {
|
|
240
|
+
const nodes = useScene.getState().nodes
|
|
241
|
+
const placementLevelId = resolveStairPlacementLevelId(
|
|
242
|
+
nodes,
|
|
243
|
+
currentLevelId,
|
|
244
|
+
useViewer.getState().selection.buildingId,
|
|
245
|
+
)
|
|
246
|
+
if (!placementLevelId) return null
|
|
247
|
+
|
|
248
|
+
const destinationPlan = resolveStairDestinationLevel({
|
|
249
|
+
createMissing: true,
|
|
250
|
+
fromLevelId: placementLevelId,
|
|
251
|
+
nodes,
|
|
252
|
+
})
|
|
253
|
+
const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
|
|
254
|
+
const segment = createDefaultStairSegment()
|
|
255
|
+
const stair = createDefaultStairNode({
|
|
256
|
+
name: 'Staircase Preview',
|
|
257
|
+
levelId: placementLevelId,
|
|
258
|
+
nextLevelId,
|
|
259
|
+
position,
|
|
260
|
+
rotation,
|
|
261
|
+
segmentId: segment.id,
|
|
262
|
+
})
|
|
263
|
+
const previewNodes = {
|
|
264
|
+
...nodes,
|
|
265
|
+
...(destinationPlan?.createdLevel
|
|
266
|
+
? { [destinationPlan.createdLevel.id]: destinationPlan.createdLevel }
|
|
267
|
+
: {}),
|
|
268
|
+
[stair.id]: { ...stair, parentId: placementLevelId },
|
|
269
|
+
[segment.id]: { ...segment, parentId: stair.id },
|
|
270
|
+
} as Record<string, AnyNode>
|
|
271
|
+
|
|
272
|
+
return { placementLevelId, previewNodes, stair }
|
|
273
|
+
}
|
|
150
274
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
275
|
+
// The preview rebuild (full-scene copy + destination-level resolution +
|
|
276
|
+
// auto-opening CSG) is expensive; `grid:move` fires it every pointer event
|
|
277
|
+
// but the placed position is grid-snapped, so within a cell every rebuild
|
|
278
|
+
// is identical. Dedupe on the snapped position + rotation so we rebuild
|
|
279
|
+
// only when the staircase would actually land somewhere new — this is the
|
|
280
|
+
// difference between a smooth and a stuttering stair tool (the elevator is
|
|
281
|
+
// cheap because it has no opening sync).
|
|
282
|
+
let lastPreviewKey: string | null = null
|
|
283
|
+
|
|
284
|
+
const applyDraftPreview = (position: [number, number, number], rotation: number) => {
|
|
285
|
+
const key = `${position[0].toFixed(3)},${position[2].toFixed(3)},${rotation.toFixed(4)}`
|
|
286
|
+
if (key === lastPreviewKey) return
|
|
287
|
+
lastPreviewKey = key
|
|
288
|
+
const preview = buildPreviewScene(position, rotation)
|
|
289
|
+
const visualPosition = preview
|
|
290
|
+
? getFloorStackPreviewPosition({
|
|
291
|
+
node: preview.stair,
|
|
292
|
+
position,
|
|
293
|
+
rotation,
|
|
294
|
+
levelId: preview.placementLevelId,
|
|
295
|
+
nodes: preview.previewNodes,
|
|
296
|
+
})
|
|
297
|
+
: position
|
|
156
298
|
if (cursorRef.current) {
|
|
157
|
-
cursorRef.current.position.set(
|
|
299
|
+
cursorRef.current.position.set(
|
|
300
|
+
visualPosition[0],
|
|
301
|
+
visualPosition[1] + GRID_OFFSET,
|
|
302
|
+
visualPosition[2],
|
|
303
|
+
)
|
|
158
304
|
}
|
|
159
305
|
|
|
160
306
|
if (previewRef.current) {
|
|
161
|
-
previewRef.current.position.set(
|
|
307
|
+
previewRef.current.position.set(...visualPosition)
|
|
308
|
+
previewRef.current.rotation.y = rotation
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Forward-facing triangle (editor-side overlay). The run ascends along
|
|
312
|
+
// local +Z from the entry at z≈0; the stair's front is the -Z entry side,
|
|
313
|
+
// so `reversed` points the triangle out of the entry (where you approach
|
|
314
|
+
// from), sitting just before it — not inside the footprint or at the
|
|
315
|
+
// elevated far end. Centre is the footprint mid-run (origin is the entry).
|
|
316
|
+
useFacingPose.getState().set({
|
|
317
|
+
position: visualPosition,
|
|
318
|
+
rotationY: rotation,
|
|
319
|
+
depth: DEFAULT_STAIR_LENGTH,
|
|
320
|
+
center: [0, DEFAULT_STAIR_LENGTH / 2],
|
|
321
|
+
reversed: true,
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
if (!preview) {
|
|
325
|
+
openingPreview.clear()
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
openingPreview.apply(syncAutoStairOpenings(preview.previewNodes))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Alignment candidates — anchors of every alignable object; refreshed
|
|
333
|
+
// after each placement. The moving stair aligns by its footprint edges so
|
|
334
|
+
// users can snap the run side against walls, slabs, elevators, or another
|
|
335
|
+
// stair instead of only lining up the invisible origin point.
|
|
336
|
+
let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
|
|
337
|
+
const resolveStairFootprintAlignment = (
|
|
338
|
+
x: number,
|
|
339
|
+
z: number,
|
|
340
|
+
rotation: number,
|
|
341
|
+
): ReturnType<typeof resolveAlignment> | null => {
|
|
342
|
+
const preview = buildPreviewScene([x, 0, z], rotation)
|
|
343
|
+
const moving = preview
|
|
344
|
+
? movingAlignmentAnchors(preview.stair, preview.previewNodes, x, z, rotation)
|
|
345
|
+
: []
|
|
346
|
+
if (moving.length === 0) return null
|
|
347
|
+
return resolveAlignment({
|
|
348
|
+
moving,
|
|
349
|
+
candidates: alignmentCandidates,
|
|
350
|
+
threshold: ALIGNMENT_THRESHOLD_M,
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
// The probe is the RAW cursor, not the grid-snapped point: resolving
|
|
354
|
+
// against the grid point would only catch anchors that happen to sit near
|
|
355
|
+
// a grid line. Matched axes use the raw probe + snap delta; unmatched axes
|
|
356
|
+
// keep the normal grid snap. Guides are published in every snapping mode
|
|
357
|
+
// (including Off); the magnetic pull toward them (applySnap) applies only in
|
|
358
|
+
// 'lines' mode.
|
|
359
|
+
const alignPoint = (
|
|
360
|
+
gridX: number,
|
|
361
|
+
gridZ: number,
|
|
362
|
+
rawX: number,
|
|
363
|
+
rawZ: number,
|
|
364
|
+
bypass: boolean,
|
|
365
|
+
applySnap: boolean,
|
|
366
|
+
): [number, number] => {
|
|
367
|
+
if (bypass || alignmentCandidates.length === 0) {
|
|
368
|
+
useAlignmentGuides.getState().clear()
|
|
369
|
+
return [gridX, gridZ]
|
|
370
|
+
}
|
|
371
|
+
const ar = resolveStairFootprintAlignment(rawX, rawZ, rotationRef.current)
|
|
372
|
+
if (!ar || ar.guides.length === 0) {
|
|
373
|
+
useAlignmentGuides.getState().clear()
|
|
374
|
+
return [gridX, gridZ]
|
|
162
375
|
}
|
|
376
|
+
if (!applySnap) {
|
|
377
|
+
useAlignmentGuides.getState().set(ar.guides)
|
|
378
|
+
return [gridX, gridZ]
|
|
379
|
+
}
|
|
380
|
+
let x = gridX
|
|
381
|
+
let z = gridZ
|
|
382
|
+
if (ar.snap) {
|
|
383
|
+
if (ar.guides.some((guide) => guide.axis === 'x')) x = rawX + ar.snap.dx
|
|
384
|
+
if (ar.guides.some((guide) => guide.axis === 'z')) z = rawZ + ar.snap.dz
|
|
385
|
+
}
|
|
386
|
+
const finalAlignment = resolveStairFootprintAlignment(x, z, rotationRef.current)
|
|
387
|
+
useAlignmentGuides.getState().set(finalAlignment?.guides ?? ar.guides)
|
|
388
|
+
return [x, z]
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const onGridMove = (event: GridEvent) => {
|
|
392
|
+
// Grid snap follows the global mode (live step so the HUD chip is
|
|
393
|
+
// honest); Off keeps the raw cursor. Shift cycles the mode centrally.
|
|
394
|
+
const step = useEditor.getState().gridSnapStep
|
|
395
|
+
const [gridX, gridZ] = alignPoint(
|
|
396
|
+
isGridSnapActive()
|
|
397
|
+
? Math.round(event.localPosition[0] / step) * step
|
|
398
|
+
: event.localPosition[0],
|
|
399
|
+
isGridSnapActive()
|
|
400
|
+
? Math.round(event.localPosition[2] / step) * step
|
|
401
|
+
: event.localPosition[2],
|
|
402
|
+
event.localPosition[0],
|
|
403
|
+
event.localPosition[2],
|
|
404
|
+
!isAlignmentGuideActive(),
|
|
405
|
+
isMagneticSnapActive(),
|
|
406
|
+
)
|
|
407
|
+
const position: [number, number, number] = [gridX, 0, gridZ]
|
|
408
|
+
lastCanonicalPositionRef.current = position
|
|
409
|
+
applyDraftPreview(position, rotationRef.current)
|
|
163
410
|
|
|
164
411
|
if (
|
|
412
|
+
(isGridSnapActive() || isMagneticSnapActive()) &&
|
|
165
413
|
previousGridPosRef.current &&
|
|
166
414
|
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
167
415
|
) {
|
|
@@ -171,12 +419,52 @@ export const StairTool: React.FC = () => {
|
|
|
171
419
|
previousGridPosRef.current = [gridX, gridZ]
|
|
172
420
|
}
|
|
173
421
|
|
|
174
|
-
const
|
|
422
|
+
const getAlignedGridPosition = (event: GridEvent): [number, number, number] => {
|
|
423
|
+
const step = useEditor.getState().gridSnapStep
|
|
424
|
+
const [gridX, gridZ] = alignPoint(
|
|
425
|
+
isGridSnapActive()
|
|
426
|
+
? Math.round(event.localPosition[0] / step) * step
|
|
427
|
+
: event.localPosition[0],
|
|
428
|
+
isGridSnapActive()
|
|
429
|
+
? Math.round(event.localPosition[2] / step) * step
|
|
430
|
+
: event.localPosition[2],
|
|
431
|
+
event.localPosition[0],
|
|
432
|
+
event.localPosition[2],
|
|
433
|
+
!isAlignmentGuideActive(),
|
|
434
|
+
isMagneticSnapActive(),
|
|
435
|
+
)
|
|
436
|
+
return [gridX, 0, gridZ]
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const commitAtCursor = (event: ClickTriggerEvent) => {
|
|
175
440
|
if (!currentLevelId) return
|
|
441
|
+
const nodeEvent = 'node' in event ? (event as NodeEvent<AnyNode>) : null
|
|
442
|
+
if (nodeEvent) {
|
|
443
|
+
nodeEvent.stopPropagation()
|
|
444
|
+
nodeEvent.nativeEvent.stopPropagation()
|
|
445
|
+
}
|
|
176
446
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
447
|
+
const position = nodeEvent
|
|
448
|
+
? lastCanonicalPositionRef.current
|
|
449
|
+
: getAlignedGridPosition(event as GridEvent)
|
|
450
|
+
if (!position) return
|
|
451
|
+
|
|
452
|
+
commitStairPlacement(currentLevelId, position, rotationRef.current)
|
|
453
|
+
openingPreview.clear()
|
|
454
|
+
// Commit cleared the opening preview, so force the next hover (even on the
|
|
455
|
+
// same cell) to rebuild rather than dedupe against the just-placed key.
|
|
456
|
+
lastPreviewKey = null
|
|
457
|
+
useAlignmentGuides.getState().clear()
|
|
458
|
+
|
|
459
|
+
// Single by default; the C-toggle ('point' context, shared with every
|
|
460
|
+
// other placement tool) opts into placing more. On single, drop the tool
|
|
461
|
+
// and the facing triangle so we fall back to select after one stair.
|
|
462
|
+
if (useEditor.getState().getContinuation('point') === 'repeat') {
|
|
463
|
+
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
|
|
464
|
+
} else {
|
|
465
|
+
useFacingPose.getState().clear()
|
|
466
|
+
useEditor.getState().setTool(null)
|
|
467
|
+
}
|
|
180
468
|
}
|
|
181
469
|
|
|
182
470
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
@@ -193,20 +481,35 @@ export const StairTool: React.FC = () => {
|
|
|
193
481
|
event.preventDefault()
|
|
194
482
|
sfxEmitter.emit('sfx:item-rotate')
|
|
195
483
|
rotationRef.current += rotationDelta
|
|
196
|
-
if (
|
|
484
|
+
if (lastCanonicalPositionRef.current) {
|
|
485
|
+
applyDraftPreview(lastCanonicalPositionRef.current, rotationRef.current)
|
|
486
|
+
} else if (previewRef.current) {
|
|
197
487
|
previewRef.current.rotation.y = rotationRef.current
|
|
198
488
|
}
|
|
199
489
|
}
|
|
200
490
|
}
|
|
201
491
|
|
|
202
492
|
emitter.on('grid:move', onGridMove)
|
|
203
|
-
emitter.on('grid:click',
|
|
493
|
+
emitter.on('grid:click', commitAtCursor)
|
|
494
|
+
type SuffixedKey<K extends string> = `${K}:${EventSuffix}`
|
|
495
|
+
type ClickKey = SuffixedKey<(typeof CLICK_TRIGGER_KINDS)[number]>
|
|
496
|
+
for (const kind of CLICK_TRIGGER_KINDS) {
|
|
497
|
+
const key = `${kind}:click` as ClickKey
|
|
498
|
+
emitter.on(key, commitAtCursor as never)
|
|
499
|
+
}
|
|
204
500
|
window.addEventListener('keydown', onKeyDown)
|
|
205
501
|
|
|
206
502
|
return () => {
|
|
207
503
|
emitter.off('grid:move', onGridMove)
|
|
208
|
-
emitter.off('grid:click',
|
|
504
|
+
emitter.off('grid:click', commitAtCursor)
|
|
505
|
+
for (const kind of CLICK_TRIGGER_KINDS) {
|
|
506
|
+
const key = `${kind}:click` as ClickKey
|
|
507
|
+
emitter.off(key, commitAtCursor as never)
|
|
508
|
+
}
|
|
209
509
|
window.removeEventListener('keydown', onKeyDown)
|
|
510
|
+
useAlignmentGuides.getState().clear()
|
|
511
|
+
openingPreview.clear()
|
|
512
|
+
useFacingPose.getState().clear()
|
|
210
513
|
}
|
|
211
514
|
}, [currentLevelId])
|
|
212
515
|
|
|
@@ -214,7 +517,9 @@ export const StairTool: React.FC = () => {
|
|
|
214
517
|
<group>
|
|
215
518
|
<CursorSphere ref={cursorRef} />
|
|
216
519
|
|
|
217
|
-
{/* 3D ghost preview — position/rotation updated imperatively
|
|
520
|
+
{/* 3D ghost preview — position/rotation updated imperatively. The
|
|
521
|
+
forward-facing triangle is drawn by the editor-side overlay from the
|
|
522
|
+
pose published in `applyDraftPreview`. */}
|
|
218
523
|
<group ref={previewRef}>
|
|
219
524
|
<mesh castShadow geometry={previewGeometry}>
|
|
220
525
|
<meshStandardMaterial color="#818cf8" depthWrite={false} opacity={0.35} transparent />
|