@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,109 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
nodeRegistry,
|
|
6
|
+
registerNode,
|
|
7
|
+
useScene,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
import useEditor from '../../../store/use-editor'
|
|
12
|
+
import { collectSelectableCandidateIds } from './select-candidates'
|
|
13
|
+
|
|
14
|
+
function registerSelectableElevatorTestKind() {
|
|
15
|
+
if (nodeRegistry.has('elevator')) return
|
|
16
|
+
|
|
17
|
+
registerNode({
|
|
18
|
+
kind: 'elevator',
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
schema: z.object({ type: z.literal('elevator') }) as never,
|
|
21
|
+
category: 'structure',
|
|
22
|
+
defaults: () => ({}),
|
|
23
|
+
capabilities: { selectable: {} },
|
|
24
|
+
floorplanScope: 'building',
|
|
25
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
26
|
+
} as AnyNodeDefinition)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('selectable candidates', () => {
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
registerSelectableElevatorTestKind()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
useScene.setState({
|
|
36
|
+
nodes: {},
|
|
37
|
+
rootNodeIds: [],
|
|
38
|
+
dirtyNodes: new Set(),
|
|
39
|
+
collections: {},
|
|
40
|
+
} as never)
|
|
41
|
+
useViewer.setState({
|
|
42
|
+
selection: {
|
|
43
|
+
buildingId: 'building_test',
|
|
44
|
+
levelId: 'level_test',
|
|
45
|
+
zoneId: null,
|
|
46
|
+
selectedIds: [],
|
|
47
|
+
},
|
|
48
|
+
previewSelectedIds: [],
|
|
49
|
+
})
|
|
50
|
+
useEditor.setState({
|
|
51
|
+
phase: 'structure',
|
|
52
|
+
structureLayer: 'elements',
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('includes building-scoped elevators for the active level building', () => {
|
|
57
|
+
useScene.setState({
|
|
58
|
+
nodes: {
|
|
59
|
+
building_test: {
|
|
60
|
+
id: 'building_test',
|
|
61
|
+
type: 'building',
|
|
62
|
+
children: ['level_test', 'elevator_test'],
|
|
63
|
+
},
|
|
64
|
+
level_test: {
|
|
65
|
+
id: 'level_test',
|
|
66
|
+
type: 'level',
|
|
67
|
+
parentId: 'building_test',
|
|
68
|
+
children: [],
|
|
69
|
+
},
|
|
70
|
+
elevator_test: {
|
|
71
|
+
id: 'elevator_test',
|
|
72
|
+
type: 'elevator',
|
|
73
|
+
parentId: 'building_test',
|
|
74
|
+
position: [1, 0, 2],
|
|
75
|
+
rotation: 0,
|
|
76
|
+
},
|
|
77
|
+
} as unknown as Record<string, AnyNode>,
|
|
78
|
+
} as never)
|
|
79
|
+
|
|
80
|
+
expect(collectSelectableCandidateIds()).toContain('elevator_test')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('includes legacy level-parented elevators already loaded in the editor', () => {
|
|
84
|
+
useScene.setState({
|
|
85
|
+
nodes: {
|
|
86
|
+
building_test: {
|
|
87
|
+
id: 'building_test',
|
|
88
|
+
type: 'building',
|
|
89
|
+
children: ['level_test'],
|
|
90
|
+
},
|
|
91
|
+
level_test: {
|
|
92
|
+
id: 'level_test',
|
|
93
|
+
type: 'level',
|
|
94
|
+
parentId: 'building_test',
|
|
95
|
+
children: ['elevator_test'],
|
|
96
|
+
},
|
|
97
|
+
elevator_test: {
|
|
98
|
+
id: 'elevator_test',
|
|
99
|
+
type: 'elevator',
|
|
100
|
+
parentId: 'level_test',
|
|
101
|
+
position: [1, 0, 2],
|
|
102
|
+
rotation: 0,
|
|
103
|
+
},
|
|
104
|
+
} as unknown as Record<string, AnyNode>,
|
|
105
|
+
} as never)
|
|
106
|
+
|
|
107
|
+
expect(collectSelectableCandidateIds()).toContain('elevator_test')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
isRegistrySelectable,
|
|
5
|
+
type LevelNode,
|
|
6
|
+
nodeRegistry,
|
|
7
|
+
resolveBuildingForLevel,
|
|
8
|
+
resolveLevelId,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
12
|
+
import useEditor from '../../../store/use-editor'
|
|
13
|
+
|
|
14
|
+
function isVisibleSelectableNode(node: AnyNode): boolean {
|
|
15
|
+
if ((node as { visible?: boolean }).visible === false) return false
|
|
16
|
+
return isRegistrySelectable(node.type)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function collectSelectableCandidateIds(): string[] {
|
|
20
|
+
const { levelId } = useViewer.getState().selection
|
|
21
|
+
const { nodes } = useScene.getState()
|
|
22
|
+
const { phase, structureLayer } = useEditor.getState()
|
|
23
|
+
const result: string[] = []
|
|
24
|
+
const seen = new Set<string>()
|
|
25
|
+
const addNode = (node: AnyNode | undefined) => {
|
|
26
|
+
if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
|
|
27
|
+
seen.add(node.id)
|
|
28
|
+
result.push(node.id)
|
|
29
|
+
}
|
|
30
|
+
const visitLevelDescendant = (id: AnyNodeId) => {
|
|
31
|
+
const node = nodes[id]
|
|
32
|
+
if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
|
|
33
|
+
|
|
34
|
+
if (isRegistrySelectable(node.type)) {
|
|
35
|
+
addNode(node)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const children = 'children' in node && Array.isArray(node.children) ? node.children : []
|
|
39
|
+
for (const childId of children) {
|
|
40
|
+
visitLevelDescendant(childId as AnyNodeId)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (phase === 'site') {
|
|
45
|
+
for (const node of Object.values(nodes)) {
|
|
46
|
+
if (node.type === 'building') addNode(node)
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!levelId) return []
|
|
52
|
+
const levelNode = nodes[levelId as AnyNodeId] as LevelNode | undefined
|
|
53
|
+
if (levelNode?.type !== 'level') return []
|
|
54
|
+
|
|
55
|
+
if (phase === 'structure' && structureLayer === 'zones') {
|
|
56
|
+
for (const childId of levelNode.children) {
|
|
57
|
+
const node = nodes[childId as AnyNodeId]
|
|
58
|
+
if (node?.type === 'zone') addNode(node)
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const childId of levelNode.children) {
|
|
64
|
+
visitLevelDescendant(childId as AnyNodeId)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const buildingId = resolveBuildingForLevel(levelId as AnyNodeId, nodes)
|
|
68
|
+
for (const node of Object.values(nodes)) {
|
|
69
|
+
if (!node || node.type === 'level' || !isVisibleSelectableNode(node)) continue
|
|
70
|
+
|
|
71
|
+
const def = nodeRegistry.get(node.type)
|
|
72
|
+
const isBuildingScoped = def?.floorplanScope === 'building'
|
|
73
|
+
const parentId = (node as { parentId?: AnyNodeId | null }).parentId
|
|
74
|
+
if (isBuildingScoped && buildingId && parentId === buildingId) {
|
|
75
|
+
addNode(node)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!isBuildingScoped && resolveLevelId(node, nodes) === levelId) {
|
|
80
|
+
addNode(node)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { nodeRegistry } from '@pascal-app/core'
|
|
2
|
+
import { type ComponentType, lazy } from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Phase 5 Stage D — runtime lazy-load of a kind's affordance tool.
|
|
6
|
+
*
|
|
7
|
+
* The editor can't statically import from `@pascal-app/nodes` (the
|
|
8
|
+
* nodes package depends on editor — static imports would cycle). The
|
|
9
|
+
* kind declares its drag-affordance components in
|
|
10
|
+
* `def.affordanceTools[<key>]: () => import('./<name>-tool')`; this
|
|
11
|
+
* helper resolves that to a `React.lazy` component at the call site.
|
|
12
|
+
*
|
|
13
|
+
* Returns null when the kind doesn't declare the affordance — callers
|
|
14
|
+
* mount the legacy fallback in that case.
|
|
15
|
+
*/
|
|
16
|
+
const lazyToolCache = new WeakMap<() => Promise<unknown>, ComponentType>()
|
|
17
|
+
|
|
18
|
+
export function getRegistryAffordanceTool(
|
|
19
|
+
kind: string,
|
|
20
|
+
affordance: string,
|
|
21
|
+
): ComponentType<any> | null {
|
|
22
|
+
const def = nodeRegistry.get(kind)
|
|
23
|
+
const loader = def?.affordanceTools?.[affordance]
|
|
24
|
+
if (!loader) return null
|
|
25
|
+
const cached = lazyToolCache.get(loader)
|
|
26
|
+
if (cached) return cached
|
|
27
|
+
const Comp = lazy(loader as () => Promise<{ default: ComponentType<any> }>)
|
|
28
|
+
lazyToolCache.set(loader, Comp as unknown as ComponentType)
|
|
29
|
+
return Comp as unknown as ComponentType<any>
|
|
30
|
+
}
|
|
@@ -6,24 +6,45 @@ import { furnishTools } from '../../../components/ui/action-menu/furnish-tools'
|
|
|
6
6
|
import { tools } from '../../../components/ui/action-menu/structure-tools'
|
|
7
7
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
8
8
|
import useEditor from '../../../store/use-editor'
|
|
9
|
+
import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
|
|
9
10
|
|
|
10
11
|
interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
11
12
|
color?: string
|
|
12
13
|
depthWrite?: boolean
|
|
13
14
|
showTooltip?: boolean
|
|
14
15
|
height?: number
|
|
16
|
+
/**
|
|
17
|
+
* Put the bright marker dot at the TIP of the vertical line (y = height)
|
|
18
|
+
* instead of on the ground ring. Used when the point being placed hangs
|
|
19
|
+
* above the floor (e.g. duct drawn against the ceiling): the dot rides at
|
|
20
|
+
* the cursor / placement point while the line drops to a floor ring that
|
|
21
|
+
* keeps the plan position readable.
|
|
22
|
+
*/
|
|
23
|
+
dotAtTip?: boolean
|
|
15
24
|
/** Custom tooltip content — overrides the auto-detected build tool icon */
|
|
16
25
|
tooltipContent?: React.ReactNode
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
export const CursorSphere = forwardRef<Group, CursorSphereProps>(function CursorSphere(
|
|
20
|
-
{
|
|
29
|
+
{
|
|
30
|
+
color = '#818cf8',
|
|
31
|
+
showTooltip = true,
|
|
32
|
+
height = 2.5,
|
|
33
|
+
dotAtTip = false,
|
|
34
|
+
visible = true,
|
|
35
|
+
tooltipContent,
|
|
36
|
+
...props
|
|
37
|
+
},
|
|
21
38
|
ref,
|
|
22
39
|
) {
|
|
23
40
|
const tool = useEditor((s) => s.tool)
|
|
24
41
|
const mode = useEditor((s) => s.mode)
|
|
25
42
|
const catalogCategory = useEditor((s) => s.catalogCategory)
|
|
26
43
|
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
|
44
|
+
// While a wall snap is active the beacon layer already marks the point
|
|
45
|
+
// (green square at a corner); hide the cursor's ground dot/ring so it
|
|
46
|
+
// doesn't sit on top of that glyph.
|
|
47
|
+
const isSnapping = useWallSnapIndicator((s) => s.point !== null)
|
|
27
48
|
|
|
28
49
|
// Find the icon for the current tool
|
|
29
50
|
let activeToolConfig = null
|
|
@@ -39,32 +60,38 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
39
60
|
|
|
40
61
|
return (
|
|
41
62
|
<group ref={ref} {...props} visible={isVisible}>
|
|
42
|
-
{/* Flat marker on the ground
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
{/* Flat marker on the ground. The bright center dot moves to the tip
|
|
64
|
+
of the line in `dotAtTip` mode (the placement point hangs above the
|
|
65
|
+
floor), leaving a faint ring here so the plan position stays read. */}
|
|
66
|
+
{!isSnapping && (
|
|
67
|
+
<group rotation={[-Math.PI / 2, 0, 0]}>
|
|
68
|
+
{/* Center dot — at the ground unless the placement point is elevated */}
|
|
69
|
+
{!dotAtTip && (
|
|
70
|
+
<mesh layers={EDITOR_LAYER} renderOrder={2}>
|
|
71
|
+
<circleGeometry args={[0.06, 32]} />
|
|
72
|
+
<meshBasicMaterial
|
|
73
|
+
color={color}
|
|
74
|
+
depthTest={false}
|
|
75
|
+
depthWrite={false}
|
|
76
|
+
opacity={0.9}
|
|
77
|
+
transparent
|
|
78
|
+
/>
|
|
79
|
+
</mesh>
|
|
80
|
+
)}
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
{/* Outer ring / glow */}
|
|
83
|
+
<mesh layers={EDITOR_LAYER} renderOrder={2}>
|
|
84
|
+
<circleGeometry args={[0.2, 32]} />
|
|
85
|
+
<meshBasicMaterial
|
|
86
|
+
color={color}
|
|
87
|
+
depthTest={false}
|
|
88
|
+
depthWrite={false}
|
|
89
|
+
opacity={dotAtTip ? 0.2 : 0.25}
|
|
90
|
+
transparent
|
|
91
|
+
/>
|
|
92
|
+
</mesh>
|
|
93
|
+
</group>
|
|
94
|
+
)}
|
|
68
95
|
|
|
69
96
|
{/* Vertical line */}
|
|
70
97
|
{height > 0 && (
|
|
@@ -80,6 +107,15 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
80
107
|
</mesh>
|
|
81
108
|
)}
|
|
82
109
|
|
|
110
|
+
{/* Bright marker dot at the tip of the line — the actual placement
|
|
111
|
+
point, riding at the cursor while the line drops to the floor. */}
|
|
112
|
+
{dotAtTip && height > 0 && (
|
|
113
|
+
<mesh layers={EDITOR_LAYER} position={[0, height, 0]} renderOrder={2}>
|
|
114
|
+
<sphereGeometry args={[0.08, 20, 14]} />
|
|
115
|
+
<meshBasicMaterial color={color} depthTest={false} depthWrite={false} />
|
|
116
|
+
</mesh>
|
|
117
|
+
)}
|
|
118
|
+
|
|
83
119
|
{/* Tool Icon Tooltip at the top of the line */}
|
|
84
120
|
{isVisible && showTooltip && (activeToolConfig || tooltipContent) && (
|
|
85
121
|
<Html
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, resolveFacingIndicator, sceneRegistry, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useEffect, useMemo } from 'react'
|
|
5
|
+
import {
|
|
6
|
+
Box3,
|
|
7
|
+
BoxGeometry,
|
|
8
|
+
EdgesGeometry,
|
|
9
|
+
Matrix4,
|
|
10
|
+
type Mesh,
|
|
11
|
+
type Object3D,
|
|
12
|
+
PlaneGeometry,
|
|
13
|
+
Vector3,
|
|
14
|
+
} from 'three'
|
|
15
|
+
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
16
|
+
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
17
|
+
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
18
|
+
import useFacingPose from '../../../store/use-facing-pose'
|
|
19
|
+
|
|
20
|
+
const NO_RAYCAST = () => null
|
|
21
|
+
|
|
22
|
+
/** green-500 — matches the item placement box's "placeable" state. */
|
|
23
|
+
const DEFAULT_COLOR = 0x22_c5_5e
|
|
24
|
+
|
|
25
|
+
type LocalBounds = { size: [number, number, number]; center: [number, number, number] }
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The node's bounding box in its OWN, unrotated frame — measured from the
|
|
29
|
+
* rendered geometry so it captures the full extent (base, cap, overhang),
|
|
30
|
+
* not just the declared width/height/depth. World position + rotation are
|
|
31
|
+
* cancelled out (invert the root's world matrix), so the result is stable
|
|
32
|
+
* regardless of where the live drag has moved the node; the caller re-applies
|
|
33
|
+
* the current Y rotation on the group. Returns null when nothing measurable.
|
|
34
|
+
*/
|
|
35
|
+
function measureLocalBounds(obj: Object3D): LocalBounds | null {
|
|
36
|
+
obj.updateWorldMatrix(true, true)
|
|
37
|
+
const inverseRoot = new Matrix4().copy(obj.matrixWorld).invert()
|
|
38
|
+
const box = new Box3()
|
|
39
|
+
const meshBox = new Box3()
|
|
40
|
+
const toLocal = new Matrix4()
|
|
41
|
+
let measured = false
|
|
42
|
+
obj.traverse((child) => {
|
|
43
|
+
const mesh = child as Mesh
|
|
44
|
+
if (!mesh.isMesh || !mesh.geometry) return
|
|
45
|
+
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox()
|
|
46
|
+
const bb = mesh.geometry.boundingBox
|
|
47
|
+
if (!bb) return
|
|
48
|
+
toLocal.copy(inverseRoot).multiply(mesh.matrixWorld)
|
|
49
|
+
meshBox.copy(bb).applyMatrix4(toLocal)
|
|
50
|
+
box.union(meshBox)
|
|
51
|
+
measured = true
|
|
52
|
+
})
|
|
53
|
+
if (!measured || box.isEmpty()) return null
|
|
54
|
+
const size = box.getSize(new Vector3())
|
|
55
|
+
const center = box.getCenter(new Vector3())
|
|
56
|
+
return { size: [size.x, size.y, size.z], center: [center.x, center.y, center.z] }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface DragBoundingBoxProps {
|
|
60
|
+
/** Node whose rendered geometry is measured for the box extents. */
|
|
61
|
+
nodeId: string
|
|
62
|
+
/** Footprint origin on the floor, `[x, 0, z]` — the node's live position. */
|
|
63
|
+
position: [number, number, number]
|
|
64
|
+
/** Y rotation (radians) applied to the box, matching the dragged node. */
|
|
65
|
+
rotationY?: number
|
|
66
|
+
/** Declared `[width, height, depth]`, used until/if the mesh can't be measured. */
|
|
67
|
+
fallbackSize?: [number, number, number]
|
|
68
|
+
/**
|
|
69
|
+
* Hard override for the box extents — wins over both mesh measurement and
|
|
70
|
+
* `fallbackSize`. Use when the rendered mesh contains extras the user
|
|
71
|
+
* wouldn't read as "the thing being dragged" (e.g. an elevator whose mesh
|
|
72
|
+
* includes per-level landings outside the shaft footprint).
|
|
73
|
+
*/
|
|
74
|
+
size?: [number, number, number]
|
|
75
|
+
/** Local center of `size`. Defaults to the node origin plus half-height. */
|
|
76
|
+
center?: [number, number, number]
|
|
77
|
+
/** Y center of the box in the node's local frame. Defaults to `size[1] / 2`. */
|
|
78
|
+
centerY?: number
|
|
79
|
+
color?: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Footprint box drawn around a node while it is being dragged — the same
|
|
84
|
+
* affordance items get during placement: a wireframe cube spanning the node's
|
|
85
|
+
* full measured extent plus a ground plane with a radial opacity gradient
|
|
86
|
+
* (transparent in the centre, opaque toward the edges). Overlay layer +
|
|
87
|
+
* `depthTest: false` keep it drawn on top of scene geometry throughout the
|
|
88
|
+
* drag, and the box visualises the bounds that drive alignment snapping.
|
|
89
|
+
*/
|
|
90
|
+
export function DragBoundingBox({
|
|
91
|
+
nodeId,
|
|
92
|
+
position,
|
|
93
|
+
rotationY = 0,
|
|
94
|
+
fallbackSize = [0, 0, 0],
|
|
95
|
+
size,
|
|
96
|
+
center,
|
|
97
|
+
centerY,
|
|
98
|
+
color = DEFAULT_COLOR,
|
|
99
|
+
}: DragBoundingBoxProps) {
|
|
100
|
+
const nodeType = useScene((state) => state.nodes[nodeId as AnyNodeId]?.type)
|
|
101
|
+
const facing = nodeType ? resolveFacingIndicator(nodeType) : null
|
|
102
|
+
|
|
103
|
+
const measured = useMemo(() => {
|
|
104
|
+
if (size) return null
|
|
105
|
+
const obj = sceneRegistry.nodes.get(nodeId)
|
|
106
|
+
return obj ? measureLocalBounds(obj) : null
|
|
107
|
+
}, [nodeId, size])
|
|
108
|
+
|
|
109
|
+
const [w, h, d] = size ?? measured?.size ?? fallbackSize
|
|
110
|
+
const [cx, cy, cz] = size
|
|
111
|
+
? (center ?? [0, centerY ?? size[1] / 2, 0])
|
|
112
|
+
: (measured?.center ?? [0, fallbackSize[1] / 2, 0])
|
|
113
|
+
const minY = cy - h / 2
|
|
114
|
+
const groundY = minY + 0.01
|
|
115
|
+
|
|
116
|
+
const edgeGeometry = useMemo(() => {
|
|
117
|
+
const box = new BoxGeometry(w, h, d)
|
|
118
|
+
const edges = new EdgesGeometry(box)
|
|
119
|
+
box.dispose()
|
|
120
|
+
return edges
|
|
121
|
+
}, [w, h, d])
|
|
122
|
+
|
|
123
|
+
// Flat on the ground (XZ) at the box's base, nudged up 0.01m to avoid
|
|
124
|
+
// z-fighting with slabs.
|
|
125
|
+
const planeGeometry = useMemo(() => {
|
|
126
|
+
const plane = new PlaneGeometry(w, d)
|
|
127
|
+
plane.rotateX(-Math.PI / 2)
|
|
128
|
+
plane.translate(cx, groundY, cz)
|
|
129
|
+
return plane
|
|
130
|
+
}, [w, d, cx, groundY, cz])
|
|
131
|
+
|
|
132
|
+
const edgeMaterial = useMemo(
|
|
133
|
+
() => new LineBasicNodeMaterial({ color, linewidth: 3, depthTest: false, depthWrite: false }),
|
|
134
|
+
[color],
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const planeMaterial = useMemo(() => {
|
|
138
|
+
const material = new MeshBasicNodeMaterial({
|
|
139
|
+
color,
|
|
140
|
+
transparent: true,
|
|
141
|
+
depthTest: false,
|
|
142
|
+
depthWrite: false,
|
|
143
|
+
})
|
|
144
|
+
material.opacityNode = smoothstep(0, 0.7, distance(uv(), vec2(0.5, 0.5))).mul(0.6)
|
|
145
|
+
return material
|
|
146
|
+
}, [color])
|
|
147
|
+
|
|
148
|
+
useEffect(
|
|
149
|
+
() => () => {
|
|
150
|
+
edgeGeometry.dispose()
|
|
151
|
+
planeGeometry.dispose()
|
|
152
|
+
edgeMaterial.dispose()
|
|
153
|
+
planeMaterial.dispose()
|
|
154
|
+
},
|
|
155
|
+
[edgeGeometry, planeGeometry, edgeMaterial, planeMaterial],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Publish the facing pose to the editor-side overlay (the single triangle
|
|
159
|
+
// renderer) rather than drawing it here. The node origin is `position`; the
|
|
160
|
+
// footprint centre is `[cx, cz]` in the node's local frame. Runs each drag
|
|
161
|
+
// frame so the triangle follows; a separate mount/unmount effect clears it.
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!facing || d <= 0) return
|
|
164
|
+
useFacingPose.getState().set({
|
|
165
|
+
position: [position[0], position[1] + groundY, position[2]],
|
|
166
|
+
rotationY,
|
|
167
|
+
depth: d,
|
|
168
|
+
center: [cx, cz],
|
|
169
|
+
reversed: facing.reversed,
|
|
170
|
+
})
|
|
171
|
+
}, [facing, position, rotationY, d, cx, cz, groundY])
|
|
172
|
+
useEffect(() => () => useFacingPose.getState().clear(), [])
|
|
173
|
+
|
|
174
|
+
if (w <= 0 || h <= 0 || d <= 0) return null
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<group position={position} rotation={[0, rotationY, 0]}>
|
|
178
|
+
<mesh
|
|
179
|
+
geometry={planeGeometry}
|
|
180
|
+
layers={EDITOR_LAYER}
|
|
181
|
+
material={planeMaterial}
|
|
182
|
+
raycast={NO_RAYCAST}
|
|
183
|
+
renderOrder={999}
|
|
184
|
+
/>
|
|
185
|
+
<lineSegments
|
|
186
|
+
geometry={edgeGeometry}
|
|
187
|
+
layers={EDITOR_LAYER}
|
|
188
|
+
material={edgeMaterial}
|
|
189
|
+
position={[cx, cy, cz]}
|
|
190
|
+
raycast={NO_RAYCAST}
|
|
191
|
+
renderOrder={999}
|
|
192
|
+
/>
|
|
193
|
+
</group>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react'
|
|
2
|
+
import { BufferGeometry, DoubleSide, Float32BufferAttribute } from 'three'
|
|
3
|
+
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
4
|
+
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
5
|
+
|
|
6
|
+
// A flat forward-pointing triangle drawn on the floor just in front of a
|
|
7
|
+
// placement ghost, so the direction the node will face is obvious. Tip at local
|
|
8
|
+
// +Z (every kind's forward face); render it inside the ghost's rotated group so
|
|
9
|
+
// it inherits the node's yaw. Matches the item coordinator / drag-bounding-box
|
|
10
|
+
// indicators (same size, colour, double-sided so it shows from above).
|
|
11
|
+
const FACING_INDICATOR_WIDTH = 0.4
|
|
12
|
+
const FACING_INDICATOR_LENGTH = 0.46
|
|
13
|
+
const FACING_INDICATOR_GAP = 0.45
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param depth bbox depth (along local Z) of the ghost — positions the
|
|
17
|
+
* triangle just past the front edge.
|
|
18
|
+
* @param center optional [x, z] of the bbox centre in the ghost's local frame.
|
|
19
|
+
* @param reversed point along local -Z (the front is the -Z side, e.g. a stair
|
|
20
|
+
* entry) instead of +Z.
|
|
21
|
+
* @param y small lift off the floor to avoid z-fighting.
|
|
22
|
+
*/
|
|
23
|
+
export function FacingIndicator({
|
|
24
|
+
depth,
|
|
25
|
+
center = [0, 0],
|
|
26
|
+
reversed = false,
|
|
27
|
+
y = 0.02,
|
|
28
|
+
}: {
|
|
29
|
+
depth: number
|
|
30
|
+
center?: [number, number]
|
|
31
|
+
reversed?: boolean
|
|
32
|
+
y?: number
|
|
33
|
+
}) {
|
|
34
|
+
const dir = reversed ? -1 : 1
|
|
35
|
+
// Per-instance geometry/material (not module singletons) so this works no
|
|
36
|
+
// matter which package mounts it (the tools live in `nodes`, imported via
|
|
37
|
+
// `@pascal-app/editor`). Disposed on unmount.
|
|
38
|
+
const geometry = useMemo(() => {
|
|
39
|
+
const g = new BufferGeometry()
|
|
40
|
+
g.setAttribute(
|
|
41
|
+
'position',
|
|
42
|
+
new Float32BufferAttribute(
|
|
43
|
+
[
|
|
44
|
+
0,
|
|
45
|
+
0,
|
|
46
|
+
dir * FACING_INDICATOR_LENGTH,
|
|
47
|
+
FACING_INDICATOR_WIDTH / 2,
|
|
48
|
+
0,
|
|
49
|
+
0,
|
|
50
|
+
-FACING_INDICATOR_WIDTH / 2,
|
|
51
|
+
0,
|
|
52
|
+
0,
|
|
53
|
+
],
|
|
54
|
+
3,
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
return g
|
|
58
|
+
}, [dir])
|
|
59
|
+
const material = useMemo(
|
|
60
|
+
() =>
|
|
61
|
+
new MeshBasicNodeMaterial({
|
|
62
|
+
color: 0x22_c5_5e, // green-500 (forward)
|
|
63
|
+
depthTest: false,
|
|
64
|
+
depthWrite: false,
|
|
65
|
+
side: DoubleSide,
|
|
66
|
+
}),
|
|
67
|
+
[],
|
|
68
|
+
)
|
|
69
|
+
useEffect(
|
|
70
|
+
() => () => {
|
|
71
|
+
geometry.dispose()
|
|
72
|
+
material.dispose()
|
|
73
|
+
},
|
|
74
|
+
[geometry, material],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<mesh
|
|
79
|
+
frustumCulled={false}
|
|
80
|
+
geometry={geometry}
|
|
81
|
+
layers={EDITOR_LAYER}
|
|
82
|
+
material={material}
|
|
83
|
+
position={[center[0], y, center[1] + dir * (depth / 2 + FACING_INDICATOR_GAP)]}
|
|
84
|
+
renderOrder={1001}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import type { Group } from 'three'
|
|
3
|
+
import useFacingPose, { type FacingPose } from '../../../store/use-facing-pose'
|
|
4
|
+
import { FacingIndicator } from './facing-indicator'
|
|
5
|
+
|
|
6
|
+
// The single editor-side renderer for the placement/move facing triangle.
|
|
7
|
+
// Mounted once inside ToolManager's building-local group; every tool publishes
|
|
8
|
+
// its ghost pose to `useFacingPose` and this draws the triangle. The pose
|
|
9
|
+
// (position/yaw) is applied imperatively to a ref so the per-frame cursor
|
|
10
|
+
// updates don't re-render React — only a change in footprint shape (depth /
|
|
11
|
+
// centre), which is constant per tool session, triggers a re-render.
|
|
12
|
+
export function FacingPoseIndicator() {
|
|
13
|
+
const groupRef = useRef<Group>(null)
|
|
14
|
+
const [shape, setShape] = useState<Pick<FacingPose, 'depth' | 'center' | 'reversed'> | null>(null)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const apply = (pose: FacingPose | null) => {
|
|
18
|
+
const group = groupRef.current
|
|
19
|
+
if (group) {
|
|
20
|
+
if (pose) {
|
|
21
|
+
group.visible = true
|
|
22
|
+
group.position.set(...pose.position)
|
|
23
|
+
group.rotation.y = pose.rotationY
|
|
24
|
+
} else {
|
|
25
|
+
group.visible = false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
setShape((prev) => {
|
|
29
|
+
if (!pose) return null
|
|
30
|
+
const center = pose.center ?? [0, 0]
|
|
31
|
+
if (
|
|
32
|
+
prev &&
|
|
33
|
+
prev.depth === pose.depth &&
|
|
34
|
+
prev.reversed === pose.reversed &&
|
|
35
|
+
(prev.center ?? [0, 0])[0] === center[0] &&
|
|
36
|
+
(prev.center ?? [0, 0])[1] === center[1]
|
|
37
|
+
) {
|
|
38
|
+
return prev
|
|
39
|
+
}
|
|
40
|
+
return { depth: pose.depth, center, reversed: pose.reversed }
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
apply(useFacingPose.getState().pose)
|
|
44
|
+
return useFacingPose.subscribe((state) => apply(state.pose))
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<group ref={groupRef} visible={false}>
|
|
49
|
+
{shape ? (
|
|
50
|
+
<FacingIndicator center={shape.center} depth={shape.depth} reversed={shape.reversed} />
|
|
51
|
+
) : null}
|
|
52
|
+
</group>
|
|
53
|
+
)
|
|
54
|
+
}
|