@pascal-app/editor 0.9.1 → 1.0.0-beta.1
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 +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -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 +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- 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-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- 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 +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- 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 +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- 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/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- 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 +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- 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 +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- 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/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- 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 +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- 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 +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -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 +44 -3
- 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/tab-bar.tsx +42 -28
- 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/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- 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 +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- 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 +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- 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 +188 -0
- package/src/lib/measurements.ts +210 -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/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- 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 +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -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 +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -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 +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- 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 +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
import useDeleteConfirmation from '../../store/use-delete-confirmation'
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from '../ui/primitives/dialog'
|
|
13
|
+
|
|
14
|
+
export function DeleteConfirmationDialog() {
|
|
15
|
+
const request = useDeleteConfirmation((state) => state.request)
|
|
16
|
+
const cancel = useDeleteConfirmation((state) => state.cancel)
|
|
17
|
+
const confirm = useDeleteConfirmation((state) => state.confirm)
|
|
18
|
+
|
|
19
|
+
useEffect(() => cancel, [cancel])
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Dialog onOpenChange={(open) => !open && cancel()} open={request !== null}>
|
|
23
|
+
<DialogContent
|
|
24
|
+
className="border-border/70 bg-background/95 shadow-2xl backdrop-blur-xl sm:max-w-md"
|
|
25
|
+
data-delete-confirmation-dialog
|
|
26
|
+
showCloseButton={false}
|
|
27
|
+
>
|
|
28
|
+
<DialogHeader>
|
|
29
|
+
<DialogTitle>Delete {request?.count ?? 0} elements?</DialogTitle>
|
|
30
|
+
<DialogDescription>
|
|
31
|
+
This removes every selected element. You can undo the deletion while it remains in the
|
|
32
|
+
editor history.
|
|
33
|
+
</DialogDescription>
|
|
34
|
+
</DialogHeader>
|
|
35
|
+
<DialogFooter>
|
|
36
|
+
<button
|
|
37
|
+
className="rounded-full border border-border px-4 py-2 text-sm transition-colors hover:bg-accent"
|
|
38
|
+
onClick={cancel}
|
|
39
|
+
type="button"
|
|
40
|
+
>
|
|
41
|
+
Cancel
|
|
42
|
+
</button>
|
|
43
|
+
<button
|
|
44
|
+
className="rounded-full bg-red-600 px-4 py-2 text-sm text-white transition-colors hover:bg-red-700"
|
|
45
|
+
onClick={confirm}
|
|
46
|
+
type="button"
|
|
47
|
+
>
|
|
48
|
+
Delete
|
|
49
|
+
</button>
|
|
50
|
+
</DialogFooter>
|
|
51
|
+
</DialogContent>
|
|
52
|
+
</Dialog>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -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
|
}
|
|
@@ -52,6 +52,15 @@ function LeftColumn({
|
|
|
52
52
|
}
|
|
53
53
|
}, [activePanel])
|
|
54
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
|
+
|
|
55
64
|
const handleResizerDown = useCallback(
|
|
56
65
|
(e: React.PointerEvent) => {
|
|
57
66
|
e.preventDefault()
|
|
@@ -150,11 +159,13 @@ function RightColumn({
|
|
|
150
159
|
toolbarRight,
|
|
151
160
|
children,
|
|
152
161
|
overlays,
|
|
162
|
+
stageOverlay,
|
|
153
163
|
}: {
|
|
154
164
|
toolbarLeft?: ReactNode
|
|
155
165
|
toolbarRight?: ReactNode
|
|
156
166
|
children: ReactNode
|
|
157
167
|
overlays?: ReactNode
|
|
168
|
+
stageOverlay?: ReactNode
|
|
158
169
|
}) {
|
|
159
170
|
return (
|
|
160
171
|
<div
|
|
@@ -174,6 +185,10 @@ function RightColumn({
|
|
|
174
185
|
)}
|
|
175
186
|
{/* Canvas area */}
|
|
176
187
|
<div className="relative flex-1 overflow-hidden">{children}</div>
|
|
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>}
|
|
177
192
|
{/* Overlays scoped to the viewer column. `data-viewer-bounds` marks the
|
|
178
193
|
draggable region the floating inspector clamps itself to. */}
|
|
179
194
|
{overlays && (
|
|
@@ -200,6 +215,7 @@ export interface EditorLayoutV2Props {
|
|
|
200
215
|
viewerToolbarRight?: ReactNode
|
|
201
216
|
viewerContent: ReactNode
|
|
202
217
|
overlays?: ReactNode
|
|
218
|
+
stageOverlay?: ReactNode
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
export function EditorLayoutV2({
|
|
@@ -211,6 +227,7 @@ export function EditorLayoutV2({
|
|
|
211
227
|
viewerToolbarRight,
|
|
212
228
|
viewerContent,
|
|
213
229
|
overlays,
|
|
230
|
+
stageOverlay,
|
|
214
231
|
}: EditorLayoutV2Props) {
|
|
215
232
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
216
233
|
const isMobile = useIsMobile()
|
|
@@ -246,6 +263,7 @@ export function EditorLayoutV2({
|
|
|
246
263
|
)}
|
|
247
264
|
<RightColumn
|
|
248
265
|
overlays={overlays}
|
|
266
|
+
stageOverlay={stageOverlay}
|
|
249
267
|
toolbarLeft={isCaptureMode ? undefined : viewerToolbarLeft}
|
|
250
268
|
toolbarRight={isCaptureMode ? undefined : viewerToolbarRight}
|
|
251
269
|
>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { sceneRegistry } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { Html } from '@react-three/drei'
|
|
6
|
+
import { useFrame } from '@react-three/fiber'
|
|
7
|
+
import { memo, useMemo, useRef } from 'react'
|
|
8
|
+
import { BoxGeometry, CircleGeometry, type Group } from 'three'
|
|
9
|
+
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
10
|
+
import { EDITOR_LAYER } from '../../lib/constants'
|
|
11
|
+
import useElevationGuides from '../../store/use-elevation-guides'
|
|
12
|
+
import { formatMeasurement } from './measurement-pill'
|
|
13
|
+
|
|
14
|
+
const LINE_COLOR = 0x81_8c_f8
|
|
15
|
+
const PILL_COLOR = '#6366f1'
|
|
16
|
+
const GUIDE_LIFT = 0.025
|
|
17
|
+
const GUIDE_LENGTH = 2.4
|
|
18
|
+
const DASH_LENGTH = 0.16
|
|
19
|
+
const DASH_GAP = 0.1
|
|
20
|
+
const LINE_WIDTH = 0.035
|
|
21
|
+
const DOT_RADIUS = 0.07
|
|
22
|
+
|
|
23
|
+
const guideMaterial = new MeshBasicNodeMaterial({
|
|
24
|
+
color: LINE_COLOR,
|
|
25
|
+
depthTest: false,
|
|
26
|
+
depthWrite: false,
|
|
27
|
+
toneMapped: false,
|
|
28
|
+
transparent: true,
|
|
29
|
+
opacity: 0.9,
|
|
30
|
+
})
|
|
31
|
+
const dotMaterial = new MeshBasicNodeMaterial({
|
|
32
|
+
color: LINE_COLOR,
|
|
33
|
+
depthTest: false,
|
|
34
|
+
depthWrite: false,
|
|
35
|
+
toneMapped: false,
|
|
36
|
+
transparent: true,
|
|
37
|
+
opacity: 0.95,
|
|
38
|
+
})
|
|
39
|
+
const DASH_GEOMETRY = new BoxGeometry(1, 1, 1)
|
|
40
|
+
const DOT_GEOMETRY = new CircleGeometry(1, 24)
|
|
41
|
+
|
|
42
|
+
type Vec3 = [number, number, number]
|
|
43
|
+
|
|
44
|
+
export const Elevation3DGuideLayer = memo(function Elevation3DGuideLayer() {
|
|
45
|
+
const guide = useElevationGuides((state) => state.guide)
|
|
46
|
+
const unit = useViewer((state) => state.unit)
|
|
47
|
+
const groupRef = useRef<Group>(null)
|
|
48
|
+
|
|
49
|
+
useFrame(() => {
|
|
50
|
+
const group = groupRef.current
|
|
51
|
+
if (!(group && guide)) return
|
|
52
|
+
const levelObject = sceneRegistry.nodes.get(guide.levelId)
|
|
53
|
+
group.position.y = (levelObject?.position.y ?? 0) + guide.elevation + GUIDE_LIFT
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const dashes = useMemo(() => {
|
|
57
|
+
if (!guide) return [] as Vec3[]
|
|
58
|
+
const [cx, cz] = guide.center
|
|
59
|
+
const [dx, dz] = guide.direction
|
|
60
|
+
const start = -GUIDE_LENGTH / 2
|
|
61
|
+
const period = DASH_LENGTH + DASH_GAP
|
|
62
|
+
const centers: Vec3[] = []
|
|
63
|
+
for (let offset = start + DASH_LENGTH / 2; offset < GUIDE_LENGTH / 2; offset += period) {
|
|
64
|
+
centers.push([cx + dx * offset, 0, cz + dz * offset])
|
|
65
|
+
}
|
|
66
|
+
return centers
|
|
67
|
+
}, [guide])
|
|
68
|
+
|
|
69
|
+
if (!guide) return null
|
|
70
|
+
|
|
71
|
+
const angleY = -Math.atan2(guide.direction[1], guide.direction[0])
|
|
72
|
+
const labelPosition: Vec3 = [
|
|
73
|
+
guide.center[0] - guide.direction[1] * 0.65,
|
|
74
|
+
0.16,
|
|
75
|
+
guide.center[1] + guide.direction[0] * 0.65,
|
|
76
|
+
]
|
|
77
|
+
const elevationText =
|
|
78
|
+
Math.abs(guide.elevation) < 1e-6
|
|
79
|
+
? formatMeasurement(0, unit)
|
|
80
|
+
: `${guide.elevation < 0 ? '-' : '+'}${formatMeasurement(Math.abs(guide.elevation), unit)}`
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<group ref={groupRef}>
|
|
84
|
+
{dashes.map((position, index) => (
|
|
85
|
+
<mesh
|
|
86
|
+
geometry={DASH_GEOMETRY}
|
|
87
|
+
key={index}
|
|
88
|
+
layers={EDITOR_LAYER}
|
|
89
|
+
material={guideMaterial}
|
|
90
|
+
position={position}
|
|
91
|
+
renderOrder={1010}
|
|
92
|
+
rotation={[0, angleY, 0]}
|
|
93
|
+
scale={[DASH_LENGTH, 0.002, LINE_WIDTH]}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
<mesh
|
|
97
|
+
geometry={DOT_GEOMETRY}
|
|
98
|
+
layers={EDITOR_LAYER}
|
|
99
|
+
material={dotMaterial}
|
|
100
|
+
position={[guide.center[0], 0.002, guide.center[1]]}
|
|
101
|
+
renderOrder={1011}
|
|
102
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
103
|
+
scale={[DOT_RADIUS, DOT_RADIUS, DOT_RADIUS]}
|
|
104
|
+
/>
|
|
105
|
+
<Html
|
|
106
|
+
center
|
|
107
|
+
position={labelPosition}
|
|
108
|
+
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
109
|
+
zIndexRange={[25, 0]}
|
|
110
|
+
>
|
|
111
|
+
<div
|
|
112
|
+
className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-medium font-sans text-[11px] text-white"
|
|
113
|
+
style={{ backgroundColor: PILL_COLOR }}
|
|
114
|
+
>
|
|
115
|
+
{`${guide.label} · Y ${elevationText}`}
|
|
116
|
+
</div>
|
|
117
|
+
</Html>
|
|
118
|
+
</group>
|
|
119
|
+
)
|
|
120
|
+
})
|
|
@@ -1,12 +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 { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
|
|
7
|
+
import * as THREE from 'three'
|
|
8
8
|
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
|
|
9
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
|
+
}
|
|
10
33
|
|
|
11
34
|
export function ExportManager() {
|
|
12
35
|
const scene = useThree((state) => state.scene)
|
|
@@ -22,42 +45,54 @@ export function ExportManager() {
|
|
|
22
45
|
}
|
|
23
46
|
|
|
24
47
|
const date = new Date().toISOString().split('T')[0]
|
|
25
|
-
const exportScene = prepareSceneForExport(sceneGroup)
|
|
26
|
-
|
|
27
|
-
if (format === 'stl') {
|
|
28
|
-
const exporter = new STLExporter()
|
|
29
|
-
const result = exporter.parse(exportScene, { binary: true })
|
|
30
|
-
const blob = new Blob([result], { type: 'model/stl' })
|
|
31
|
-
downloadBlob(blob, `model_${date}.stl`)
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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()
|
|
56
|
+
|
|
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
|
+
}
|
|
63
|
+
|
|
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)
|
|
77
|
+
|
|
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)
|
|
41
95
|
}
|
|
42
|
-
|
|
43
|
-
// Default: GLB export (existing behavior)
|
|
44
|
-
const exporter = new GLTFExporter()
|
|
45
|
-
|
|
46
|
-
return new Promise<void>((resolve, reject) => {
|
|
47
|
-
exporter.parse(
|
|
48
|
-
exportScene,
|
|
49
|
-
(gltf) => {
|
|
50
|
-
const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' })
|
|
51
|
-
downloadBlob(blob, `model_${date}.glb`)
|
|
52
|
-
resolve()
|
|
53
|
-
},
|
|
54
|
-
(error) => {
|
|
55
|
-
console.error('Export error:', error)
|
|
56
|
-
reject(error)
|
|
57
|
-
},
|
|
58
|
-
{ binary: true },
|
|
59
|
-
)
|
|
60
|
-
})
|
|
61
96
|
}
|
|
62
97
|
|
|
63
98
|
setExportScene(exportFn)
|
|
@@ -70,33 +105,6 @@ export function ExportManager() {
|
|
|
70
105
|
return null
|
|
71
106
|
}
|
|
72
107
|
|
|
73
|
-
function prepareSceneForExport(source: Object3D) {
|
|
74
|
-
const clone = source.clone(true)
|
|
75
|
-
const meshesToRemove: Mesh[] = []
|
|
76
|
-
|
|
77
|
-
clone.traverse((object) => {
|
|
78
|
-
if (isMeshWithInvalidGeometry(object)) meshesToRemove.push(object)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
for (const mesh of meshesToRemove) {
|
|
82
|
-
mesh.removeFromParent()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return clone
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function isMeshWithInvalidGeometry(object: Object3D): object is Mesh {
|
|
89
|
-
if (!isMesh(object)) return false
|
|
90
|
-
|
|
91
|
-
// Three exporters can crash when a Mesh has no readable position attribute.
|
|
92
|
-
const position = object.geometry?.getAttribute('position')
|
|
93
|
-
return !position || position.count === 0
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function isMesh(object: Object3D): object is Mesh {
|
|
97
|
-
return (object as Mesh).isMesh === true
|
|
98
|
-
}
|
|
99
|
-
|
|
100
108
|
function downloadBlob(blob: Blob, filename: string) {
|
|
101
109
|
const url = URL.createObjectURL(blob)
|
|
102
110
|
const link = document.createElement('a')
|
|
@@ -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
|
+
}
|
|
@@ -2,9 +2,14 @@ import { afterEach, describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import {
|
|
3
3
|
type AnyNode,
|
|
4
4
|
type AnyNodeDefinition,
|
|
5
|
+
applyHeightPatch,
|
|
6
|
+
BuildingNode,
|
|
5
7
|
CeilingNode,
|
|
6
8
|
ColumnNode,
|
|
9
|
+
createTerrainField,
|
|
7
10
|
ElevatorNode,
|
|
11
|
+
encodeTerrainField,
|
|
12
|
+
flattenPatch,
|
|
8
13
|
LevelNode,
|
|
9
14
|
nodeRegistry,
|
|
10
15
|
registerNode,
|
|
@@ -13,7 +18,7 @@ import {
|
|
|
13
18
|
sceneRegistry,
|
|
14
19
|
useScene,
|
|
15
20
|
} from '@pascal-app/core'
|
|
16
|
-
import { BoxGeometry, Group, Mesh, MeshBasicMaterial } from 'three'
|
|
21
|
+
import { BoxGeometry, Group, Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three'
|
|
17
22
|
import { buildFirstPersonColliderWorldFromRegistry } from './build-collider-world'
|
|
18
23
|
|
|
19
24
|
function registerColliderDefinition(
|
|
@@ -46,8 +51,9 @@ function mountNode(
|
|
|
46
51
|
sceneRegistry.byType[node.type]!.add(node.id)
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
function mountRegistryGroup(node: AnyNode) {
|
|
54
|
+
function mountRegistryGroup(node: AnyNode, position: [number, number, number] = [0, 0, 0]) {
|
|
50
55
|
const group = new Group()
|
|
56
|
+
group.position.set(position[0], position[1], position[2])
|
|
51
57
|
group.updateMatrixWorld(true)
|
|
52
58
|
sceneRegistry.nodes.set(node.id, group)
|
|
53
59
|
sceneRegistry.byType[node.type]!.add(node.id)
|
|
@@ -105,6 +111,36 @@ describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
|
105
111
|
world?.dispose()
|
|
106
112
|
})
|
|
107
113
|
|
|
114
|
+
test('skips meshes hidden by an invisible ancestor (stale roof segment CSG)', () => {
|
|
115
|
+
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
116
|
+
|
|
117
|
+
// Mirror the roof's segments-wrapper shape: the registered mesh's own
|
|
118
|
+
// visible flag stays true while a hidden wrapper hides it at render
|
|
119
|
+
// time. The collider must match the render, not the own-flag.
|
|
120
|
+
const column = ColumnNode.parse({ id: 'column_test' })
|
|
121
|
+
const visibleColumn = ColumnNode.parse({ id: 'column_visible', position: [3, 0, 0] })
|
|
122
|
+
setSceneNodes([column, visibleColumn])
|
|
123
|
+
|
|
124
|
+
const wrapper = new Group()
|
|
125
|
+
wrapper.visible = false
|
|
126
|
+
const hiddenMesh = new Mesh(new BoxGeometry(10, 2, 10), new MeshBasicMaterial())
|
|
127
|
+
wrapper.add(hiddenMesh)
|
|
128
|
+
wrapper.updateMatrixWorld(true)
|
|
129
|
+
sceneRegistry.nodes.set(column.id, hiddenMesh)
|
|
130
|
+
sceneRegistry.byType[column.type]!.add(column.id)
|
|
131
|
+
|
|
132
|
+
mountNode(visibleColumn, [1, 2, 1], [3, 1, 0])
|
|
133
|
+
|
|
134
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
135
|
+
|
|
136
|
+
expect(world).not.toBeNull()
|
|
137
|
+
// Bounds reflect only the visible 1×1 column at x = 3; the 10×10 mesh
|
|
138
|
+
// under the hidden wrapper contributed no geometry.
|
|
139
|
+
expect(world?.bounds?.min.x).toBeCloseTo(2.5)
|
|
140
|
+
expect(world?.bounds?.max.x).toBeCloseTo(3.5)
|
|
141
|
+
world?.dispose()
|
|
142
|
+
})
|
|
143
|
+
|
|
108
144
|
test('leaves elevators to their dedicated dynamic collider meshes', () => {
|
|
109
145
|
registerColliderDefinition('elevator', ElevatorNode, 'structure')
|
|
110
146
|
|
|
@@ -130,6 +166,34 @@ describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
|
130
166
|
world?.dispose()
|
|
131
167
|
})
|
|
132
168
|
|
|
169
|
+
test('adds a fallback floor only for the lowest slab-less level in a building', () => {
|
|
170
|
+
const building = BuildingNode.parse({
|
|
171
|
+
id: 'building_test',
|
|
172
|
+
children: ['level_ground', 'level_upper'],
|
|
173
|
+
})
|
|
174
|
+
const groundLevel = LevelNode.parse({
|
|
175
|
+
id: 'level_ground',
|
|
176
|
+
parentId: building.id,
|
|
177
|
+
level: 0,
|
|
178
|
+
height: 3,
|
|
179
|
+
})
|
|
180
|
+
const upperLevel = LevelNode.parse({
|
|
181
|
+
id: 'level_upper',
|
|
182
|
+
parentId: building.id,
|
|
183
|
+
level: 1,
|
|
184
|
+
})
|
|
185
|
+
setSceneNodes([building, groundLevel, upperLevel])
|
|
186
|
+
mountRegistryGroup(groundLevel)
|
|
187
|
+
mountRegistryGroup(upperLevel, [0, 3, 0])
|
|
188
|
+
|
|
189
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
190
|
+
|
|
191
|
+
expect(world).not.toBeNull()
|
|
192
|
+
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
193
|
+
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
194
|
+
world?.dispose()
|
|
195
|
+
})
|
|
196
|
+
|
|
133
197
|
test('adds a site ground collider so a spawn on bare ground has a floor', () => {
|
|
134
198
|
const site = SiteNode.parse({ id: 'site_test' })
|
|
135
199
|
setSceneNodes([site])
|
|
@@ -141,9 +205,42 @@ describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
|
141
205
|
// Ground slab sits just below the site ground plane (y = 0).
|
|
142
206
|
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
143
207
|
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
expect(world?.bounds?.
|
|
208
|
+
// The ground collider extends far past the site polygon so stepping out of
|
|
209
|
+
// the site boundary never drops the player below the ground plane.
|
|
210
|
+
expect(world?.bounds?.min.x).toBeCloseTo(-1000)
|
|
211
|
+
expect(world?.bounds?.max.x).toBeCloseTo(1000)
|
|
212
|
+
expect(world?.bounds?.min.z).toBeCloseTo(-1000)
|
|
213
|
+
expect(world?.bounds?.max.z).toBeCloseTo(1000)
|
|
147
214
|
world?.dispose()
|
|
148
215
|
})
|
|
216
|
+
|
|
217
|
+
test('a sculpted site walks on its terrain instead of the flat ground slab', () => {
|
|
218
|
+
const base = createTerrainField({ cols: 17, rows: 17, spacing: 1, origin: [-8, -8] })
|
|
219
|
+
const patch = flattenPatch(base, { minX: 2, minZ: 2, maxX: 5, maxZ: 5 }, 2.5)
|
|
220
|
+
const field = applyHeightPatch(base, patch as never)
|
|
221
|
+
const site = SiteNode.parse({ id: 'site_test', terrain: encodeTerrainField(field) })
|
|
222
|
+
setSceneNodes([site])
|
|
223
|
+
mountRegistryGroup(site)
|
|
224
|
+
|
|
225
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
226
|
+
expect(world).not.toBeNull()
|
|
227
|
+
if (!world) return
|
|
228
|
+
|
|
229
|
+
// The hill is in the collider: a flat slab would top out at 0.
|
|
230
|
+
expect(world.bounds?.max.y).toBeCloseTo(2.5)
|
|
231
|
+
// And it still reaches past the site so stepping out does not drop the player.
|
|
232
|
+
expect(world.bounds?.min.x).toBeCloseTo(-1008)
|
|
233
|
+
expect(world.bounds?.max.x).toBeCloseTo(1008)
|
|
234
|
+
|
|
235
|
+
// What the player actually stands on, on the plateau and off it.
|
|
236
|
+
const raycaster = new Raycaster()
|
|
237
|
+
const standOn = (x: number, z: number) => {
|
|
238
|
+
raycaster.set(new Vector3(x, 500, z), new Vector3(0, -1, 0))
|
|
239
|
+
return raycaster.intersectObject(world.mesh, false)[0]?.point.y ?? null
|
|
240
|
+
}
|
|
241
|
+
expect(standOn(3, 3)).toBeCloseTo(2.5, 4)
|
|
242
|
+
expect(standOn(-3, -3)).toBeCloseTo(0, 4)
|
|
243
|
+
|
|
244
|
+
world.dispose()
|
|
245
|
+
})
|
|
149
246
|
})
|