@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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { type ComponentType, Suspense, useMemo } from 'react'
|
|
6
|
+
import { getRegistryAffordanceTool } from '../tools/shared/affordance-dispatch'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Editor-mounted dispatcher for a kind's selection-time editing UI.
|
|
10
|
+
*
|
|
11
|
+
* Some kinds expose drag-to-edit affordances that should appear only
|
|
12
|
+
* while a single node of that kind is selected — duct / pipe / lineset
|
|
13
|
+
* path-point handles, fitting Alt-axis-cycling listeners. These read
|
|
14
|
+
* `useEditor` (grid snap step, rotation axis) and render the editor's
|
|
15
|
+
* `DimensionPill`, so they must NOT ride in `def.system` (which the
|
|
16
|
+
* viewer package mounts for the read-only route). The kind declares the
|
|
17
|
+
* component under `def.affordanceTools.selection` and this manager —
|
|
18
|
+
* mounted inside the editor only — loads it for the selected kind.
|
|
19
|
+
*/
|
|
20
|
+
export function SelectionAffordanceManager() {
|
|
21
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
22
|
+
const selectedKind = useScene((s) => {
|
|
23
|
+
if (selectedIds.length !== 1) return null
|
|
24
|
+
return s.nodes[selectedIds[0] as AnyNodeId]?.type ?? null
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const Component = useMemo<ComponentType | null>(() => {
|
|
28
|
+
if (!selectedKind) return null
|
|
29
|
+
return getRegistryAffordanceTool(selectedKind, 'selection')
|
|
30
|
+
}, [selectedKind])
|
|
31
|
+
|
|
32
|
+
if (!Component) return null
|
|
33
|
+
return (
|
|
34
|
+
<Suspense fallback={null}>
|
|
35
|
+
<Component />
|
|
36
|
+
</Suspense>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -6,8 +6,10 @@ import { Check, Pencil } from 'lucide-react'
|
|
|
6
6
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
7
7
|
import { createPortal } from 'react-dom'
|
|
8
8
|
import { useShallow } from 'zustand/react/shallow'
|
|
9
|
+
import { resolveOverlayPolicy } from '../../../lib/interaction/overlay-policy'
|
|
9
10
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
10
11
|
import useEditor from '../../../store/use-editor'
|
|
12
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
11
13
|
|
|
12
14
|
// ─── Per-zone label editor ────────────────────────────────────────────────────
|
|
13
15
|
|
|
@@ -19,6 +21,10 @@ function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
|
|
|
19
21
|
const selectedZoneId = useViewer((s) => s.selection.zoneId)
|
|
20
22
|
const hoveredId = useViewer((s) => s.hoveredId)
|
|
21
23
|
const mode = useEditor((s) => s.mode)
|
|
24
|
+
// During an active interaction the zone label is a context badge that steps
|
|
25
|
+
// back: faded + non-interactive so it can't be hovered/clicked mid-action.
|
|
26
|
+
const scope = useInteractionScope((s) => s.scope)
|
|
27
|
+
const labelStepBack = resolveOverlayPolicy(scope).contextBadges === 'faded'
|
|
22
28
|
const isSelected = selectedZoneId === zoneId
|
|
23
29
|
const isDeleteHovered = mode === 'delete' && hoveredId === zoneId
|
|
24
30
|
const [editing, setEditing] = useState(false)
|
|
@@ -149,7 +155,8 @@ function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
|
|
|
149
155
|
fontSize: 14,
|
|
150
156
|
fontFamily: 'sans-serif',
|
|
151
157
|
userSelect: 'none',
|
|
152
|
-
pointerEvents: 'auto',
|
|
158
|
+
pointerEvents: labelStepBack ? 'none' : 'auto',
|
|
159
|
+
opacity: labelStepBack ? 0.4 : undefined,
|
|
153
160
|
display: 'inline-flex',
|
|
154
161
|
alignItems: 'center',
|
|
155
162
|
gap: 4,
|
|
@@ -309,6 +316,12 @@ export function ZoneLabelEditorSystem() {
|
|
|
309
316
|
.map((n) => n.id as ZoneNode['id']),
|
|
310
317
|
),
|
|
311
318
|
)
|
|
319
|
+
// The zone renderer unmounts its <Html> label when zones are hidden —
|
|
320
|
+
// unmount the portals with it, or each editor would hold (and rAF-poll for)
|
|
321
|
+
// a detached label element.
|
|
322
|
+
const showZones = useViewer((s) => s.showZones)
|
|
323
|
+
|
|
324
|
+
if (!showZones) return null
|
|
312
325
|
|
|
313
326
|
return (
|
|
314
327
|
<>
|
|
@@ -1,21 +1,46 @@
|
|
|
1
1
|
import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { useFrame } from '@react-three/fiber'
|
|
4
|
+
import { useEffect } from 'react'
|
|
4
5
|
import { type Group, MathUtils, type Mesh } from 'three'
|
|
5
6
|
import type { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
7
|
+
import { resolveOverlayPolicy } from '../../../lib/interaction/overlay-policy'
|
|
6
8
|
import useEditor from '../../../store/use-editor'
|
|
9
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
7
10
|
|
|
8
11
|
// Disable raycasting on zone geometry so clicks pass through to items underneath.
|
|
9
12
|
// Zone selection in the editor is handled exclusively via the HTML label overlay.
|
|
10
13
|
const noopRaycast = () => {}
|
|
11
14
|
|
|
12
15
|
export const ZoneSystem = () => {
|
|
16
|
+
// Outside the zones layer (or during snapshot capture) zones unmount
|
|
17
|
+
// entirely — meshes AND drei <Html> labels, which cost per-frame matrix work
|
|
18
|
+
// + live DOM even at opacity 0. The renderer reads this viewer flag; the
|
|
19
|
+
// unmount cleanup restores the default so preview / first-person surfaces
|
|
20
|
+
// (which swap this system for ViewerZoneSystem) keep their labels.
|
|
21
|
+
const structureLayerState = useEditor((s) => s.structureLayer)
|
|
22
|
+
const isCaptureModeState = useEditor((s) => s.isCaptureMode)
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
useViewer.getState().setShowZones(structureLayerState === 'zones' && !isCaptureModeState)
|
|
25
|
+
return () => useViewer.getState().setShowZones(true)
|
|
26
|
+
}, [structureLayerState, isCaptureModeState])
|
|
27
|
+
|
|
13
28
|
useFrame((_, delta) => {
|
|
29
|
+
if (!useViewer.getState().showZones) return
|
|
30
|
+
|
|
14
31
|
const structureLayer = useEditor.getState().structureLayer
|
|
15
32
|
const editorMode = useEditor.getState().mode
|
|
16
33
|
const selectedLevelId = useViewer.getState().selection.levelId
|
|
17
34
|
const selectedZoneId = useViewer.getState().selection.zoneId
|
|
18
35
|
const hoveredId = useViewer.getState().hoveredId
|
|
36
|
+
// Snapshot capture is a clean, camera-only surface — never show zone
|
|
37
|
+
// geometry or the HTML zone tags in the framed shot.
|
|
38
|
+
const isCaptureMode = useEditor.getState().isCaptureMode
|
|
39
|
+
|
|
40
|
+
// During any active interaction zone labels step back entirely — they are
|
|
41
|
+
// not a primary editing concern and would distract / invite misclicks.
|
|
42
|
+
const zoneLabelsHidden =
|
|
43
|
+
resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
|
|
19
44
|
|
|
20
45
|
const zoneGeometryVisible = structureLayer === 'zones'
|
|
21
46
|
const zones = sceneRegistry.byType.zone || new Set()
|
|
@@ -35,12 +60,22 @@ export const ZoneSystem = () => {
|
|
|
35
60
|
// Keep group visible (so <Html> labels stay active), hide/show meshes only.
|
|
36
61
|
// Show meshes when: in zone mode, selected, or delete-hovered.
|
|
37
62
|
if (!obj.visible) obj.visible = true
|
|
38
|
-
const meshVisible = zoneGeometryVisible || isSelected || isDeleteHovered
|
|
39
|
-
const targetOpacity =
|
|
63
|
+
const meshVisible = !isCaptureMode && (zoneGeometryVisible || isSelected || isDeleteHovered)
|
|
64
|
+
const targetOpacity = isCaptureMode
|
|
65
|
+
? 0
|
|
66
|
+
: isSelected || isDeleteHovered
|
|
67
|
+
? 1
|
|
68
|
+
: zoneGeometryVisible
|
|
69
|
+
? 1
|
|
70
|
+
: 0
|
|
40
71
|
|
|
72
|
+
// Raycast is re-disabled per frame (not once per group): the meshes
|
|
73
|
+
// remount whenever the zones layer toggles, so a one-shot flag on the
|
|
74
|
+
// persistent group would leave fresh meshes clickable.
|
|
41
75
|
const walls = (obj as Group).getObjectByName('walls') as Mesh | undefined
|
|
42
76
|
if (walls) {
|
|
43
77
|
walls.visible = meshVisible
|
|
78
|
+
walls.raycast = noopRaycast
|
|
44
79
|
const material = walls.material as MeshBasicNodeMaterial
|
|
45
80
|
if (material?.userData?.uOpacity) {
|
|
46
81
|
material.userData.uOpacity.value = MathUtils.lerp(
|
|
@@ -54,6 +89,7 @@ export const ZoneSystem = () => {
|
|
|
54
89
|
const floor = (obj as Group).getObjectByName('floor') as Mesh | undefined
|
|
55
90
|
if (floor) {
|
|
56
91
|
floor.visible = meshVisible
|
|
92
|
+
floor.raycast = noopRaycast
|
|
57
93
|
const material = floor.material as MeshBasicNodeMaterial
|
|
58
94
|
if (material?.userData?.uOpacity) {
|
|
59
95
|
material.userData.uOpacity.value = MathUtils.lerp(
|
|
@@ -64,17 +100,10 @@ export const ZoneSystem = () => {
|
|
|
64
100
|
}
|
|
65
101
|
}
|
|
66
102
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
child.raycast = noopRaycast
|
|
72
|
-
})
|
|
73
|
-
obj.userData.__raycastDisabled = true
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Labels: always visible on the current level (regardless of mode)
|
|
77
|
-
const showLabel = !!selectedLevelId && isOnSelectedLevel
|
|
103
|
+
// Labels: visible on the current level (regardless of mode), but never
|
|
104
|
+
// during snapshot capture.
|
|
105
|
+
const showLabel =
|
|
106
|
+
!isCaptureMode && !zoneLabelsHidden && !!selectedLevelId && isOnSelectedLevel
|
|
78
107
|
const labelOpacity = showLabel ? '1' : '0'
|
|
79
108
|
const labelEl = document.getElementById(`${zoneId}-label`)
|
|
80
109
|
if (labelEl && labelEl.style.opacity !== labelOpacity) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const DEFAULT_ELEVATOR_WIDTH = 1.84
|
|
2
|
+
export const DEFAULT_ELEVATOR_DEPTH = 1.84
|
|
3
|
+
export const DEFAULT_ELEVATOR_CAB_HEIGHT = 2.35
|
|
4
|
+
export const DEFAULT_ELEVATOR_DOOR_WIDTH = 0.95
|
|
5
|
+
export const DEFAULT_ELEVATOR_DOOR_HEIGHT = 2.1
|
|
6
|
+
export const DEFAULT_ELEVATOR_SPEED = 2.2
|
|
7
|
+
export const DEFAULT_ELEVATOR_DOOR_DURATION_MS = 900
|
|
8
|
+
export const DEFAULT_ELEVATOR_DWELL_MS = 1400
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
type BuildingNode,
|
|
4
|
+
collectAlignmentAnchors,
|
|
5
|
+
ElevatorNode,
|
|
6
|
+
emitter,
|
|
7
|
+
type GridEvent,
|
|
8
|
+
type LevelNode,
|
|
9
|
+
resolveAlignment,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
13
|
+
import * as THREE from 'three'
|
|
14
|
+
import { resolveCurrentBuildingId, resolveElevatorSupportY } from '../../../lib/elevator-support'
|
|
15
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
|
+
|
|
17
|
+
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
18
|
+
import useEditor, {
|
|
19
|
+
isAlignmentGuideActive,
|
|
20
|
+
isGridSnapActive,
|
|
21
|
+
isMagneticSnapActive,
|
|
22
|
+
} from '../../../store/use-editor'
|
|
23
|
+
|
|
24
|
+
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
25
|
+
import { CursorSphere } from '../shared/cursor-sphere'
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_ELEVATOR_CAB_HEIGHT,
|
|
28
|
+
DEFAULT_ELEVATOR_DEPTH,
|
|
29
|
+
DEFAULT_ELEVATOR_DOOR_DURATION_MS,
|
|
30
|
+
DEFAULT_ELEVATOR_DOOR_HEIGHT,
|
|
31
|
+
DEFAULT_ELEVATOR_DOOR_WIDTH,
|
|
32
|
+
DEFAULT_ELEVATOR_DWELL_MS,
|
|
33
|
+
DEFAULT_ELEVATOR_SPEED,
|
|
34
|
+
DEFAULT_ELEVATOR_WIDTH,
|
|
35
|
+
} from './elevator-defaults'
|
|
36
|
+
|
|
37
|
+
const GRID_OFFSET = 0.02
|
|
38
|
+
/** Figma-style alignment-snap threshold (meters), matching the move tools. */
|
|
39
|
+
const ALIGNMENT_THRESHOLD_M = 0.08
|
|
40
|
+
|
|
41
|
+
type ElevatorToolProps = {
|
|
42
|
+
buildingId: BuildingNode['id'] | null
|
|
43
|
+
levelId: LevelNode['id'] | null
|
|
44
|
+
onPlaced?: (elevatorId: AnyNodeId, buildingId: BuildingNode['id']) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveDefaultServiceRange(
|
|
48
|
+
buildingId: BuildingNode['id'],
|
|
49
|
+
selectedLevelId: LevelNode['id'] | null,
|
|
50
|
+
): {
|
|
51
|
+
defaultLevelId: LevelNode['id'] | null
|
|
52
|
+
fromLevelId: LevelNode['id'] | null
|
|
53
|
+
toLevelId: LevelNode['id'] | null
|
|
54
|
+
} {
|
|
55
|
+
const nodes = useScene.getState().nodes
|
|
56
|
+
const building = nodes[buildingId as AnyNodeId]
|
|
57
|
+
if (building?.type !== 'building') {
|
|
58
|
+
return { defaultLevelId: null, fromLevelId: null, toLevelId: null }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const levels = building.children
|
|
62
|
+
.map((childId) => nodes[childId as AnyNodeId])
|
|
63
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
64
|
+
.sort((left, right) => left.level - right.level)
|
|
65
|
+
const selectedLevelIndex = levels.findIndex((level) => level.id === selectedLevelId)
|
|
66
|
+
const fromIndex = selectedLevelIndex >= 0 ? selectedLevelIndex : 0
|
|
67
|
+
const fromLevel = levels[fromIndex]
|
|
68
|
+
const toLevel = levels[Math.min(fromIndex + 1, levels.length - 1)] ?? fromLevel
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
defaultLevelId: fromLevel?.id ?? null,
|
|
72
|
+
fromLevelId: fromLevel?.id ?? null,
|
|
73
|
+
toLevelId: toLevel?.id ?? fromLevel?.id ?? null,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createElevatorPreviewGeometry(): THREE.BufferGeometry {
|
|
78
|
+
return new THREE.BoxGeometry(
|
|
79
|
+
DEFAULT_ELEVATOR_WIDTH,
|
|
80
|
+
DEFAULT_ELEVATOR_CAB_HEIGHT,
|
|
81
|
+
DEFAULT_ELEVATOR_DEPTH,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createElevatorPreviewEdgeGeometry(): THREE.BufferGeometry {
|
|
86
|
+
return new THREE.EdgesGeometry(createElevatorPreviewGeometry())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function commitElevatorPlacement(
|
|
90
|
+
buildingId: BuildingNode['id'],
|
|
91
|
+
selectedLevelId: LevelNode['id'] | null,
|
|
92
|
+
x: number,
|
|
93
|
+
z: number,
|
|
94
|
+
rotation: number,
|
|
95
|
+
onPlaced: ElevatorToolProps['onPlaced'],
|
|
96
|
+
): void {
|
|
97
|
+
const { createNode, nodes } = useScene.getState()
|
|
98
|
+
const elevatorCount = Object.values(nodes).filter((node) => node.type === 'elevator').length
|
|
99
|
+
const serviceRange = resolveDefaultServiceRange(buildingId, selectedLevelId)
|
|
100
|
+
const supportY = resolveElevatorSupportY({
|
|
101
|
+
buildingId,
|
|
102
|
+
preferredLevelId: serviceRange.fromLevelId ?? serviceRange.defaultLevelId,
|
|
103
|
+
x,
|
|
104
|
+
z,
|
|
105
|
+
})
|
|
106
|
+
const elevator = ElevatorNode.parse({
|
|
107
|
+
name: `Elevator ${elevatorCount + 1}`,
|
|
108
|
+
parentId: buildingId,
|
|
109
|
+
position: [x, supportY, z],
|
|
110
|
+
rotation,
|
|
111
|
+
width: DEFAULT_ELEVATOR_WIDTH,
|
|
112
|
+
depth: DEFAULT_ELEVATOR_DEPTH,
|
|
113
|
+
cabHeight: DEFAULT_ELEVATOR_CAB_HEIGHT,
|
|
114
|
+
doorWidth: DEFAULT_ELEVATOR_DOOR_WIDTH,
|
|
115
|
+
doorHeight: DEFAULT_ELEVATOR_DOOR_HEIGHT,
|
|
116
|
+
...serviceRange,
|
|
117
|
+
speed: DEFAULT_ELEVATOR_SPEED,
|
|
118
|
+
doorDurationMs: DEFAULT_ELEVATOR_DOOR_DURATION_MS,
|
|
119
|
+
dwellMs: DEFAULT_ELEVATOR_DWELL_MS,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
createNode(elevator, buildingId)
|
|
123
|
+
onPlaced?.(elevator.id as AnyNodeId, buildingId)
|
|
124
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId, onPlaced }) => {
|
|
128
|
+
const cursorRef = useRef<THREE.Group>(null)
|
|
129
|
+
const previewRef = useRef<THREE.Group>(null)
|
|
130
|
+
const rotationRef = useRef(0)
|
|
131
|
+
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
132
|
+
const previewGeometry = useMemo(() => createElevatorPreviewGeometry(), [])
|
|
133
|
+
const previewEdgeGeometry = useMemo(() => createElevatorPreviewEdgeGeometry(), [])
|
|
134
|
+
|
|
135
|
+
// Default-shaped elevator for the 2D floor-plan placement ghost. The 3D
|
|
136
|
+
// preview meshes below are hidden in 2D (canvas `display:none`), so this
|
|
137
|
+
// feeds `usePlacementPreview` → `FloorplanPlacementPreviewLayer`, which
|
|
138
|
+
// renders the elevator's footprint following the cursor.
|
|
139
|
+
const floorplanPreviewNode = useMemo(
|
|
140
|
+
() =>
|
|
141
|
+
ElevatorNode.parse({
|
|
142
|
+
name: 'Elevator',
|
|
143
|
+
position: [0, 0, 0],
|
|
144
|
+
rotation: 0,
|
|
145
|
+
width: DEFAULT_ELEVATOR_WIDTH,
|
|
146
|
+
depth: DEFAULT_ELEVATOR_DEPTH,
|
|
147
|
+
cabHeight: DEFAULT_ELEVATOR_CAB_HEIGHT,
|
|
148
|
+
doorWidth: DEFAULT_ELEVATOR_DOOR_WIDTH,
|
|
149
|
+
doorHeight: DEFAULT_ELEVATOR_DOOR_HEIGHT,
|
|
150
|
+
}),
|
|
151
|
+
[],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const currentBuildingId = resolveCurrentBuildingId({
|
|
156
|
+
buildingId,
|
|
157
|
+
levelId,
|
|
158
|
+
nodes: useScene.getState().nodes,
|
|
159
|
+
})
|
|
160
|
+
if (!currentBuildingId) return
|
|
161
|
+
|
|
162
|
+
rotationRef.current = 0
|
|
163
|
+
if (previewRef.current) previewRef.current.rotation.y = 0
|
|
164
|
+
|
|
165
|
+
// Alignment candidates — anchors of every alignable object; refreshed
|
|
166
|
+
// after each placement. The elevator aligns by its ORIGIN point.
|
|
167
|
+
let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', levelId)
|
|
168
|
+
// Snap the elevator origin onto another object's nearest real anchor and
|
|
169
|
+
// publish the guide. The probe is the RAW cursor, NOT the 0.5m-grid-snapped
|
|
170
|
+
// point: resolving against the grid point would only ever catch anchors
|
|
171
|
+
// that happen to sit on a grid line, so off-grid items (furniture, angled
|
|
172
|
+
// walls) would never surface a guide. The matched axis locks exactly to the
|
|
173
|
+
// candidate's coordinate; the other axis keeps its grid snap. Guides are
|
|
174
|
+
// published in every snapping mode (including Off); the magnetic pull toward
|
|
175
|
+
// them (applySnap) applies only in 'lines' mode.
|
|
176
|
+
const alignPoint = (
|
|
177
|
+
gridX: number,
|
|
178
|
+
gridZ: number,
|
|
179
|
+
rawX: number,
|
|
180
|
+
rawZ: number,
|
|
181
|
+
bypass: boolean,
|
|
182
|
+
applySnap: boolean,
|
|
183
|
+
): [number, number] => {
|
|
184
|
+
if (bypass || alignmentCandidates.length === 0) {
|
|
185
|
+
useAlignmentGuides.getState().clear()
|
|
186
|
+
return [gridX, gridZ]
|
|
187
|
+
}
|
|
188
|
+
const ar = resolveAlignment({
|
|
189
|
+
moving: [{ nodeId: '__elevator-draft__', kind: 'corner', x: rawX, z: rawZ }],
|
|
190
|
+
candidates: alignmentCandidates,
|
|
191
|
+
threshold: ALIGNMENT_THRESHOLD_M,
|
|
192
|
+
})
|
|
193
|
+
if (ar.guides.length === 0) {
|
|
194
|
+
useAlignmentGuides.getState().clear()
|
|
195
|
+
return [gridX, gridZ]
|
|
196
|
+
}
|
|
197
|
+
useAlignmentGuides.getState().set(ar.guides)
|
|
198
|
+
if (!applySnap) return [gridX, gridZ]
|
|
199
|
+
let x = gridX
|
|
200
|
+
let z = gridZ
|
|
201
|
+
for (const guide of ar.guides) {
|
|
202
|
+
if (guide.axis === 'x') x = guide.coord
|
|
203
|
+
else z = guide.coord
|
|
204
|
+
}
|
|
205
|
+
return [x, z]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const onGridMove = (event: GridEvent) => {
|
|
209
|
+
// Grid snap follows the global mode (live step so the HUD chip is
|
|
210
|
+
// honest); Off keeps the raw cursor. Shift cycles the mode centrally.
|
|
211
|
+
const step = useEditor.getState().gridSnapStep
|
|
212
|
+
const [gridX, gridZ] = alignPoint(
|
|
213
|
+
isGridSnapActive()
|
|
214
|
+
? Math.round(event.localPosition[0] / step) * step
|
|
215
|
+
: event.localPosition[0],
|
|
216
|
+
isGridSnapActive()
|
|
217
|
+
? Math.round(event.localPosition[2] / step) * step
|
|
218
|
+
: event.localPosition[2],
|
|
219
|
+
event.localPosition[0],
|
|
220
|
+
event.localPosition[2],
|
|
221
|
+
!isAlignmentGuideActive(),
|
|
222
|
+
isMagneticSnapActive(),
|
|
223
|
+
)
|
|
224
|
+
const supportY = resolveElevatorSupportY({
|
|
225
|
+
buildingId: currentBuildingId,
|
|
226
|
+
preferredLevelId: levelId as LevelNode['id'] | null,
|
|
227
|
+
x: gridX,
|
|
228
|
+
z: gridZ,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
cursorRef.current?.position.set(gridX, supportY + GRID_OFFSET, gridZ)
|
|
232
|
+
previewRef.current?.position.set(gridX, supportY + DEFAULT_ELEVATOR_CAB_HEIGHT / 2, gridZ)
|
|
233
|
+
|
|
234
|
+
// Publish the 2D floor-plan ghost at the snapped/aligned cursor.
|
|
235
|
+
usePlacementPreview.getState().set({
|
|
236
|
+
...floorplanPreviewNode,
|
|
237
|
+
position: [gridX, supportY, gridZ],
|
|
238
|
+
rotation: rotationRef.current,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
(isGridSnapActive() || isMagneticSnapActive()) &&
|
|
243
|
+
previousGridPosRef.current &&
|
|
244
|
+
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
245
|
+
) {
|
|
246
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
previousGridPosRef.current = [gridX, gridZ]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const onGridClick = (event: GridEvent) => {
|
|
253
|
+
const latestBuildingId = resolveCurrentBuildingId({
|
|
254
|
+
buildingId,
|
|
255
|
+
levelId,
|
|
256
|
+
nodes: useScene.getState().nodes,
|
|
257
|
+
})
|
|
258
|
+
if (!latestBuildingId) return
|
|
259
|
+
|
|
260
|
+
const step = useEditor.getState().gridSnapStep
|
|
261
|
+
const [gridX, gridZ] = alignPoint(
|
|
262
|
+
isGridSnapActive()
|
|
263
|
+
? Math.round(event.localPosition[0] / step) * step
|
|
264
|
+
: event.localPosition[0],
|
|
265
|
+
isGridSnapActive()
|
|
266
|
+
? Math.round(event.localPosition[2] / step) * step
|
|
267
|
+
: event.localPosition[2],
|
|
268
|
+
event.localPosition[0],
|
|
269
|
+
event.localPosition[2],
|
|
270
|
+
!isAlignmentGuideActive(),
|
|
271
|
+
isMagneticSnapActive(),
|
|
272
|
+
)
|
|
273
|
+
commitElevatorPlacement(
|
|
274
|
+
latestBuildingId,
|
|
275
|
+
levelId,
|
|
276
|
+
gridX,
|
|
277
|
+
gridZ,
|
|
278
|
+
rotationRef.current,
|
|
279
|
+
onPlaced,
|
|
280
|
+
)
|
|
281
|
+
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', levelId)
|
|
282
|
+
useAlignmentGuides.getState().clear()
|
|
283
|
+
// The placed elevator's footprint now renders for real; drop the ghost
|
|
284
|
+
// (the next grid:move re-publishes it for the following placement).
|
|
285
|
+
usePlacementPreview.getState().clear()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
289
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const ROTATION_STEP = Math.PI / 4
|
|
294
|
+
let rotationDelta = 0
|
|
295
|
+
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
|
296
|
+
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
|
297
|
+
|
|
298
|
+
if (rotationDelta !== 0) {
|
|
299
|
+
event.preventDefault()
|
|
300
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
301
|
+
rotationRef.current += rotationDelta
|
|
302
|
+
if (previewRef.current) previewRef.current.rotation.y = rotationRef.current
|
|
303
|
+
// Reflect the rotation in the 2D ghost immediately (no pointer move
|
|
304
|
+
// needed) by republishing at the last snapped cursor position.
|
|
305
|
+
const last = previousGridPosRef.current
|
|
306
|
+
if (last) {
|
|
307
|
+
usePlacementPreview.getState().set({
|
|
308
|
+
...floorplanPreviewNode,
|
|
309
|
+
position: [last[0], 0, last[1]],
|
|
310
|
+
rotation: rotationRef.current,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
emitter.on('grid:move', onGridMove)
|
|
317
|
+
emitter.on('grid:click', onGridClick)
|
|
318
|
+
window.addEventListener('keydown', onKeyDown)
|
|
319
|
+
|
|
320
|
+
return () => {
|
|
321
|
+
emitter.off('grid:move', onGridMove)
|
|
322
|
+
emitter.off('grid:click', onGridClick)
|
|
323
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
324
|
+
useAlignmentGuides.getState().clear()
|
|
325
|
+
usePlacementPreview.getState().clear()
|
|
326
|
+
}
|
|
327
|
+
}, [buildingId, levelId, onPlaced, floorplanPreviewNode])
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<group>
|
|
331
|
+
<CursorSphere ref={cursorRef} />
|
|
332
|
+
<group ref={previewRef}>
|
|
333
|
+
<mesh geometry={previewGeometry}>
|
|
334
|
+
<meshStandardMaterial color="#38bdf8" depthWrite={false} opacity={0.12} transparent />
|
|
335
|
+
</mesh>
|
|
336
|
+
<lineSegments geometry={previewEdgeGeometry}>
|
|
337
|
+
<lineBasicMaterial color="#38bdf8" opacity={0.9} transparent />
|
|
338
|
+
</lineSegments>
|
|
339
|
+
<mesh
|
|
340
|
+
position={[0, -DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + 0.015, 0]}
|
|
341
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
342
|
+
>
|
|
343
|
+
<planeGeometry args={[DEFAULT_ELEVATOR_WIDTH, DEFAULT_ELEVATOR_DEPTH]} />
|
|
344
|
+
<meshBasicMaterial color="#38bdf8" opacity={0.16} transparent />
|
|
345
|
+
</mesh>
|
|
346
|
+
<mesh position={[0, 0, -DEFAULT_ELEVATOR_DEPTH / 2 - 0.01]}>
|
|
347
|
+
<planeGeometry args={[DEFAULT_ELEVATOR_WIDTH, DEFAULT_ELEVATOR_CAB_HEIGHT]} />
|
|
348
|
+
<meshBasicMaterial color="#e0f2fe" opacity={0.18} transparent />
|
|
349
|
+
</mesh>
|
|
350
|
+
<mesh
|
|
351
|
+
position={[
|
|
352
|
+
0,
|
|
353
|
+
-DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + DEFAULT_ELEVATOR_DOOR_HEIGHT / 2,
|
|
354
|
+
-DEFAULT_ELEVATOR_DEPTH / 2 - 0.03,
|
|
355
|
+
]}
|
|
356
|
+
>
|
|
357
|
+
<boxGeometry args={[DEFAULT_ELEVATOR_DOOR_WIDTH, DEFAULT_ELEVATOR_DOOR_HEIGHT, 0.035]} />
|
|
358
|
+
<meshStandardMaterial color="#e5e7eb" depthWrite={false} opacity={0.45} transparent />
|
|
359
|
+
</mesh>
|
|
360
|
+
<mesh
|
|
361
|
+
position={[
|
|
362
|
+
0,
|
|
363
|
+
-DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + DEFAULT_ELEVATOR_DOOR_HEIGHT / 2,
|
|
364
|
+
-DEFAULT_ELEVATOR_DEPTH / 2 - 0.049,
|
|
365
|
+
]}
|
|
366
|
+
>
|
|
367
|
+
<boxGeometry
|
|
368
|
+
args={[DEFAULT_ELEVATOR_DOOR_WIDTH + 0.14, DEFAULT_ELEVATOR_DOOR_HEIGHT + 0.14, 0.01]}
|
|
369
|
+
/>
|
|
370
|
+
<meshBasicMaterial color="#38bdf8" opacity={0.5} transparent />
|
|
371
|
+
</mesh>
|
|
372
|
+
</group>
|
|
373
|
+
</group>
|
|
374
|
+
)
|
|
375
|
+
}
|