@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,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import { getSceneTheme, useViewer } from '@pascal-app/viewer'
|
|
4
4
|
import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
5
5
|
import useEditor from '../../store/use-editor'
|
|
6
6
|
import { MobileTabBar } from '../ui/sidebar/mobile-tab-bar'
|
|
@@ -56,8 +56,8 @@ export function EditorLayoutMobile({
|
|
|
56
56
|
const activePanel = useEditor((s) => s.activeSidebarPanel)
|
|
57
57
|
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
|
|
58
58
|
const panelSheetHeight = useEditor((s) => s.mobilePanelSheetHeight)
|
|
59
|
-
const
|
|
60
|
-
const viewerBg =
|
|
59
|
+
const isDark = useViewer((s) => getSceneTheme(s.sceneTheme).appearance === 'dark')
|
|
60
|
+
const viewerBg = isDark ? VIEWER_BG_DARK : VIEWER_BG_LIGHT
|
|
61
61
|
|
|
62
62
|
const middleRef = useRef<HTMLDivElement>(null)
|
|
63
63
|
const sheetRef = useRef<BottomSheetHandle>(null)
|
|
@@ -159,6 +159,11 @@ export function EditorLayoutMobile({
|
|
|
159
159
|
const expandedThreshold = Math.max(SHEET_HANDLE_PX, defaultPx * 0.5)
|
|
160
160
|
if (current > expandedThreshold) {
|
|
161
161
|
sheetRef.current?.snapTo(SHEET_HANDLE_PX)
|
|
162
|
+
// Closing the sheet disarms any build tool back to select
|
|
163
|
+
const { mode, setMode } = useEditor.getState()
|
|
164
|
+
if (mode === 'build') {
|
|
165
|
+
setMode('select')
|
|
166
|
+
}
|
|
162
167
|
} else {
|
|
163
168
|
sheetRef.current?.snapTo(defaultPx)
|
|
164
169
|
}
|
|
@@ -5,12 +5,14 @@ import { useIsMobile } from '../../hooks/use-mobile'
|
|
|
5
5
|
import useEditor from '../../store/use-editor'
|
|
6
6
|
|
|
7
7
|
import { useSidebarStore } from '../ui/primitives/sidebar'
|
|
8
|
-
import { type SidebarTab
|
|
8
|
+
import { IconRail, type SidebarTab } from '../ui/sidebar/tab-bar'
|
|
9
9
|
import { EditorLayoutMobile } from './editor-layout-mobile'
|
|
10
10
|
|
|
11
11
|
const SIDEBAR_MIN_WIDTH = 300
|
|
12
12
|
const SIDEBAR_MAX_WIDTH = 800
|
|
13
13
|
const SIDEBAR_COLLAPSE_THRESHOLD = 220
|
|
14
|
+
// Matches the `w-14` rail in <IconRail>; the resize math is relative to it.
|
|
15
|
+
const RAIL_WIDTH = 56
|
|
14
16
|
|
|
15
17
|
// ── Left column: resizable panel with tab bar ────────────────────────────────
|
|
16
18
|
|
|
@@ -33,7 +35,6 @@ function LeftColumn({
|
|
|
33
35
|
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
|
|
34
36
|
|
|
35
37
|
const isResizing = useRef(false)
|
|
36
|
-
const isExpanding = useRef(false)
|
|
37
38
|
|
|
38
39
|
// Ensure active panel is a valid tab
|
|
39
40
|
useEffect(() => {
|
|
@@ -51,6 +52,15 @@ function LeftColumn({
|
|
|
51
52
|
}
|
|
52
53
|
}, [activePanel])
|
|
53
54
|
|
|
55
|
+
// Closing (collapsing) the sidebar disarms any build tool back to select
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!isCollapsed) return
|
|
58
|
+
const { mode, setMode } = useEditor.getState()
|
|
59
|
+
if (mode === 'build') {
|
|
60
|
+
setMode('select')
|
|
61
|
+
}
|
|
62
|
+
}, [isCollapsed])
|
|
63
|
+
|
|
54
64
|
const handleResizerDown = useCallback(
|
|
55
65
|
(e: React.PointerEvent) => {
|
|
56
66
|
e.preventDefault()
|
|
@@ -62,35 +72,40 @@ function LeftColumn({
|
|
|
62
72
|
[setIsDragging],
|
|
63
73
|
)
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
// Rail click: reopen a collapsed panel, collapse when re-clicking the open
|
|
76
|
+
// tab, otherwise switch tabs. Reopening clamps below-min persisted widths
|
|
77
|
+
// up to the minimum so the panel always returns to a usable size.
|
|
78
|
+
const handleRailClick = useCallback(
|
|
79
|
+
(id: string) => {
|
|
80
|
+
if (isCollapsed) {
|
|
81
|
+
setIsCollapsed(false)
|
|
82
|
+
if (width < SIDEBAR_MIN_WIDTH) setWidth(SIDEBAR_MIN_WIDTH)
|
|
83
|
+
setActivePanel(id)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if (id === activePanel) {
|
|
87
|
+
setIsCollapsed(true)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
setActivePanel(id)
|
|
72
91
|
},
|
|
73
|
-
[
|
|
92
|
+
[isCollapsed, width, activePanel, setIsCollapsed, setWidth, setActivePanel],
|
|
74
93
|
)
|
|
75
94
|
|
|
76
95
|
useEffect(() => {
|
|
77
96
|
const handlePointerMove = (e: PointerEvent) => {
|
|
78
|
-
if (isResizing.current)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
setWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(newWidth, SIDEBAR_MAX_WIDTH)))
|
|
85
|
-
}
|
|
86
|
-
} else if (isExpanding.current && e.clientX > 60) {
|
|
97
|
+
if (!isResizing.current) return
|
|
98
|
+
// Rail occupies the leftmost 48px; the panel starts after it.
|
|
99
|
+
const newWidth = e.clientX - RAIL_WIDTH
|
|
100
|
+
if (newWidth < SIDEBAR_COLLAPSE_THRESHOLD) {
|
|
101
|
+
setIsCollapsed(true)
|
|
102
|
+
} else {
|
|
87
103
|
setIsCollapsed(false)
|
|
88
|
-
setWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(
|
|
104
|
+
setWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(newWidth, SIDEBAR_MAX_WIDTH)))
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
107
|
const handlePointerUp = () => {
|
|
92
108
|
isResizing.current = false
|
|
93
|
-
isExpanding.current = false
|
|
94
109
|
setIsDragging(false)
|
|
95
110
|
document.body.style.cursor = ''
|
|
96
111
|
document.body.style.userSelect = ''
|
|
@@ -103,37 +118,36 @@ function LeftColumn({
|
|
|
103
118
|
}
|
|
104
119
|
}, [setWidth, setIsCollapsed, setIsDragging])
|
|
105
120
|
|
|
106
|
-
if (isCollapsed) {
|
|
107
|
-
return (
|
|
108
|
-
<div
|
|
109
|
-
className="relative h-full w-2 flex-shrink-0 cursor-col-resize transition-colors hover:bg-primary/20"
|
|
110
|
-
onPointerDown={handleGrabDown}
|
|
111
|
-
title="Expand sidebar"
|
|
112
|
-
/>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
121
|
return (
|
|
117
|
-
<div
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
<div className="relative z-10 flex h-full flex-shrink-0 bg-sidebar text-sidebar-foreground">
|
|
123
|
+
<IconRail
|
|
124
|
+
activeTab={activePanel}
|
|
125
|
+
collapsed={isCollapsed}
|
|
126
|
+
onIconClick={handleRailClick}
|
|
127
|
+
tabs={tabs}
|
|
128
|
+
/>
|
|
129
|
+
{!isCollapsed && (
|
|
130
|
+
<div
|
|
131
|
+
className="relative flex h-full flex-col"
|
|
132
|
+
style={{
|
|
133
|
+
width,
|
|
134
|
+
transition: isDragging ? 'none' : 'width 150ms ease',
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<div className="relative flex flex-1 flex-col overflow-hidden">
|
|
138
|
+
{renderTabContent(activePanel)}
|
|
139
|
+
{sidebarOverlay && <div className="absolute inset-0 z-50">{sidebarOverlay}</div>}
|
|
140
|
+
</div>
|
|
129
141
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
{/* Resize handle + hit area */}
|
|
143
|
+
<div
|
|
144
|
+
className="absolute inset-y-0 -right-3 z-[100] flex w-6 cursor-col-resize items-center justify-center"
|
|
145
|
+
onPointerDown={handleResizerDown}
|
|
146
|
+
>
|
|
147
|
+
<div className="h-8 w-1 rounded-full bg-neutral-500" />
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
137
151
|
</div>
|
|
138
152
|
)
|
|
139
153
|
}
|
|
@@ -145,11 +159,13 @@ function RightColumn({
|
|
|
145
159
|
toolbarRight,
|
|
146
160
|
children,
|
|
147
161
|
overlays,
|
|
162
|
+
stageOverlay,
|
|
148
163
|
}: {
|
|
149
164
|
toolbarLeft?: ReactNode
|
|
150
165
|
toolbarRight?: ReactNode
|
|
151
166
|
children: ReactNode
|
|
152
167
|
overlays?: ReactNode
|
|
168
|
+
stageOverlay?: ReactNode
|
|
153
169
|
}) {
|
|
154
170
|
return (
|
|
155
171
|
<div
|
|
@@ -169,10 +185,16 @@ function RightColumn({
|
|
|
169
185
|
)}
|
|
170
186
|
{/* Canvas area */}
|
|
171
187
|
<div className="relative flex-1 overflow-hidden">{children}</div>
|
|
172
|
-
{/*
|
|
188
|
+
{/* Stage overlay — replaces the canvas visually (e.g. studio gallery)
|
|
189
|
+
while keeping it mounted. Sits below the viewer toolbar (z-20) so
|
|
190
|
+
the stage switch stays reachable. */}
|
|
191
|
+
{stageOverlay && <div className="absolute inset-0 z-10">{stageOverlay}</div>}
|
|
192
|
+
{/* Overlays scoped to the viewer column. `data-viewer-bounds` marks the
|
|
193
|
+
draggable region the floating inspector clamps itself to. */}
|
|
173
194
|
{overlays && (
|
|
174
195
|
<div
|
|
175
196
|
className="pointer-events-none absolute inset-0 z-30"
|
|
197
|
+
data-viewer-bounds
|
|
176
198
|
style={{ transform: 'translateZ(0)' }}
|
|
177
199
|
>
|
|
178
200
|
{overlays}
|
|
@@ -193,6 +215,7 @@ export interface EditorLayoutV2Props {
|
|
|
193
215
|
viewerToolbarRight?: ReactNode
|
|
194
216
|
viewerContent: ReactNode
|
|
195
217
|
overlays?: ReactNode
|
|
218
|
+
stageOverlay?: ReactNode
|
|
196
219
|
}
|
|
197
220
|
|
|
198
221
|
export function EditorLayoutV2({
|
|
@@ -204,6 +227,7 @@ export function EditorLayoutV2({
|
|
|
204
227
|
viewerToolbarRight,
|
|
205
228
|
viewerContent,
|
|
206
229
|
overlays,
|
|
230
|
+
stageOverlay,
|
|
207
231
|
}: EditorLayoutV2Props) {
|
|
208
232
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
209
233
|
const isMobile = useIsMobile()
|
|
@@ -239,6 +263,7 @@ export function EditorLayoutV2({
|
|
|
239
263
|
)}
|
|
240
264
|
<RightColumn
|
|
241
265
|
overlays={overlays}
|
|
266
|
+
stageOverlay={stageOverlay}
|
|
242
267
|
toolbarLeft={isCaptureMode ? undefined : viewerToolbarLeft}
|
|
243
268
|
toolbarRight={isCaptureMode ? undefined : viewerToolbarRight}
|
|
244
269
|
>
|
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { emitter, useScene } from '@pascal-app/core'
|
|
3
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
5
|
import { useThree } from '@react-three/fiber'
|
|
5
6
|
import { useEffect } from 'react'
|
|
6
|
-
import
|
|
7
|
+
import * as THREE from 'three'
|
|
7
8
|
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
|
|
8
9
|
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
|
|
10
|
+
import { exportSceneToGlb, nextFrames, prepareSceneForExport } from '../../lib/glb-export'
|
|
11
|
+
|
|
12
|
+
// prepareSceneForExport neutralises container meshes (door/window hitbox roots,
|
|
13
|
+
// material-less renderables) with an attribute-less geometry — GLTFExporter
|
|
14
|
+
// emits those as plain transform nodes, but STL/OBJExporter read
|
|
15
|
+
// `position.count` unconditionally and crash. Swap in a geometry with an empty
|
|
16
|
+
// (count-0) position so they iterate zero vertices instead. Shared: the export
|
|
17
|
+
// scene is a throwaway clone, only its geometry *ref* is swapped.
|
|
18
|
+
const EMPTY_POSITION_GEOMETRY = new THREE.BufferGeometry()
|
|
19
|
+
EMPTY_POSITION_GEOMETRY.setAttribute(
|
|
20
|
+
'position',
|
|
21
|
+
new THREE.Float32BufferAttribute(new Float32Array(0), 3),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
function ensurePositionAttributes(root: THREE.Object3D) {
|
|
25
|
+
root.traverse((object) => {
|
|
26
|
+
const renderable = object as THREE.Mesh & { isLine?: boolean; isPoints?: boolean }
|
|
27
|
+
if (!(renderable.isMesh || renderable.isLine || renderable.isPoints)) return
|
|
28
|
+
if (!renderable.geometry?.getAttribute('position')) {
|
|
29
|
+
renderable.geometry = EMPTY_POSITION_GEOMETRY
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
9
33
|
|
|
10
34
|
export function ExportManager() {
|
|
11
35
|
const scene = useThree((state) => state.scene)
|
|
@@ -22,40 +46,53 @@ export function ExportManager() {
|
|
|
22
46
|
|
|
23
47
|
const date = new Date().toISOString().split('T')[0]
|
|
24
48
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
// Signal export so instanced kinds (trees/flowers/grass) swap their
|
|
50
|
+
// invisible proxy for real, exportable geometry, then wait for the
|
|
51
|
+
// commit before cloning the scene graph (same dance as BakeExporter —
|
|
52
|
+
// without it every plant exports as its raycast collider, a white box).
|
|
53
|
+
useViewer.getState().setExporting(true)
|
|
54
|
+
try {
|
|
55
|
+
await nextFrames()
|
|
32
56
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
57
|
+
if (format === 'glb') {
|
|
58
|
+
const buffer = await exportSceneToGlb(sceneGroup, useScene.getState().nodes)
|
|
59
|
+
const blob = new Blob([buffer], { type: 'model/gltf-binary' })
|
|
60
|
+
downloadBlob(blob, `model_${date}.glb`)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
40
63
|
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
// Hide editor affordances that live on the scene layer (selection handles,
|
|
65
|
+
// ceiling/site brackets) and let wall-cutout reveal all walls — the same
|
|
66
|
+
// synchronous capture path thumbnails use. We clone the scene inside the
|
|
67
|
+
// window, so the export snapshots the clean building, then restore.
|
|
68
|
+
emitter.emit('thumbnail:before-capture', undefined)
|
|
69
|
+
let prepared: ReturnType<typeof prepareSceneForExport>
|
|
70
|
+
try {
|
|
71
|
+
prepared = prepareSceneForExport(sceneGroup, useScene.getState().nodes)
|
|
72
|
+
} finally {
|
|
73
|
+
emitter.emit('thumbnail:after-capture', undefined)
|
|
74
|
+
}
|
|
75
|
+
const { scene: exportScene } = prepared
|
|
76
|
+
ensurePositionAttributes(exportScene)
|
|
43
77
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
if (format === 'stl') {
|
|
79
|
+
const exporter = new STLExporter()
|
|
80
|
+
const result = exporter.parse(exportScene, { binary: true })
|
|
81
|
+
const blob = new Blob([result], { type: 'model/stl' })
|
|
82
|
+
downloadBlob(blob, `model_${date}.stl`)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (format === 'obj') {
|
|
87
|
+
const exporter = new OBJExporter()
|
|
88
|
+
const result = exporter.parse(exportScene)
|
|
89
|
+
const blob = new Blob([result], { type: 'model/obj' })
|
|
90
|
+
downloadBlob(blob, `model_${date}.obj`)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
useViewer.getState().setExporting(false)
|
|
95
|
+
}
|
|
59
96
|
}
|
|
60
97
|
|
|
61
98
|
setExportScene(exportFn)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
type FenceNode,
|
|
6
|
+
getFenceControlHandle,
|
|
7
|
+
isSplineFence,
|
|
8
|
+
sceneRegistry,
|
|
9
|
+
useLiveNodeOverrides,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
+
import { createPortal, useFrame } from '@react-three/fiber'
|
|
14
|
+
import { useMemo, useState } from 'react'
|
|
15
|
+
import { BufferGeometry, type Object3D, Vector3 } from 'three'
|
|
16
|
+
import { EDITOR_LAYER } from '../../lib/constants'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Straight connecting line through each spline-fence control point joining its
|
|
20
|
+
* two tangent handle ends (the classic pen-tool look). The handle *dots* are
|
|
21
|
+
* registry tap-handles (see `fence/definition.ts`); this overlay only draws
|
|
22
|
+
* the line between them, in both views' 3D scene.
|
|
23
|
+
*
|
|
24
|
+
* Must match the handle placement's arm scale so the line ends land exactly on
|
|
25
|
+
* the dots. Portals into the fence's own Object3D so it inherits the same world
|
|
26
|
+
* transform as the geometry (path coords are node-local plan meters), and reads
|
|
27
|
+
* the live override so the line tracks an in-flight tangent / point drag.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Keep in sync with TANGENT_HANDLE_ARM_SCALE in fence/definition.ts.
|
|
31
|
+
const TANGENT_HANDLE_ARM_SCALE = 3
|
|
32
|
+
const LINE_LIFT_Y = 0.02
|
|
33
|
+
|
|
34
|
+
export function FenceTangentLines3D() {
|
|
35
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
36
|
+
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
37
|
+
|
|
38
|
+
const rawNode = useScene((s) => (selectedId ? s.nodes[selectedId as AnyNodeId] : null))
|
|
39
|
+
const liveOverride = useLiveNodeOverrides((s) =>
|
|
40
|
+
selectedId ? s.overrides.get(selectedId as AnyNodeId) : undefined,
|
|
41
|
+
)
|
|
42
|
+
const fence = useMemo<FenceNode | null>(() => {
|
|
43
|
+
if (rawNode?.type !== 'fence') return null
|
|
44
|
+
const merged = liveOverride ? ({ ...rawNode, ...liveOverride } as FenceNode) : rawNode
|
|
45
|
+
return isSplineFence(merged) ? merged : null
|
|
46
|
+
}, [rawNode, liveOverride])
|
|
47
|
+
|
|
48
|
+
const [object, setObject] = useState<{ id: AnyNodeId; object: Object3D } | null>(null)
|
|
49
|
+
const selectedObject = selectedId && object?.id === selectedId ? object.object : null
|
|
50
|
+
|
|
51
|
+
useFrame(() => {
|
|
52
|
+
if (!selectedId || selectedObject) return
|
|
53
|
+
const next = sceneRegistry.nodes.get(selectedId)
|
|
54
|
+
if (next) setObject({ id: selectedId as AnyNodeId, object: next })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const geometry = useMemo(() => {
|
|
58
|
+
if (!fence?.path) return null
|
|
59
|
+
const positions: Vector3[] = []
|
|
60
|
+
for (let i = 0; i < fence.path.length; i += 1) {
|
|
61
|
+
const point = fence.path[i]!
|
|
62
|
+
const handle = getFenceControlHandle(fence.path, fence.tangents, i)
|
|
63
|
+
const ax = handle.x * TANGENT_HANDLE_ARM_SCALE
|
|
64
|
+
const az = handle.y * TANGENT_HANDLE_ARM_SCALE
|
|
65
|
+
// One disjoint segment per point — `lineSegments` pairs vertices, so the
|
|
66
|
+
// in/out ends connect through the point without joining across points.
|
|
67
|
+
positions.push(new Vector3(point[0] - ax, LINE_LIFT_Y, point[1] - az))
|
|
68
|
+
positions.push(new Vector3(point[0] + ax, LINE_LIFT_Y, point[1] + az))
|
|
69
|
+
}
|
|
70
|
+
return new BufferGeometry().setFromPoints(positions)
|
|
71
|
+
}, [fence])
|
|
72
|
+
|
|
73
|
+
if (!(fence && selectedObject && geometry)) return null
|
|
74
|
+
|
|
75
|
+
return createPortal(
|
|
76
|
+
<lineSegments frustumCulled={false} geometry={geometry} layers={EDITOR_LAYER} renderOrder={3}>
|
|
77
|
+
<lineBasicNodeMaterial
|
|
78
|
+
color="#8381ed"
|
|
79
|
+
depthTest={false}
|
|
80
|
+
depthWrite={false}
|
|
81
|
+
opacity={0.85}
|
|
82
|
+
transparent
|
|
83
|
+
/>
|
|
84
|
+
</lineSegments>,
|
|
85
|
+
selectedObject,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
CeilingNode,
|
|
6
|
+
ColumnNode,
|
|
7
|
+
ElevatorNode,
|
|
8
|
+
LevelNode,
|
|
9
|
+
nodeRegistry,
|
|
10
|
+
registerNode,
|
|
11
|
+
ShelfNode,
|
|
12
|
+
SiteNode,
|
|
13
|
+
sceneRegistry,
|
|
14
|
+
useScene,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
import { BoxGeometry, Group, Mesh, MeshBasicMaterial } from 'three'
|
|
17
|
+
import { buildFirstPersonColliderWorldFromRegistry } from './build-collider-world'
|
|
18
|
+
|
|
19
|
+
function registerColliderDefinition(
|
|
20
|
+
kind: AnyNode['type'],
|
|
21
|
+
schema: AnyNodeDefinition['schema'],
|
|
22
|
+
category: AnyNodeDefinition['category'],
|
|
23
|
+
surfaceRole?: AnyNodeDefinition['surfaceRole'],
|
|
24
|
+
) {
|
|
25
|
+
registerNode({
|
|
26
|
+
kind,
|
|
27
|
+
schema,
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
category,
|
|
30
|
+
surfaceRole,
|
|
31
|
+
capabilities: {},
|
|
32
|
+
} as AnyNodeDefinition)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mountNode(
|
|
36
|
+
node: AnyNode,
|
|
37
|
+
box: [number, number, number],
|
|
38
|
+
position: [number, number, number],
|
|
39
|
+
) {
|
|
40
|
+
const group = new Group()
|
|
41
|
+
const mesh = new Mesh(new BoxGeometry(box[0], box[1], box[2]), new MeshBasicMaterial())
|
|
42
|
+
mesh.position.set(position[0], position[1], position[2])
|
|
43
|
+
group.add(mesh)
|
|
44
|
+
group.updateMatrixWorld(true)
|
|
45
|
+
sceneRegistry.nodes.set(node.id, group)
|
|
46
|
+
sceneRegistry.byType[node.type]!.add(node.id)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mountRegistryGroup(node: AnyNode) {
|
|
50
|
+
const group = new Group()
|
|
51
|
+
group.updateMatrixWorld(true)
|
|
52
|
+
sceneRegistry.nodes.set(node.id, group)
|
|
53
|
+
sceneRegistry.byType[node.type]!.add(node.id)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setSceneNodes(nodes: AnyNode[]) {
|
|
57
|
+
useScene.setState({
|
|
58
|
+
nodes: Object.fromEntries(nodes.map((node) => [node.id, node])),
|
|
59
|
+
rootNodeIds: nodes.map((node) => node.id),
|
|
60
|
+
} as never)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
sceneRegistry.clear()
|
|
66
|
+
nodeRegistry._reset()
|
|
67
|
+
useScene.setState({ nodes: {}, rootNodeIds: [] } as never)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('includes structure and furnish nodes discovered through the node registry', () => {
|
|
71
|
+
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
72
|
+
registerColliderDefinition('shelf', ShelfNode, 'furnish')
|
|
73
|
+
|
|
74
|
+
const column = ColumnNode.parse({ id: 'column_test' })
|
|
75
|
+
const shelf = ShelfNode.parse({ id: 'shelf_test', position: [3, 0, 0] })
|
|
76
|
+
setSceneNodes([column, shelf])
|
|
77
|
+
mountNode(column, [1, 2, 1], [0, 1, 0])
|
|
78
|
+
mountNode(shelf, [2, 1, 1], [3, 0.5, 0])
|
|
79
|
+
|
|
80
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
81
|
+
|
|
82
|
+
expect(world).not.toBeNull()
|
|
83
|
+
expect(world?.bounds?.min.x).toBeCloseTo(-0.5)
|
|
84
|
+
expect(world?.bounds?.max.x).toBeCloseTo(4)
|
|
85
|
+
world?.dispose()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('excludes ceiling surfaces so the walkthrough player passes through them', () => {
|
|
89
|
+
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
90
|
+
registerColliderDefinition('ceiling', CeilingNode, 'structure', 'ceiling')
|
|
91
|
+
|
|
92
|
+
const column = ColumnNode.parse({ id: 'column_test' })
|
|
93
|
+
const ceiling = CeilingNode.parse({ id: 'ceiling_test', polygon: [] })
|
|
94
|
+
setSceneNodes([column, ceiling])
|
|
95
|
+
mountNode(column, [1, 2, 1], [0, 1, 0])
|
|
96
|
+
// A wide ceiling at head height — if it were collected, bounds would span ±5.
|
|
97
|
+
mountNode(ceiling, [10, 0.1, 10], [0, 2.5, 0])
|
|
98
|
+
|
|
99
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
100
|
+
|
|
101
|
+
expect(world).not.toBeNull()
|
|
102
|
+
// Bounds reflect only the 1×1 column; the ceiling contributed no geometry.
|
|
103
|
+
expect(world?.bounds?.min.x).toBeCloseTo(-0.5)
|
|
104
|
+
expect(world?.bounds?.max.x).toBeCloseTo(0.5)
|
|
105
|
+
world?.dispose()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('skips meshes hidden by an invisible ancestor (stale roof segment CSG)', () => {
|
|
109
|
+
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
110
|
+
|
|
111
|
+
// Mirror the roof's segments-wrapper shape: the registered mesh's own
|
|
112
|
+
// visible flag stays true while a hidden wrapper hides it at render
|
|
113
|
+
// time. The collider must match the render, not the own-flag.
|
|
114
|
+
const column = ColumnNode.parse({ id: 'column_test' })
|
|
115
|
+
const visibleColumn = ColumnNode.parse({ id: 'column_visible', position: [3, 0, 0] })
|
|
116
|
+
setSceneNodes([column, visibleColumn])
|
|
117
|
+
|
|
118
|
+
const wrapper = new Group()
|
|
119
|
+
wrapper.visible = false
|
|
120
|
+
const hiddenMesh = new Mesh(new BoxGeometry(10, 2, 10), new MeshBasicMaterial())
|
|
121
|
+
wrapper.add(hiddenMesh)
|
|
122
|
+
wrapper.updateMatrixWorld(true)
|
|
123
|
+
sceneRegistry.nodes.set(column.id, hiddenMesh)
|
|
124
|
+
sceneRegistry.byType[column.type]!.add(column.id)
|
|
125
|
+
|
|
126
|
+
mountNode(visibleColumn, [1, 2, 1], [3, 1, 0])
|
|
127
|
+
|
|
128
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
129
|
+
|
|
130
|
+
expect(world).not.toBeNull()
|
|
131
|
+
// Bounds reflect only the visible 1×1 column at x = 3; the 10×10 mesh
|
|
132
|
+
// under the hidden wrapper contributed no geometry.
|
|
133
|
+
expect(world?.bounds?.min.x).toBeCloseTo(2.5)
|
|
134
|
+
expect(world?.bounds?.max.x).toBeCloseTo(3.5)
|
|
135
|
+
world?.dispose()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('leaves elevators to their dedicated dynamic collider meshes', () => {
|
|
139
|
+
registerColliderDefinition('elevator', ElevatorNode, 'structure')
|
|
140
|
+
|
|
141
|
+
const elevator = ElevatorNode.parse({ id: 'elevator_test' })
|
|
142
|
+
setSceneNodes([elevator])
|
|
143
|
+
mountNode(elevator, [2, 3, 2], [0, 1.5, 0])
|
|
144
|
+
|
|
145
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
146
|
+
|
|
147
|
+
expect(world).toBeNull()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('adds a fallback floor for a visible level with no slab', () => {
|
|
151
|
+
const level = LevelNode.parse({ id: 'level_test', level: 0 })
|
|
152
|
+
setSceneNodes([level])
|
|
153
|
+
mountRegistryGroup(level)
|
|
154
|
+
|
|
155
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
156
|
+
|
|
157
|
+
expect(world).not.toBeNull()
|
|
158
|
+
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
159
|
+
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
160
|
+
world?.dispose()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('adds a site ground collider so a spawn on bare ground has a floor', () => {
|
|
164
|
+
const site = SiteNode.parse({ id: 'site_test' })
|
|
165
|
+
setSceneNodes([site])
|
|
166
|
+
mountRegistryGroup(site)
|
|
167
|
+
|
|
168
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
169
|
+
|
|
170
|
+
expect(world).not.toBeNull()
|
|
171
|
+
// Ground slab sits just below the site ground plane (y = 0).
|
|
172
|
+
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
173
|
+
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
174
|
+
// The ground collider extends far past the site polygon so stepping out of
|
|
175
|
+
// the site boundary never drops the player below the ground plane.
|
|
176
|
+
expect(world?.bounds?.min.x).toBeCloseTo(-1000)
|
|
177
|
+
expect(world?.bounds?.max.x).toBeCloseTo(1000)
|
|
178
|
+
expect(world?.bounds?.min.z).toBeCloseTo(-1000)
|
|
179
|
+
expect(world?.bounds?.max.z).toBeCloseTo(1000)
|
|
180
|
+
world?.dispose()
|
|
181
|
+
})
|
|
182
|
+
})
|