@pascal-app/editor 0.8.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { type AssetInput, isObject } from '@pascal-app/core'
|
|
2
|
-
import
|
|
2
|
+
import { Euler, Matrix3, type Matrix4, Quaternion, Vector3 } from 'three'
|
|
3
|
+
import { resolveSnapFlags } from '../../../lib/snapping-mode'
|
|
4
|
+
import useEditor, { getActiveSnappingMode } from '../../../store/use-editor'
|
|
3
5
|
|
|
6
|
+
// Sentinel returned when the active context's snapping mode disables grid snap.
|
|
7
|
+
// The snap helpers below treat any `step <= 0` as "no grid snap" and pass the
|
|
8
|
+
// raw value through. For items the default mode is now `lines` (grid off), so
|
|
9
|
+
// item placement/move is free + line-snap unless the user opts into `grid`.
|
|
4
10
|
function getGridSnapStep(): number {
|
|
5
|
-
return useEditor.getState().gridSnapStep
|
|
11
|
+
return resolveSnapFlags(getActiveSnappingMode()).grid ? useEditor.getState().gridSnapStep : 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ROTATION_QUANTUM = Math.PI / 4
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* R/T rotation: round the current angle to the nearest 45° then step ONE
|
|
18
|
+
* increment in `direction` (+1 / -1), so the node always lands on a clean 45°
|
|
19
|
+
* multiple regardless of its starting angle (12° → 45°, 40° → 90°) rather than a
|
|
20
|
+
* blind ±45° from an arbitrary angle.
|
|
21
|
+
*/
|
|
22
|
+
export function steppedRotation(current: number, direction: 1 | -1): number {
|
|
23
|
+
return (Math.round(current / ROTATION_QUANTUM) + direction) * ROTATION_QUANTUM
|
|
6
24
|
}
|
|
7
25
|
|
|
8
26
|
function positiveModulo(value: number, divisor: number): number {
|
|
@@ -13,6 +31,7 @@ function positiveModulo(value: number, divisor: number): number {
|
|
|
13
31
|
* Snaps a position to the active grid step, aligning item edges to grid lines.
|
|
14
32
|
*/
|
|
15
33
|
export function snapToGrid(position: number, dimension: number, step = getGridSnapStep()): number {
|
|
34
|
+
if (step <= 0) return position
|
|
16
35
|
const halfDim = dimension / 2
|
|
17
36
|
const offset = positiveModulo(halfDim, step)
|
|
18
37
|
return Math.round((position - offset) / step) * step + offset
|
|
@@ -22,6 +41,7 @@ export function snapToGrid(position: number, dimension: number, step = getGridSn
|
|
|
22
41
|
* Snap a value to the active grid step (used for wall-local positions).
|
|
23
42
|
*/
|
|
24
43
|
export function snapToHalf(value: number, step = getGridSnapStep()): number {
|
|
44
|
+
if (step <= 0) return value
|
|
25
45
|
return Math.round(value / step) * step
|
|
26
46
|
}
|
|
27
47
|
|
|
@@ -29,6 +49,7 @@ export function snapToHalf(value: number, step = getGridSnapStep()): number {
|
|
|
29
49
|
* Round a value up to the next multiple of `step`, with a minimum of `step`.
|
|
30
50
|
*/
|
|
31
51
|
export function snapUpToGridStep(value: number, step = getGridSnapStep()): number {
|
|
52
|
+
if (step <= 0) return value
|
|
32
53
|
return Math.max(step, Math.ceil(value / step) * step)
|
|
33
54
|
}
|
|
34
55
|
|
|
@@ -54,6 +75,12 @@ export function getGridAlignedDimensions(
|
|
|
54
75
|
return [snapUpToGridStep(w, step), h, snapUpToGridStep(d, step)]
|
|
55
76
|
}
|
|
56
77
|
|
|
78
|
+
export function getDetachedAttachmentPreviewLift(
|
|
79
|
+
attachTo: AssetInput['attachTo'] | null | undefined,
|
|
80
|
+
): number {
|
|
81
|
+
return attachTo ? 0.45 : 0
|
|
82
|
+
}
|
|
83
|
+
|
|
57
84
|
/**
|
|
58
85
|
* Calculate cursor rotation in WORLD space from wall normal and orientation.
|
|
59
86
|
*/
|
|
@@ -110,11 +137,36 @@ export function isValidWallSideFace(normal: [number, number, number] | undefined
|
|
|
110
137
|
return Math.abs(normal[2]) > 0.7
|
|
111
138
|
}
|
|
112
139
|
|
|
113
|
-
/**
|
|
114
|
-
* Strip the `isTransient` flag from node metadata before committing.
|
|
115
|
-
*/
|
|
140
|
+
/** Strip placement-only metadata flags before committing a draft. */
|
|
116
141
|
export function stripTransient(meta: any): any {
|
|
117
142
|
if (!isObject(meta)) return meta
|
|
118
|
-
const {
|
|
119
|
-
|
|
143
|
+
const nextMeta = { ...(meta as Record<string, any>) }
|
|
144
|
+
delete nextMeta.isNew
|
|
145
|
+
delete nextMeta.isTransient
|
|
146
|
+
return nextMeta
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const _up = new Vector3(0, 1, 0)
|
|
150
|
+
const _normal = new Vector3()
|
|
151
|
+
const _quat = new Quaternion()
|
|
152
|
+
const _euler = new Euler()
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Compute euler rotation that tilts an item so its local +Y aligns with a
|
|
156
|
+
* roof surface normal. The normal is in the hit mesh's local space and is
|
|
157
|
+
* transformed to world space via the mesh's matrixWorld.
|
|
158
|
+
*/
|
|
159
|
+
export function calculateRoofRotation(
|
|
160
|
+
normal: [number, number, number] | undefined,
|
|
161
|
+
objectMatrixWorld: Matrix4,
|
|
162
|
+
): [number, number, number] {
|
|
163
|
+
if (!normal) return [0, 0, 0]
|
|
164
|
+
|
|
165
|
+
_normal.set(normal[0], normal[1], normal[2])
|
|
166
|
+
_normal.applyNormalMatrix(new Matrix3().getNormalMatrix(objectMatrixWorld)).normalize()
|
|
167
|
+
|
|
168
|
+
_quat.setFromUnitVectors(_up, _normal)
|
|
169
|
+
_euler.setFromQuaternion(_quat, 'XYZ')
|
|
170
|
+
|
|
171
|
+
return [_euler.x, _euler.y, _euler.z]
|
|
120
172
|
}
|
|
@@ -6,16 +6,29 @@ import type {
|
|
|
6
6
|
GridEvent,
|
|
7
7
|
ItemEvent,
|
|
8
8
|
ItemNode,
|
|
9
|
+
RoofEvent,
|
|
10
|
+
RoofNode,
|
|
11
|
+
RoofSegmentNode,
|
|
12
|
+
RoofWallFaceId,
|
|
13
|
+
ShelfEvent,
|
|
14
|
+
ShelfNode,
|
|
9
15
|
WallEvent,
|
|
10
16
|
WallNode,
|
|
11
17
|
} from '@pascal-app/core'
|
|
12
18
|
import {
|
|
19
|
+
canHostOnTop,
|
|
20
|
+
clampRectToRoofWallFace,
|
|
21
|
+
getRoofSegmentWallFace,
|
|
13
22
|
getScaledDimensions,
|
|
14
23
|
isLowProfileItemSurface,
|
|
24
|
+
nodeRegistry,
|
|
25
|
+
roofFacePointToSegment,
|
|
15
26
|
sceneRegistry,
|
|
16
27
|
useScene,
|
|
17
28
|
} from '@pascal-app/core'
|
|
18
29
|
import { Euler, Matrix3, Quaternion, Vector3 } from 'three'
|
|
30
|
+
import { hasRoofFaceChildOverlap, resolveRoofWallHit } from '../../../lib/roof-wall-hit'
|
|
31
|
+
import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
|
|
19
32
|
import {
|
|
20
33
|
calculateCursorRotation,
|
|
21
34
|
calculateItemRotation,
|
|
@@ -52,6 +65,7 @@ function isUpwardItemSurfaceHit(event: ItemEvent): boolean {
|
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
function getSurfacePlacementHeight(surfaceItem: ItemNode, event: ItemEvent, localPos: Vector3) {
|
|
68
|
+
if (!canHostOnTop(surfaceItem)) return null
|
|
55
69
|
if (isLowProfileItemSurface(surfaceItem)) return null
|
|
56
70
|
if (!isUpwardItemSurfaceHit(event)) return null
|
|
57
71
|
|
|
@@ -96,16 +110,25 @@ export const floorStrategy = {
|
|
|
96
110
|
const [dimX, , dimZ] = dims
|
|
97
111
|
const rotY = ctx.draftItem?.rotation?.[1] ?? 0
|
|
98
112
|
const swapDims = Math.abs(Math.sin(rotY)) > 0.9
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
// Snap on the world XZ grid (the grid the editor renders) so the
|
|
114
|
+
// item edges land on the visible grid even when the active building
|
|
115
|
+
// is rotated; then project the world point back into building-local
|
|
116
|
+
// for storage. Without this, a rotated building drags placement off
|
|
117
|
+
// the world grid.
|
|
118
|
+
// Snapping is governed by the active mode (snapToGrid returns raw in Off /
|
|
119
|
+
// non-grid modes); Alt is force-place only and never bypasses snapping here.
|
|
120
|
+
const [x, z] = snapWorldXZForActiveBuilding(
|
|
121
|
+
snapToGrid(event.position[0], swapDims ? dimZ : dimX),
|
|
122
|
+
snapToGrid(event.position[2], swapDims ? dimX : dimZ),
|
|
123
|
+
0,
|
|
124
|
+
).local
|
|
125
|
+
const y = ctx.gridPosition.y
|
|
103
126
|
|
|
104
127
|
return {
|
|
105
|
-
gridPosition: [x,
|
|
106
|
-
cursorPosition: [x,
|
|
107
|
-
cursorRotationY:
|
|
108
|
-
nodeUpdate: { position: [x,
|
|
128
|
+
gridPosition: [x, y, z],
|
|
129
|
+
cursorPosition: [x, y, z],
|
|
130
|
+
cursorRotationY: rotY,
|
|
131
|
+
nodeUpdate: { position: [x, y, z] },
|
|
109
132
|
stopPropagation: false,
|
|
110
133
|
dirtyNodeId: null,
|
|
111
134
|
}
|
|
@@ -122,8 +145,13 @@ export const floorStrategy = {
|
|
|
122
145
|
): CommitResult | null {
|
|
123
146
|
if (ctx.state.surface !== 'floor') return null
|
|
124
147
|
if (!(ctx.levelId && ctx.draftItem)) return null
|
|
148
|
+
if (ctx.draftItem.asset.attachTo) return null
|
|
125
149
|
|
|
126
|
-
const pos: [number, number, number] = [
|
|
150
|
+
const pos: [number, number, number] = [
|
|
151
|
+
ctx.gridPosition.x,
|
|
152
|
+
ctx.gridPosition.y,
|
|
153
|
+
ctx.gridPosition.z,
|
|
154
|
+
]
|
|
127
155
|
const valid = validators.canPlaceOnFloor(
|
|
128
156
|
ctx.levelId,
|
|
129
157
|
pos,
|
|
@@ -197,10 +225,13 @@ export const wallStrategy = {
|
|
|
197
225
|
const adjustedY = validation.adjustedY ?? y
|
|
198
226
|
|
|
199
227
|
return {
|
|
200
|
-
stateUpdate: { surface: 'wall', wallId: event.node.id },
|
|
228
|
+
stateUpdate: { surface: 'wall', wallId: event.node.id, roofSegmentId: null },
|
|
201
229
|
nodeUpdate: {
|
|
202
230
|
position: [x, adjustedY, z],
|
|
203
231
|
parentId: event.node.id,
|
|
232
|
+
// The draft may arrive from a roof-segment wall face.
|
|
233
|
+
roofSegmentId: undefined,
|
|
234
|
+
roofFace: undefined,
|
|
204
235
|
side,
|
|
205
236
|
rotation: [0, itemRotation, 0],
|
|
206
237
|
},
|
|
@@ -299,6 +330,8 @@ export const wallStrategy = {
|
|
|
299
330
|
nodeUpdate: {
|
|
300
331
|
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
301
332
|
parentId: event.node.id,
|
|
333
|
+
roofSegmentId: undefined,
|
|
334
|
+
roofFace: undefined,
|
|
302
335
|
side: ctx.draftItem.side,
|
|
303
336
|
rotation: ctx.draftItem.rotation,
|
|
304
337
|
metadata: stripTransient(ctx.draftItem.metadata),
|
|
@@ -328,6 +361,225 @@ export const wallStrategy = {
|
|
|
328
361
|
},
|
|
329
362
|
}
|
|
330
363
|
|
|
364
|
+
// ============================================================================
|
|
365
|
+
// ROOF WALL STRATEGY
|
|
366
|
+
// ============================================================================
|
|
367
|
+
|
|
368
|
+
type RoofWallTarget = {
|
|
369
|
+
segment: RoofSegmentNode
|
|
370
|
+
faceId: RoofWallFaceId
|
|
371
|
+
faceYaw: number
|
|
372
|
+
/** Stored node position: FACE-LOCAL, y = bottom edge. */
|
|
373
|
+
position: [number, number, number]
|
|
374
|
+
/** Face-coord center of the placed rect (for the overlap guard). */
|
|
375
|
+
centerU: number
|
|
376
|
+
centerV: number
|
|
377
|
+
width: number
|
|
378
|
+
height: number
|
|
379
|
+
cursorPosition: [number, number, number]
|
|
380
|
+
cursorRotationY: number
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Resolve a roof pointer event to an item placement on a segment wall
|
|
385
|
+
* face. Items snap u / bottom-v to the 0.5m grid, then the rect is
|
|
386
|
+
* clamped inside the face profile (sliding under the gable slopes).
|
|
387
|
+
* Position frame matches wall hosting: y anchors the BOTTOM edge;
|
|
388
|
+
* `wall-side` items mount on the outer surface, `wall` items center in
|
|
389
|
+
* the wall thickness.
|
|
390
|
+
*
|
|
391
|
+
* `freePlace` mirrors the wall flow's Alt override (stubbed
|
|
392
|
+
* validators): the profile clamp is skipped, so the rect may overhang
|
|
393
|
+
* the face edges — placement follows the snapped cursor as-is.
|
|
394
|
+
*/
|
|
395
|
+
function resolveRoofWallTarget(
|
|
396
|
+
ctx: PlacementContext,
|
|
397
|
+
event: RoofEvent,
|
|
398
|
+
freePlace = false,
|
|
399
|
+
): RoofWallTarget | null {
|
|
400
|
+
const attachTo = ctx.asset.attachTo
|
|
401
|
+
if (attachTo !== 'wall' && attachTo !== 'wall-side') return null
|
|
402
|
+
|
|
403
|
+
const hit = resolveRoofWallHit(event.node as RoofNode, event.position, event.normal, event.object)
|
|
404
|
+
if (!hit) return null
|
|
405
|
+
|
|
406
|
+
const rawDims = ctx.draftItem
|
|
407
|
+
? getScaledDimensions(ctx.draftItem)
|
|
408
|
+
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
409
|
+
const dims = getGridAlignedDimensions(rawDims, attachTo)
|
|
410
|
+
const [width, height] = dims
|
|
411
|
+
|
|
412
|
+
// Snap follows the active mode (snapToHalf returns raw in Off/non-grid);
|
|
413
|
+
// `freePlace` (Alt) is force-place — it only skips the face-fit validity gate.
|
|
414
|
+
const u = snapToHalf(hit.u)
|
|
415
|
+
const centerV = snapToHalf(hit.v) + height / 2
|
|
416
|
+
const fitted = freePlace ? null : clampRectToRoofWallFace(hit.face, u, centerV, width, height)
|
|
417
|
+
if (!fitted && !freePlace) return null
|
|
418
|
+
const finalU = fitted?.u ?? u
|
|
419
|
+
const finalV = fitted?.v ?? centerV
|
|
420
|
+
|
|
421
|
+
// FACE-LOCAL storage (z = 0 → wall mid-plane; ItemSystem pushes
|
|
422
|
+
// wall-side items to the outer surface, exactly like wall hosting).
|
|
423
|
+
// The renderer mounts the node inside the live face frame, so items
|
|
424
|
+
// track segment resizes without any re-anchoring.
|
|
425
|
+
const position: [number, number, number] = [finalU, finalV - height / 2, 0]
|
|
426
|
+
|
|
427
|
+
const segObj = sceneRegistry.nodes.get(hit.segment.id)
|
|
428
|
+
if (!segObj) return null
|
|
429
|
+
segObj.updateWorldMatrix(true, false)
|
|
430
|
+
const segLocal = roofFacePointToSegment(hit.segment, hit.face.id, position)
|
|
431
|
+
const worldPos = segObj.localToWorld(new Vector3(segLocal[0], segLocal[1], segLocal[2]))
|
|
432
|
+
|
|
433
|
+
const nodes = useScene.getState().nodes
|
|
434
|
+
const roof = hit.segment.parentId
|
|
435
|
+
? (nodes[hit.segment.parentId as AnyNodeId] as RoofNode | undefined)
|
|
436
|
+
: undefined
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
segment: hit.segment,
|
|
440
|
+
faceId: hit.face.id,
|
|
441
|
+
faceYaw: hit.face.yaw,
|
|
442
|
+
position,
|
|
443
|
+
centerU: finalU,
|
|
444
|
+
centerV: finalV,
|
|
445
|
+
width,
|
|
446
|
+
height,
|
|
447
|
+
cursorPosition: [worldPos.x, worldPos.y, worldPos.z],
|
|
448
|
+
cursorRotationY: (roof?.rotation ?? 0) + (hit.segment.rotation ?? 0) + hit.face.yaw,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Validation half of `checkCanPlace` for the roof-wall surface. */
|
|
453
|
+
function canPlaceOnRoofWall(ctx: PlacementContext): boolean {
|
|
454
|
+
const segmentId = ctx.state.roofSegmentId
|
|
455
|
+
if (!(segmentId && ctx.draftItem)) return false
|
|
456
|
+
const segment = useScene.getState().nodes[segmentId as AnyNodeId] as RoofSegmentNode | undefined
|
|
457
|
+
if (segment?.type !== 'roof-segment') return false
|
|
458
|
+
const faceId = ctx.draftItem.roofFace
|
|
459
|
+
if (!faceId) return false
|
|
460
|
+
const face = getRoofSegmentWallFace(segment, faceId)
|
|
461
|
+
|
|
462
|
+
const dims = getGridAlignedDimensions(
|
|
463
|
+
getScaledDimensions(ctx.draftItem),
|
|
464
|
+
ctx.draftItem.asset.attachTo,
|
|
465
|
+
)
|
|
466
|
+
const [width, height] = dims
|
|
467
|
+
// gridPosition carries the stored FACE-LOCAL coords (u, bottom-v, z).
|
|
468
|
+
const u = ctx.gridPosition.x
|
|
469
|
+
const centerV = ctx.gridPosition.y + height / 2
|
|
470
|
+
const clamped = clampRectToRoofWallFace(face, u, centerV, width, height)
|
|
471
|
+
if (!clamped || Math.abs(clamped.u - u) > 1e-3 || Math.abs(clamped.v - centerV) > 1e-3) {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
return !hasRoofFaceChildOverlap(segment, faceId, u, centerV, width, height, ctx.draftItem.id)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export const roofWallStrategy = {
|
|
478
|
+
/**
|
|
479
|
+
* Handle roof:enter / first hover — transition onto a segment wall
|
|
480
|
+
* face. Returns null when the item doesn't wall-attach or the pointer
|
|
481
|
+
* isn't over a placeable face.
|
|
482
|
+
*/
|
|
483
|
+
enter(ctx: PlacementContext, event: RoofEvent, freePlace = false): TransitionResult | null {
|
|
484
|
+
const target = resolveRoofWallTarget(ctx, event, freePlace)
|
|
485
|
+
if (!target) return null
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
stateUpdate: { surface: 'roof-wall', roofSegmentId: target.segment.id, wallId: null },
|
|
489
|
+
nodeUpdate: {
|
|
490
|
+
position: target.position,
|
|
491
|
+
parentId: target.segment.id,
|
|
492
|
+
roofSegmentId: target.segment.id,
|
|
493
|
+
roofFace: target.faceId,
|
|
494
|
+
wallId: undefined,
|
|
495
|
+
side: 'front',
|
|
496
|
+
rotation: [0, 0, 0],
|
|
497
|
+
},
|
|
498
|
+
cursorRotationY: target.cursorRotationY,
|
|
499
|
+
gridPosition: target.position,
|
|
500
|
+
cursorPosition: target.cursorPosition,
|
|
501
|
+
stopPropagation: true,
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Handle roof:move while on a segment wall face. Returns null when the
|
|
507
|
+
* pointer resolves to a DIFFERENT segment (the coordinator re-enters —
|
|
508
|
+
* segment transitions inside one roof never re-fire roof:enter) or to
|
|
509
|
+
* no placeable face.
|
|
510
|
+
*/
|
|
511
|
+
move(ctx: PlacementContext, event: RoofEvent, freePlace = false): PlacementResult | null {
|
|
512
|
+
if (ctx.state.surface !== 'roof-wall') return null
|
|
513
|
+
if (!ctx.draftItem) return null
|
|
514
|
+
|
|
515
|
+
const target = resolveRoofWallTarget(ctx, event, freePlace)
|
|
516
|
+
if (!target) return null
|
|
517
|
+
if (target.segment.id !== ctx.state.roofSegmentId) return null
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
gridPosition: target.position,
|
|
521
|
+
cursorPosition: target.cursorPosition,
|
|
522
|
+
cursorRotationY: target.cursorRotationY,
|
|
523
|
+
nodeUpdate: {
|
|
524
|
+
position: target.position,
|
|
525
|
+
side: 'front',
|
|
526
|
+
rotation: [0, 0, 0],
|
|
527
|
+
roofFace: target.faceId,
|
|
528
|
+
},
|
|
529
|
+
stopPropagation: true,
|
|
530
|
+
// Items don't cut the roof — no geometry rebuild needed.
|
|
531
|
+
dirtyNodeId: null,
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Handle roof:click — commit placement on the segment wall face.
|
|
537
|
+
*/
|
|
538
|
+
click(ctx: PlacementContext, _event: RoofEvent, freePlace = false): CommitResult | null {
|
|
539
|
+
if (ctx.state.surface !== 'roof-wall') return null
|
|
540
|
+
if (!(ctx.draftItem && ctx.state.roofSegmentId)) return null
|
|
541
|
+
// Alt mirrors the wall flow's stubbed validators: skip profile-fit
|
|
542
|
+
// and overlap checks entirely.
|
|
543
|
+
if (!freePlace && !canPlaceOnRoofWall(ctx)) return null
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
nodeUpdate: {
|
|
547
|
+
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
548
|
+
parentId: ctx.state.roofSegmentId,
|
|
549
|
+
roofSegmentId: ctx.state.roofSegmentId,
|
|
550
|
+
roofFace: ctx.draftItem.roofFace,
|
|
551
|
+
wallId: undefined,
|
|
552
|
+
side: 'front',
|
|
553
|
+
rotation: [0, 0, 0],
|
|
554
|
+
metadata: stripTransient(ctx.draftItem.metadata),
|
|
555
|
+
},
|
|
556
|
+
stopPropagation: true,
|
|
557
|
+
dirtyNodeId: null,
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Handle roof:leave — transition back to floor surface.
|
|
563
|
+
*/
|
|
564
|
+
leave(ctx: PlacementContext): TransitionResult | null {
|
|
565
|
+
if (ctx.state.surface !== 'roof-wall') return null
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
stateUpdate: { surface: 'floor', roofSegmentId: null },
|
|
569
|
+
nodeUpdate: {
|
|
570
|
+
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
571
|
+
parentId: ctx.levelId,
|
|
572
|
+
roofSegmentId: undefined,
|
|
573
|
+
roofFace: undefined,
|
|
574
|
+
},
|
|
575
|
+
cursorRotationY: 0,
|
|
576
|
+
gridPosition: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
577
|
+
cursorPosition: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
578
|
+
stopPropagation: true,
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
}
|
|
582
|
+
|
|
331
583
|
// ============================================================================
|
|
332
584
|
// CEILING STRATEGY
|
|
333
585
|
// ============================================================================
|
|
@@ -362,16 +614,19 @@ export const ceilingStrategy = {
|
|
|
362
614
|
// use the ceiling hit's local position rather than world position.
|
|
363
615
|
const x = snapToGrid(event.localPosition[0], swapDims ? dimZ : dimX)
|
|
364
616
|
const z = snapToGrid(event.localPosition[2], swapDims ? dimX : dimZ)
|
|
365
|
-
|
|
617
|
+
// Recessed fixtures seat flush with the ceiling plane (body rising into the
|
|
618
|
+
// void above); everything else hangs its full height below the ceiling.
|
|
619
|
+
const seatY = ctx.asset.recessed ? 0 : -itemHeight
|
|
620
|
+
const worldSnapped = event.object.localToWorld(new Vector3(x, seatY, z))
|
|
366
621
|
|
|
367
622
|
return {
|
|
368
623
|
stateUpdate: { surface: 'ceiling', ceilingId: event.node.id },
|
|
369
624
|
nodeUpdate: {
|
|
370
|
-
position: [x,
|
|
625
|
+
position: [x, seatY, z],
|
|
371
626
|
parentId: event.node.id,
|
|
372
627
|
},
|
|
373
628
|
cursorRotationY: 0,
|
|
374
|
-
gridPosition: [x,
|
|
629
|
+
gridPosition: [x, seatY, z],
|
|
375
630
|
cursorPosition: [worldSnapped.x, worldSnapped.y, worldSnapped.z],
|
|
376
631
|
stopPropagation: true,
|
|
377
632
|
}
|
|
@@ -393,10 +648,13 @@ export const ceilingStrategy = {
|
|
|
393
648
|
|
|
394
649
|
const x = snapToGrid(event.localPosition[0], swapDims ? dimZ : dimX)
|
|
395
650
|
const z = snapToGrid(event.localPosition[2], swapDims ? dimX : dimZ)
|
|
396
|
-
|
|
651
|
+
// Recessed fixtures seat flush with the ceiling plane (body rising into the
|
|
652
|
+
// void above); everything else hangs its full height below the ceiling.
|
|
653
|
+
const seatY = ctx.draftItem.asset.recessed ? 0 : -itemHeight
|
|
654
|
+
const worldSnapped = event.object.localToWorld(new Vector3(x, seatY, z))
|
|
397
655
|
|
|
398
656
|
return {
|
|
399
|
-
gridPosition: [x,
|
|
657
|
+
gridPosition: [x, seatY, z],
|
|
400
658
|
cursorPosition: [worldSnapped.x, worldSnapped.y, worldSnapped.z],
|
|
401
659
|
cursorRotationY: 0,
|
|
402
660
|
nodeUpdate: null,
|
|
@@ -587,6 +845,156 @@ export const itemSurfaceStrategy = {
|
|
|
587
845
|
},
|
|
588
846
|
}
|
|
589
847
|
|
|
848
|
+
// ============================================================================
|
|
849
|
+
// SHELF SURFACE STRATEGY
|
|
850
|
+
// ============================================================================
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Resolve the row Y closest to the cursor's local Y. Reads candidate row
|
|
854
|
+
* positions from the kind's `capabilities.surfaces.custom` — the shelf
|
|
855
|
+
* declaration emits one `SurfacePoint` per board's top surface. The
|
|
856
|
+
* strategy stays kind-agnostic at this level: any future "multi-board"
|
|
857
|
+
* kind that declares `surfaces.custom` with upward normals gets the
|
|
858
|
+
* same hit behaviour for free.
|
|
859
|
+
*/
|
|
860
|
+
function getShelfRowSurfaceY(shelfNode: ShelfNode, localY: number): number | null {
|
|
861
|
+
const def = nodeRegistry.get('shelf')
|
|
862
|
+
const custom = def?.capabilities?.surfaces?.custom
|
|
863
|
+
if (!custom) return null
|
|
864
|
+
const candidates = custom(shelfNode as AnyNode)
|
|
865
|
+
if (candidates.length === 0) return null
|
|
866
|
+
let best = candidates[0]
|
|
867
|
+
let bestDist = Math.abs(best!.position[1] - localY)
|
|
868
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
869
|
+
const c = candidates[i]
|
|
870
|
+
if (!c) continue
|
|
871
|
+
const dist = Math.abs(c.position[1] - localY)
|
|
872
|
+
if (dist < bestDist) {
|
|
873
|
+
best = c
|
|
874
|
+
bestDist = dist
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return best?.position[1] ?? null
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export const shelfSurfaceStrategy = {
|
|
881
|
+
/**
|
|
882
|
+
* Handle shelf:enter — transition the draft onto the closest shelf
|
|
883
|
+
* row. Mirrors `itemSurfaceStrategy.enter` but reads candidate
|
|
884
|
+
* surface heights from the shelf kind's `surfaces.custom` (one Y per
|
|
885
|
+
* board) instead of `asset.surface.height`. Picks the row whose
|
|
886
|
+
* surface Y is nearest the cursor's local Y so the user can target a
|
|
887
|
+
* specific row by hovering near it.
|
|
888
|
+
*/
|
|
889
|
+
enter(ctx: PlacementContext, event: ShelfEvent): TransitionResult | null {
|
|
890
|
+
if (ctx.asset.attachTo) return null
|
|
891
|
+
const shelfNode = event.node as ShelfNode
|
|
892
|
+
|
|
893
|
+
if (ctx.state.surface === 'shelf-surface' && ctx.state.shelfId === shelfNode.id) {
|
|
894
|
+
return null
|
|
895
|
+
}
|
|
896
|
+
if (!isUpwardShelfSurfaceHit(event)) return null
|
|
897
|
+
|
|
898
|
+
// Size check: draft footprint must fit on the shelf board (width × depth).
|
|
899
|
+
const ourDims = ctx.draftItem
|
|
900
|
+
? getScaledDimensions(ctx.draftItem)
|
|
901
|
+
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
902
|
+
if (ourDims[0] > shelfNode.width || ourDims[2] > shelfNode.depth) return null
|
|
903
|
+
|
|
904
|
+
const shelfMesh = sceneRegistry.nodes.get(shelfNode.id)
|
|
905
|
+
if (!shelfMesh) return null
|
|
906
|
+
|
|
907
|
+
const worldPos = new Vector3(event.position[0], event.position[1], event.position[2])
|
|
908
|
+
const localPos = shelfMesh.worldToLocal(worldPos)
|
|
909
|
+
const rowY = getShelfRowSurfaceY(shelfNode, localPos.y)
|
|
910
|
+
if (rowY === null) return null
|
|
911
|
+
|
|
912
|
+
const x = snapToGrid(localPos.x, ourDims[0])
|
|
913
|
+
const z = snapToGrid(localPos.z, ourDims[2])
|
|
914
|
+
|
|
915
|
+
const worldSnapped = shelfMesh.localToWorld(new Vector3(x, rowY, z))
|
|
916
|
+
|
|
917
|
+
const surfaceQuat = new Quaternion()
|
|
918
|
+
shelfMesh.getWorldQuaternion(surfaceQuat)
|
|
919
|
+
const surfaceWorldY = new Euler().setFromQuaternion(surfaceQuat, 'YXZ').y
|
|
920
|
+
const localRotationY = ctx.currentCursorRotationY - surfaceWorldY
|
|
921
|
+
const draftRotation = ctx.draftItem?.rotation ?? [0, 0, 0]
|
|
922
|
+
|
|
923
|
+
return {
|
|
924
|
+
stateUpdate: { surface: 'shelf-surface', shelfId: shelfNode.id },
|
|
925
|
+
nodeUpdate: {
|
|
926
|
+
position: [x, rowY, z],
|
|
927
|
+
parentId: shelfNode.id,
|
|
928
|
+
rotation: [draftRotation[0], localRotationY, draftRotation[2]],
|
|
929
|
+
},
|
|
930
|
+
cursorRotationY: ctx.currentCursorRotationY,
|
|
931
|
+
gridPosition: [x, rowY, z],
|
|
932
|
+
cursorPosition: [worldSnapped.x, worldSnapped.y, worldSnapped.z],
|
|
933
|
+
stopPropagation: true,
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Handle shelf:move — re-derive the closest row each tick so the user
|
|
939
|
+
* can slide between rows without leaving the shelf.
|
|
940
|
+
*/
|
|
941
|
+
move(ctx: PlacementContext, event: ShelfEvent): PlacementResult | null {
|
|
942
|
+
if (ctx.state.surface !== 'shelf-surface') return null
|
|
943
|
+
if (!(ctx.state.shelfId && ctx.draftItem)) return null
|
|
944
|
+
if (event.node.id !== ctx.state.shelfId) return null
|
|
945
|
+
|
|
946
|
+
const shelfNode = event.node as ShelfNode
|
|
947
|
+
const shelfMesh = sceneRegistry.nodes.get(shelfNode.id)
|
|
948
|
+
if (!shelfMesh) return null
|
|
949
|
+
|
|
950
|
+
const ourDims = getScaledDimensions(ctx.draftItem)
|
|
951
|
+
const worldPos = new Vector3(event.position[0], event.position[1], event.position[2])
|
|
952
|
+
const localPos = shelfMesh.worldToLocal(worldPos)
|
|
953
|
+
const rowY = getShelfRowSurfaceY(shelfNode, localPos.y)
|
|
954
|
+
if (rowY === null) return null
|
|
955
|
+
|
|
956
|
+
const x = snapToGrid(localPos.x, ourDims[0])
|
|
957
|
+
const z = snapToGrid(localPos.z, ourDims[2])
|
|
958
|
+
const worldSnapped = shelfMesh.localToWorld(new Vector3(x, rowY, z))
|
|
959
|
+
|
|
960
|
+
return {
|
|
961
|
+
gridPosition: [x, rowY, z],
|
|
962
|
+
cursorPosition: [worldSnapped.x, worldSnapped.y, worldSnapped.z],
|
|
963
|
+
cursorRotationY: ctx.currentCursorRotationY,
|
|
964
|
+
nodeUpdate: { position: [x, rowY, z] },
|
|
965
|
+
stopPropagation: true,
|
|
966
|
+
dirtyNodeId: null,
|
|
967
|
+
}
|
|
968
|
+
},
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Handle shelf:click — commit placement on the active row.
|
|
972
|
+
*/
|
|
973
|
+
click(ctx: PlacementContext, event: ShelfEvent): CommitResult | null {
|
|
974
|
+
if (ctx.state.surface !== 'shelf-surface') return null
|
|
975
|
+
if (!(ctx.draftItem && ctx.state.shelfId)) return null
|
|
976
|
+
if (event.node.id !== ctx.state.shelfId) return null
|
|
977
|
+
|
|
978
|
+
return {
|
|
979
|
+
nodeUpdate: {
|
|
980
|
+
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
981
|
+
parentId: ctx.state.shelfId,
|
|
982
|
+
metadata: stripTransient(ctx.draftItem.metadata),
|
|
983
|
+
},
|
|
984
|
+
stopPropagation: true,
|
|
985
|
+
dirtyNodeId: null,
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** Same upward-normal heuristic as `isUpwardItemSurfaceHit`, but typed
|
|
991
|
+
* for `ShelfEvent`. Re-uses the matrix-driven world normal calculation
|
|
992
|
+
* via a tiny `ItemEvent`-shaped adapter — the function only reads
|
|
993
|
+
* `event.normal` + `event.object`. */
|
|
994
|
+
function isUpwardShelfSurfaceHit(event: ShelfEvent): boolean {
|
|
995
|
+
return isUpwardItemSurfaceHit(event as unknown as ItemEvent)
|
|
996
|
+
}
|
|
997
|
+
|
|
590
998
|
// ============================================================================
|
|
591
999
|
// VALIDATION
|
|
592
1000
|
// ============================================================================
|
|
@@ -603,6 +1011,11 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
|
|
603
1011
|
return ctx.state.surfaceItemId !== null
|
|
604
1012
|
}
|
|
605
1013
|
|
|
1014
|
+
// Shelf surface: same — size check already happened on enter
|
|
1015
|
+
if (ctx.state.surface === 'shelf-surface') {
|
|
1016
|
+
return ctx.state.shelfId !== null
|
|
1017
|
+
}
|
|
1018
|
+
|
|
606
1019
|
const attachTo = ctx.draftItem.asset.attachTo
|
|
607
1020
|
|
|
608
1021
|
const alignedDims = getGridAlignedDimensions(getScaledDimensions(ctx.draftItem), attachTo)
|
|
@@ -619,6 +1032,9 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
|
|
619
1032
|
}
|
|
620
1033
|
|
|
621
1034
|
if (attachTo === 'wall' || attachTo === 'wall-side') {
|
|
1035
|
+
if (ctx.state.surface === 'roof-wall') {
|
|
1036
|
+
return canPlaceOnRoofWall(ctx)
|
|
1037
|
+
}
|
|
622
1038
|
if (ctx.state.surface !== 'wall' || !ctx.state.wallId) return false
|
|
623
1039
|
return validators.canPlaceOnWall(
|
|
624
1040
|
ctx.levelId,
|
|
@@ -635,7 +1051,7 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
|
|
635
1051
|
// Floor (no attachTo)
|
|
636
1052
|
return validators.canPlaceOnFloor(
|
|
637
1053
|
ctx.levelId,
|
|
638
|
-
[ctx.gridPosition.x,
|
|
1054
|
+
[ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
639
1055
|
alignedDims,
|
|
640
1056
|
ctx.draftItem.rotation,
|
|
641
1057
|
[ctx.draftItem.id],
|