@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,263 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
type BuildingNode,
|
|
4
|
+
type ElevatorNode,
|
|
5
|
+
ElevatorNode as ElevatorNodeSchema,
|
|
6
|
+
emitter,
|
|
7
|
+
type GridEvent,
|
|
8
|
+
type LevelNode,
|
|
9
|
+
pauseSceneHistory,
|
|
10
|
+
resumeSceneHistory,
|
|
11
|
+
sceneRegistry,
|
|
12
|
+
useLiveTransforms,
|
|
13
|
+
useScene,
|
|
14
|
+
} from '@pascal-app/core'
|
|
15
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
16
|
+
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
17
|
+
import { resolveElevatorSupportY } from '../../../lib/elevator-support'
|
|
18
|
+
import { consumePlacementDragRelease } from '../../../lib/placement-drag-release'
|
|
19
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
20
|
+
import useEditor from '../../../store/use-editor'
|
|
21
|
+
import { CursorSphere } from '../shared/cursor-sphere'
|
|
22
|
+
|
|
23
|
+
function stripMoveMetadata(metadata: ElevatorNode['metadata']) {
|
|
24
|
+
if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
|
|
25
|
+
return metadata
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const nextMeta = { ...(metadata as Record<string, unknown>) }
|
|
29
|
+
delete nextMeta.isNew
|
|
30
|
+
delete nextMeta.isTransient
|
|
31
|
+
return nextMeta as ElevatorNode['metadata']
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function MoveElevatorTool({
|
|
35
|
+
node: movingNode,
|
|
36
|
+
onCommitted,
|
|
37
|
+
}: {
|
|
38
|
+
node: ElevatorNode
|
|
39
|
+
onCommitted?: (nodeId: AnyNodeId) => void
|
|
40
|
+
}) {
|
|
41
|
+
const onCommittedRef = useRef(onCommitted)
|
|
42
|
+
const historyPausedRef = useRef(false)
|
|
43
|
+
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
44
|
+
const dragAnchorRef = useRef<[number, number] | null>(null)
|
|
45
|
+
const previewPositionRef = useRef<ElevatorNode['position']>([
|
|
46
|
+
movingNode.position[0],
|
|
47
|
+
movingNode.position[1],
|
|
48
|
+
movingNode.position[2],
|
|
49
|
+
])
|
|
50
|
+
const [cursorPosition, setCursorPosition] = useState<[number, number, number]>(() => [
|
|
51
|
+
movingNode.position[0],
|
|
52
|
+
movingNode.position[1],
|
|
53
|
+
movingNode.position[2],
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
const exitMoveMode = useCallback(() => {
|
|
57
|
+
useEditor.getState().setMovingNode(null)
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
onCommittedRef.current = onCommitted
|
|
62
|
+
}, [onCommitted])
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const pauseHistory = () => {
|
|
66
|
+
const temporal = useScene.temporal.getState()
|
|
67
|
+
if (historyPausedRef.current || !temporal.isTracking) return
|
|
68
|
+
pauseSceneHistory(useScene)
|
|
69
|
+
historyPausedRef.current = true
|
|
70
|
+
}
|
|
71
|
+
const resumeHistory = () => {
|
|
72
|
+
if (!historyPausedRef.current) return
|
|
73
|
+
resumeSceneHistory(useScene)
|
|
74
|
+
historyPausedRef.current = false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pauseHistory()
|
|
78
|
+
dragAnchorRef.current = null
|
|
79
|
+
previousGridPosRef.current = null
|
|
80
|
+
const movingNodeId = (movingNode as { id?: ElevatorNode['id'] }).id
|
|
81
|
+
|
|
82
|
+
const meta =
|
|
83
|
+
typeof movingNode.metadata === 'object' && movingNode.metadata !== null
|
|
84
|
+
? (movingNode.metadata as Record<string, unknown>)
|
|
85
|
+
: {}
|
|
86
|
+
const isNew = !!meta.isNew
|
|
87
|
+
const committedMeta = stripMoveMetadata(movingNode.metadata)
|
|
88
|
+
const original = {
|
|
89
|
+
position: [...movingNode.position] as ElevatorNode['position'],
|
|
90
|
+
rotation: movingNode.rotation,
|
|
91
|
+
metadata: movingNode.metadata,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let wasCommitted = false
|
|
95
|
+
let wasCancelled = false
|
|
96
|
+
let pendingRotation = movingNode.rotation
|
|
97
|
+
const supportBuildingId = movingNode.parentId as BuildingNode['id'] | null | undefined
|
|
98
|
+
const supportLevelId = (movingNode.fromLevelId ?? movingNode.defaultLevelId) as
|
|
99
|
+
| LevelNode['id']
|
|
100
|
+
| null
|
|
101
|
+
|
|
102
|
+
const applyPreview = (
|
|
103
|
+
position: ElevatorNode['position'],
|
|
104
|
+
rotation: ElevatorNode['rotation'],
|
|
105
|
+
) => {
|
|
106
|
+
if (movingNodeId) {
|
|
107
|
+
useLiveTransforms.getState().set(movingNodeId, { position, rotation })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const object = movingNodeId ? sceneRegistry.nodes.get(movingNodeId) : null
|
|
111
|
+
if (object) {
|
|
112
|
+
object.position.set(position[0], position[1], position[2])
|
|
113
|
+
object.rotation.y = rotation
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const resetObject = (
|
|
118
|
+
position: ElevatorNode['position'],
|
|
119
|
+
rotation: ElevatorNode['rotation'],
|
|
120
|
+
) => {
|
|
121
|
+
const object = movingNodeId ? sceneRegistry.nodes.get(movingNodeId) : null
|
|
122
|
+
if (object) {
|
|
123
|
+
object.position.set(position[0], position[1], position[2])
|
|
124
|
+
object.rotation.y = rotation
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const clearPreview = () => {
|
|
129
|
+
if (movingNodeId) {
|
|
130
|
+
useLiveTransforms.getState().clear(movingNodeId)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const onGridMove = (event: GridEvent) => {
|
|
135
|
+
const bypassSnap = event.nativeEvent?.shiftKey === true
|
|
136
|
+
const rawX = bypassSnap ? event.localPosition[0] : Math.round(event.localPosition[0] * 2) / 2
|
|
137
|
+
const rawZ = bypassSnap ? event.localPosition[2] : Math.round(event.localPosition[2] * 2) / 2
|
|
138
|
+
const anchor = dragAnchorRef.current ?? [rawX, rawZ]
|
|
139
|
+
dragAnchorRef.current = anchor
|
|
140
|
+
const gridX = movingNode.position[0] + (rawX - anchor[0])
|
|
141
|
+
const gridZ = movingNode.position[2] + (rawZ - anchor[1])
|
|
142
|
+
const supportY = resolveElevatorSupportY({
|
|
143
|
+
buildingId: supportBuildingId,
|
|
144
|
+
preferredLevelId: supportLevelId,
|
|
145
|
+
x: gridX,
|
|
146
|
+
z: gridZ,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
!bypassSnap &&
|
|
151
|
+
previousGridPosRef.current &&
|
|
152
|
+
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
153
|
+
) {
|
|
154
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
previousGridPosRef.current = [gridX, gridZ]
|
|
158
|
+
setCursorPosition([gridX, supportY, gridZ])
|
|
159
|
+
previewPositionRef.current = [gridX, supportY, gridZ]
|
|
160
|
+
applyPreview(previewPositionRef.current, pendingRotation)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const onGridClick = (event: GridEvent) => {
|
|
164
|
+
if (wasCommitted) return
|
|
165
|
+
const nextPosition: ElevatorNode['position'] = [...previewPositionRef.current]
|
|
166
|
+
|
|
167
|
+
wasCommitted = true
|
|
168
|
+
clearPreview()
|
|
169
|
+
resumeHistory()
|
|
170
|
+
if (movingNodeId && useScene.getState().nodes[movingNodeId as AnyNodeId]) {
|
|
171
|
+
useScene.getState().updateNode(movingNodeId as AnyNodeId, {
|
|
172
|
+
position: nextPosition,
|
|
173
|
+
rotation: pendingRotation,
|
|
174
|
+
metadata: committedMeta,
|
|
175
|
+
})
|
|
176
|
+
onCommittedRef.current?.(movingNodeId as AnyNodeId)
|
|
177
|
+
} else if (movingNode.parentId) {
|
|
178
|
+
const elevator = ElevatorNodeSchema.parse({
|
|
179
|
+
...movingNode,
|
|
180
|
+
id: undefined,
|
|
181
|
+
position: nextPosition,
|
|
182
|
+
rotation: pendingRotation,
|
|
183
|
+
metadata: committedMeta,
|
|
184
|
+
})
|
|
185
|
+
useScene.getState().createNode(elevator, movingNode.parentId as AnyNodeId)
|
|
186
|
+
onCommittedRef.current?.(elevator.id as AnyNodeId)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
sfxEmitter.emit('sfx:item-place')
|
|
190
|
+
exitMoveMode()
|
|
191
|
+
event.nativeEvent?.stopPropagation?.()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const onPlacementDragPointerUp = (event: PointerEvent) => {
|
|
195
|
+
if (!consumePlacementDragRelease(event)) return
|
|
196
|
+
onGridClick({ nativeEvent: event } as unknown as GridEvent)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const onCancel = () => {
|
|
200
|
+
wasCancelled = true
|
|
201
|
+
clearPreview()
|
|
202
|
+
if (isNew && movingNodeId) {
|
|
203
|
+
useScene.getState().deleteNode(movingNodeId as AnyNodeId)
|
|
204
|
+
} else {
|
|
205
|
+
if (movingNodeId) {
|
|
206
|
+
useScene.getState().updateNode(movingNodeId as AnyNodeId, {
|
|
207
|
+
position: original.position,
|
|
208
|
+
rotation: original.rotation,
|
|
209
|
+
metadata: original.metadata,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
resetObject(original.position, original.rotation)
|
|
214
|
+
resumeHistory()
|
|
215
|
+
markToolCancelConsumed()
|
|
216
|
+
exitMoveMode()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
220
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const ROTATION_STEP = Math.PI / 4
|
|
225
|
+
let rotationDelta = 0
|
|
226
|
+
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
|
227
|
+
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
|
228
|
+
|
|
229
|
+
if (rotationDelta !== 0) {
|
|
230
|
+
event.preventDefault()
|
|
231
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
232
|
+
pendingRotation += rotationDelta
|
|
233
|
+
applyPreview(previewPositionRef.current, pendingRotation)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
emitter.on('grid:move', onGridMove)
|
|
238
|
+
emitter.on('grid:click', onGridClick)
|
|
239
|
+
emitter.on('tool:cancel', onCancel)
|
|
240
|
+
window.addEventListener('keydown', onKeyDown)
|
|
241
|
+
window.addEventListener('pointerup', onPlacementDragPointerUp)
|
|
242
|
+
|
|
243
|
+
return () => {
|
|
244
|
+
clearPreview()
|
|
245
|
+
if (!(wasCommitted || wasCancelled || isNew) && movingNodeId) {
|
|
246
|
+
useScene.getState().updateNode(movingNodeId as AnyNodeId, {
|
|
247
|
+
position: original.position,
|
|
248
|
+
rotation: original.rotation,
|
|
249
|
+
metadata: original.metadata,
|
|
250
|
+
})
|
|
251
|
+
resetObject(original.position, original.rotation)
|
|
252
|
+
}
|
|
253
|
+
resumeHistory()
|
|
254
|
+
emitter.off('grid:move', onGridMove)
|
|
255
|
+
emitter.off('grid:click', onGridClick)
|
|
256
|
+
emitter.off('tool:cancel', onCancel)
|
|
257
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
258
|
+
window.removeEventListener('pointerup', onPlacementDragPointerUp)
|
|
259
|
+
}
|
|
260
|
+
}, [movingNode, exitMoveMode])
|
|
261
|
+
|
|
262
|
+
return <CursorSphere position={cursorPosition} showTooltip={false} />
|
|
263
|
+
}
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_ANGLE_STEP,
|
|
2
3
|
FenceNode,
|
|
4
|
+
getTwoPointFenceCurveTangents,
|
|
3
5
|
getWallCurveFrameAt,
|
|
4
6
|
getWallCurveLength,
|
|
5
7
|
isCurvedWall,
|
|
8
|
+
snapPointAlongAngleRay,
|
|
6
9
|
useScene,
|
|
7
10
|
type WallNode,
|
|
8
11
|
} from '@pascal-app/core'
|
|
9
12
|
import { useViewer } from '@pascal-app/viewer'
|
|
10
13
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
14
|
+
import useEditor from '../../../store/use-editor'
|
|
11
15
|
import {
|
|
12
16
|
findWallSnapTarget,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
isWallLongEnough,
|
|
16
|
-
snapPointTo45Degrees,
|
|
17
|
+
getSegmentGridStep,
|
|
18
|
+
isSegmentLongEnough,
|
|
17
19
|
snapPointToGrid,
|
|
18
20
|
type WallPlanPoint,
|
|
19
21
|
} from '../wall/wall-drafting'
|
|
20
22
|
|
|
21
23
|
export type FencePlanPoint = WallPlanPoint
|
|
22
24
|
|
|
25
|
+
const FENCE_CORNER_SNAP_RADIUS = 0.28
|
|
26
|
+
const FENCE_SPAN_SNAP_RADIUS = 0.16
|
|
27
|
+
|
|
23
28
|
type SegmentNode = {
|
|
24
29
|
start: FencePlanPoint
|
|
25
30
|
end: FencePlanPoint
|
|
@@ -57,46 +62,68 @@ function findFenceSnapTarget(
|
|
|
57
62
|
fences: FenceNode[],
|
|
58
63
|
ignoreFenceIds: string[] = [],
|
|
59
64
|
): FencePlanPoint | null {
|
|
60
|
-
const
|
|
65
|
+
const cornerRadiusSquared = FENCE_CORNER_SNAP_RADIUS ** 2
|
|
66
|
+
const spanRadiusSquared = FENCE_SPAN_SNAP_RADIUS ** 2
|
|
61
67
|
const ignoredFenceIds = new Set(ignoreFenceIds)
|
|
62
|
-
let
|
|
63
|
-
let
|
|
68
|
+
let bestCornerTarget: FencePlanPoint | null = null
|
|
69
|
+
let bestCornerDistanceSquared = Number.POSITIVE_INFINITY
|
|
70
|
+
let bestSpanTarget: FencePlanPoint | null = null
|
|
71
|
+
let bestSpanDistanceSquared = Number.POSITIVE_INFINITY
|
|
64
72
|
|
|
65
73
|
for (const fence of fences) {
|
|
66
74
|
if (ignoredFenceIds.has(fence.id)) {
|
|
67
75
|
continue
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
const
|
|
78
|
+
for (const candidate of [fence.start, fence.end]) {
|
|
79
|
+
const candidateDistanceSquared = distanceSquared(point, candidate)
|
|
80
|
+
if (
|
|
81
|
+
candidateDistanceSquared > cornerRadiusSquared ||
|
|
82
|
+
candidateDistanceSquared >= bestCornerDistanceSquared
|
|
83
|
+
) {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
bestCornerTarget = candidate
|
|
88
|
+
bestCornerDistanceSquared = candidateDistanceSquared
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
if (isCurvedWall(fence)) {
|
|
72
92
|
const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(fence) / 0.3))
|
|
73
|
-
for (let index =
|
|
93
|
+
for (let index = 1; index < sampleCount; index += 1) {
|
|
74
94
|
const frame = getWallCurveFrameAt(fence, index / sampleCount)
|
|
75
|
-
|
|
95
|
+
const candidate: FencePlanPoint = [frame.point.x, frame.point.y]
|
|
96
|
+
const candidateDistanceSquared = distanceSquared(point, candidate)
|
|
97
|
+
if (
|
|
98
|
+
candidateDistanceSquared > spanRadiusSquared ||
|
|
99
|
+
candidateDistanceSquared >= bestSpanDistanceSquared
|
|
100
|
+
) {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
bestSpanTarget = candidate
|
|
105
|
+
bestSpanDistanceSquared = candidateDistanceSquared
|
|
76
106
|
}
|
|
77
107
|
} else {
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
for (const candidate of candidates) {
|
|
108
|
+
const candidate = projectPointOntoSegment(point, fence)
|
|
82
109
|
if (!candidate) {
|
|
83
110
|
continue
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
const candidateDistanceSquared = distanceSquared(point, candidate)
|
|
87
114
|
if (
|
|
88
|
-
candidateDistanceSquared >
|
|
89
|
-
candidateDistanceSquared >=
|
|
115
|
+
candidateDistanceSquared > spanRadiusSquared ||
|
|
116
|
+
candidateDistanceSquared >= bestSpanDistanceSquared
|
|
90
117
|
) {
|
|
91
118
|
continue
|
|
92
119
|
}
|
|
93
120
|
|
|
94
|
-
|
|
95
|
-
|
|
121
|
+
bestSpanTarget = candidate
|
|
122
|
+
bestSpanDistanceSquared = candidateDistanceSquared
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
125
|
|
|
99
|
-
return
|
|
126
|
+
return bestCornerTarget ?? bestSpanTarget
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
export function snapFenceDraftPoint(args: {
|
|
@@ -106,16 +133,57 @@ export function snapFenceDraftPoint(args: {
|
|
|
106
133
|
start?: FencePlanPoint
|
|
107
134
|
angleSnap?: boolean
|
|
108
135
|
ignoreFenceIds?: string[]
|
|
136
|
+
bypassSnap?: boolean
|
|
137
|
+
magnetic?: boolean
|
|
138
|
+
/** Override the grid step. */
|
|
139
|
+
step?: number
|
|
140
|
+
/**
|
|
141
|
+
* Optional grid-snap function. When provided, replaces the default
|
|
142
|
+
* local-axis snap — lets the 2D floor-plan keep snapping to the
|
|
143
|
+
* world XZ grid even when the building is rotated. Wall / fence
|
|
144
|
+
* endpoint snap precedence is preserved.
|
|
145
|
+
*/
|
|
146
|
+
gridSnap?: (point: FencePlanPoint) => FencePlanPoint
|
|
109
147
|
}): FencePlanPoint {
|
|
110
|
-
const {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
148
|
+
const {
|
|
149
|
+
point,
|
|
150
|
+
walls,
|
|
151
|
+
fences,
|
|
152
|
+
start,
|
|
153
|
+
angleSnap = false,
|
|
154
|
+
ignoreFenceIds,
|
|
155
|
+
bypassSnap = false,
|
|
156
|
+
magnetic = true,
|
|
157
|
+
step,
|
|
158
|
+
gridSnap,
|
|
159
|
+
} = args
|
|
160
|
+
if (bypassSnap) return point
|
|
161
|
+
|
|
162
|
+
const gridStep = step ?? getSegmentGridStep()
|
|
163
|
+
|
|
164
|
+
// Magnetic endpoint snap must beat the angle lock, and the lock can pull
|
|
165
|
+
// the cursor far enough off an endpoint that probing the locked point
|
|
166
|
+
// would never engage — so under the lock, probe from the RAW cursor
|
|
167
|
+
// first (mirrors `snapWallDraftPointDetailed`'s special-point pre-pass).
|
|
168
|
+
if (start && angleSnap) {
|
|
169
|
+
const rawTarget =
|
|
170
|
+
magnetic &&
|
|
171
|
+
(findFenceSnapTarget(point, fences, ignoreFenceIds) ?? findWallSnapTarget(point, walls))
|
|
172
|
+
if (rawTarget) return rawTarget
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The angle path snaps the distance ALONG the 15° ray — a scalar, the
|
|
176
|
+
// same in world and local frames — so the `gridSnap` world-grid override
|
|
177
|
+
// only applies when the angle lock is off.
|
|
178
|
+
const basePoint: FencePlanPoint =
|
|
114
179
|
start && angleSnap
|
|
115
|
-
?
|
|
116
|
-
:
|
|
117
|
-
|
|
180
|
+
? [...snapPointAlongAngleRay(start, point, DEFAULT_ANGLE_STEP, gridStep)]
|
|
181
|
+
: gridSnap
|
|
182
|
+
? gridSnap(point)
|
|
183
|
+
: snapPointToGrid(point, gridStep)
|
|
184
|
+
if (!magnetic) return basePoint
|
|
118
185
|
|
|
186
|
+
const fenceSnapTarget = findFenceSnapTarget(basePoint, fences, ignoreFenceIds)
|
|
119
187
|
return fenceSnapTarget ?? findWallSnapTarget(basePoint, walls) ?? basePoint
|
|
120
188
|
}
|
|
121
189
|
|
|
@@ -126,15 +194,61 @@ export function createFenceOnCurrentLevel(
|
|
|
126
194
|
const currentLevelId = useViewer.getState().selection.levelId
|
|
127
195
|
const { createNode, nodes } = useScene.getState()
|
|
128
196
|
|
|
129
|
-
if (!(currentLevelId &&
|
|
197
|
+
if (!(currentLevelId && isSegmentLongEnough(start, end))) {
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
|
|
202
|
+
// Build parameters seeded by a placed preset (height, style, post
|
|
203
|
+
// spacing, …) merge in first; `name`/`start`/`end` always win. The
|
|
204
|
+
// schema parse validates and drops anything unexpected.
|
|
205
|
+
const defaults = useEditor.getState().toolDefaults.fence ?? {}
|
|
206
|
+
const fence = FenceNode.parse({
|
|
207
|
+
...defaults,
|
|
208
|
+
name: `Fence ${fenceCount + 1}`,
|
|
209
|
+
start,
|
|
210
|
+
end,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
createNode(fence, currentLevelId)
|
|
214
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
215
|
+
|
|
216
|
+
return fence
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Commit a smooth spline fence from a list of drawn control points. The
|
|
221
|
+
* centerline becomes a Catmull-Rom curve through `path`; `start`/`end` are
|
|
222
|
+
* pinned to the first/last point so endpoint handles, bbox, and miter
|
|
223
|
+
* references stay valid. Requires >= 2 points spanning a usable distance.
|
|
224
|
+
*/
|
|
225
|
+
export function createSplineFenceOnCurrentLevel(
|
|
226
|
+
path: FencePlanPoint[],
|
|
227
|
+
tangents = getTwoPointFenceCurveTangents(path),
|
|
228
|
+
): FenceNode | null {
|
|
229
|
+
const currentLevelId = useViewer.getState().selection.levelId
|
|
230
|
+
const { createNode, nodes } = useScene.getState()
|
|
231
|
+
|
|
232
|
+
if (!currentLevelId || path.length < 2) {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
const start = path[0]!
|
|
236
|
+
const end = path[path.length - 1]!
|
|
237
|
+
// A degenerate single-point-ish path (all clicks on one spot) is rejected
|
|
238
|
+
// the same way a too-short straight segment is.
|
|
239
|
+
if (!isSegmentLongEnough(start, end) && path.length < 3) {
|
|
130
240
|
return null
|
|
131
241
|
}
|
|
132
242
|
|
|
133
243
|
const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
|
|
244
|
+
const defaults = useEditor.getState().toolDefaults.fence ?? {}
|
|
134
245
|
const fence = FenceNode.parse({
|
|
246
|
+
...defaults,
|
|
135
247
|
name: `Fence ${fenceCount + 1}`,
|
|
136
248
|
start,
|
|
137
249
|
end,
|
|
250
|
+
path,
|
|
251
|
+
tangents,
|
|
138
252
|
})
|
|
139
253
|
|
|
140
254
|
createNode(fence, currentLevelId)
|
|
@@ -1,112 +1,52 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
RoofNode,
|
|
9
|
-
RoofSegmentNode,
|
|
10
|
-
SlabNode,
|
|
11
|
-
SpawnNode,
|
|
12
|
-
StairNode,
|
|
13
|
-
StairSegmentNode,
|
|
14
|
-
WallNode,
|
|
15
|
-
WindowNode,
|
|
16
|
-
} from '@pascal-app/core'
|
|
17
|
-
import { Vector3 } from 'three'
|
|
18
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
19
|
-
import useEditor from '../../../store/use-editor'
|
|
20
|
-
import { MoveBuildingContent } from '../building/move-building-tool'
|
|
21
|
-
import { MoveCeilingTool } from '../ceiling/move-ceiling-tool'
|
|
22
|
-
import { MoveColumnTool } from '../column/move-column-tool'
|
|
23
|
-
import { MoveDoorTool } from '../door/move-door-tool'
|
|
24
|
-
import { MoveFenceTool } from '../fence/move-fence-tool'
|
|
25
|
-
import { MoveRoofTool } from '../roof/move-roof-tool'
|
|
26
|
-
import { MoveSlabTool } from '../slab/move-slab-tool'
|
|
27
|
-
import { MoveSpawnTool } from '../spawn/move-spawn-tool'
|
|
28
|
-
import { MoveWallTool } from '../wall/move-wall-tool'
|
|
29
|
-
import { MoveWindowTool } from '../window/move-window-tool'
|
|
30
|
-
import type { PlacementState } from './placement-types'
|
|
31
|
-
import { useDraftNode } from './use-draft-node'
|
|
32
|
-
import { usePlacementCoordinator } from './use-placement-coordinator'
|
|
1
|
+
import type { AnyNodeId, ElevatorNode, SpawnNode } from '@pascal-app/core'
|
|
2
|
+
import { nodeRegistry } from '@pascal-app/core'
|
|
3
|
+
import { Suspense } from 'react'
|
|
4
|
+
import { useMovingNode } from '../../../store/use-interaction-scope'
|
|
5
|
+
import { MoveElevatorTool } from '../elevator/move-elevator-tool'
|
|
6
|
+
import { MoveRegistryNodeTool } from '../registry/move-registry-node-tool'
|
|
7
|
+
import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
|
|
33
8
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
9
|
+
/**
|
|
10
|
+
* MoveTool dispatcher. Routes to (in order):
|
|
11
|
+
*
|
|
12
|
+
* 1. `def.affordanceTools.move` — kind-owned move component, lazy-loaded
|
|
13
|
+
* via `getRegistryAffordanceTool`. Covers generic movers
|
|
14
|
+
* (slab / ceiling / wall / fence / column / item / door / window), the
|
|
15
|
+
* bespoke roof / roof-segment / stair / stair-segment / building
|
|
16
|
+
* movers, and the polyline / fitting ghost-placement movers
|
|
17
|
+
* (duct-segment / duct-fitting). A kind that ships its own mover wins
|
|
18
|
+
* even if it also declares `capabilities.movable` (duct-fitting keeps
|
|
19
|
+
* `movable` for the inspector / hint readers but places via its ghost).
|
|
20
|
+
* 2. `MoveRegistryNodeTool` — generic translate-on-XZ for kinds that only
|
|
21
|
+
* declare `capabilities.movable` (shelf, spawn, duct-terminal,
|
|
22
|
+
* hvac-equipment, …).
|
|
23
|
+
* 3. `elevator` is the lone remaining legacy arm — its bespoke cab/shaft
|
|
24
|
+
* mover hasn't been ported to a kind-owned affordance yet.
|
|
25
|
+
*/
|
|
26
|
+
export const MoveTool: React.FC<{
|
|
27
|
+
onNodeMoved?: (nodeId: AnyNodeId) => void
|
|
28
|
+
onSpawnMoved?: (nodeId: SpawnNode['id']) => void
|
|
29
|
+
}> = ({ onNodeMoved }) => {
|
|
30
|
+
const movingNode = useMovingNode()
|
|
47
31
|
|
|
48
|
-
|
|
49
|
-
const draftNode = useDraftNode()
|
|
50
|
-
|
|
51
|
-
const meta =
|
|
52
|
-
typeof movingNode.metadata === 'object' && movingNode.metadata !== null
|
|
53
|
-
? (movingNode.metadata as Record<string, unknown>)
|
|
54
|
-
: {}
|
|
55
|
-
const isNew = !!meta.isNew
|
|
32
|
+
if (!movingNode) return null
|
|
56
33
|
|
|
57
|
-
const
|
|
58
|
-
asset: movingNode.asset,
|
|
59
|
-
draftNode,
|
|
60
|
-
// Duplicates start fresh in floor mode; wall/ceiling draft is created lazily by ensureDraft
|
|
61
|
-
initialState: isNew
|
|
62
|
-
? { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null }
|
|
63
|
-
: getInitialState(movingNode),
|
|
64
|
-
// Preserve the original item's scale so Y-position calculations use the correct height
|
|
65
|
-
defaultScale: isNew ? movingNode.scale : undefined,
|
|
66
|
-
initDraft: (gridPosition) => {
|
|
67
|
-
if (isNew) {
|
|
68
|
-
// Duplicate: use the same create() path as ItemTool so ghost rendering works correctly.
|
|
69
|
-
// Floor items get a draft immediately; wall/ceiling items are created lazily on surface entry.
|
|
70
|
-
gridPosition.copy(new Vector3(...movingNode.position))
|
|
71
|
-
if (!movingNode.asset.attachTo) {
|
|
72
|
-
draftNode.create(gridPosition, movingNode.asset, movingNode.rotation, movingNode.scale)
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
draftNode.adopt(movingNode)
|
|
76
|
-
gridPosition.copy(new Vector3(...movingNode.position))
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
onCommitted: () => {
|
|
80
|
-
sfxEmitter.emit('sfx:item-place')
|
|
81
|
-
useEditor.getState().setMovingNode(null)
|
|
82
|
-
return false
|
|
83
|
-
},
|
|
84
|
-
onCancel: () => {
|
|
85
|
-
draftNode.destroy()
|
|
86
|
-
useEditor.getState().setMovingNode(null)
|
|
87
|
-
},
|
|
88
|
-
})
|
|
34
|
+
const def = nodeRegistry.get(movingNode.type)
|
|
89
35
|
|
|
90
|
-
|
|
91
|
-
|
|
36
|
+
const RegistryMove = getRegistryAffordanceTool(movingNode.type, 'move')
|
|
37
|
+
if (RegistryMove) {
|
|
38
|
+
return (
|
|
39
|
+
<Suspense fallback={null}>
|
|
40
|
+
<RegistryMove node={movingNode} />
|
|
41
|
+
</Suspense>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
92
44
|
|
|
93
|
-
|
|
94
|
-
|
|
45
|
+
if (def?.capabilities?.movable) {
|
|
46
|
+
return <MoveRegistryNodeTool node={movingNode} />
|
|
47
|
+
}
|
|
95
48
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (movingNode.type === 'door') return <MoveDoorTool node={movingNode as DoorNode} />
|
|
100
|
-
if (movingNode.type === 'window') return <MoveWindowTool node={movingNode as WindowNode} />
|
|
101
|
-
if (movingNode.type === 'ceiling') return <MoveCeilingTool node={movingNode as CeilingNode} />
|
|
102
|
-
if (movingNode.type === 'column') return <MoveColumnTool node={movingNode as ColumnNode} />
|
|
103
|
-
if (movingNode.type === 'slab') return <MoveSlabTool node={movingNode as SlabNode} />
|
|
104
|
-
if (movingNode.type === 'wall') return <MoveWallTool node={movingNode as WallNode} />
|
|
105
|
-
if (movingNode.type === 'fence') return <MoveFenceTool node={movingNode as FenceNode} />
|
|
106
|
-
if (movingNode.type === 'roof' || movingNode.type === 'roof-segment')
|
|
107
|
-
return <MoveRoofTool node={movingNode as RoofNode | RoofSegmentNode} />
|
|
108
|
-
if (movingNode.type === 'spawn') return <MoveSpawnTool node={movingNode as SpawnNode} />
|
|
109
|
-
if (movingNode.type === 'stair' || movingNode.type === 'stair-segment')
|
|
110
|
-
return <MoveRoofTool node={movingNode as StairNode | StairSegmentNode} />
|
|
111
|
-
return <MoveItemContent movingNode={movingNode as ItemNode} />
|
|
49
|
+
if (movingNode.type === 'elevator')
|
|
50
|
+
return <MoveElevatorTool node={movingNode as ElevatorNode} onCommitted={onNodeMoved} />
|
|
51
|
+
return null
|
|
112
52
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { getDetachedAttachmentPreviewLift, stripTransient } from './placement-math'
|
|
3
|
+
|
|
4
|
+
describe('stripTransient', () => {
|
|
5
|
+
test('removes placement-only metadata flags before commit', () => {
|
|
6
|
+
expect(stripTransient({ isNew: true, isTransient: true, label: 'copy' })).toEqual({
|
|
7
|
+
label: 'copy',
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('getDetachedAttachmentPreviewLift', () => {
|
|
13
|
+
test('raises attach-only item previews while they are detached from their host', () => {
|
|
14
|
+
expect(getDetachedAttachmentPreviewLift('wall')).toBeGreaterThan(0)
|
|
15
|
+
expect(getDetachedAttachmentPreviewLift('wall-side')).toBeGreaterThan(0)
|
|
16
|
+
expect(getDetachedAttachmentPreviewLift('ceiling')).toBeGreaterThan(0)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('keeps floor item previews on the floor', () => {
|
|
20
|
+
expect(getDetachedAttachmentPreviewLift(undefined)).toBe(0)
|
|
21
|
+
})
|
|
22
|
+
})
|