@pascal-app/editor 0.8.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
CabinetModuleNode,
|
|
6
|
+
type CabinetModuleNode as CabinetModuleNodeType,
|
|
7
|
+
CabinetNode,
|
|
8
|
+
type CabinetNode as CabinetNodeType,
|
|
9
|
+
type LevelNode,
|
|
10
|
+
MeasurementNode,
|
|
11
|
+
useScene,
|
|
12
|
+
WallNode,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
15
|
+
import {
|
|
16
|
+
copySelectedNodesToEditorClipboard,
|
|
17
|
+
getEditorClipboardSnapshot,
|
|
18
|
+
pasteEditorClipboardToLevel,
|
|
19
|
+
} from './scene-clipboard'
|
|
20
|
+
|
|
21
|
+
const sourceLevelId = 'level_clipboard-source' as LevelNode['id']
|
|
22
|
+
const targetLevelId = 'level_clipboard-target' as LevelNode['id']
|
|
23
|
+
const runId = 'cabinet_clipboard-run' as CabinetNodeType['id']
|
|
24
|
+
const leftModuleId = 'cabinet-module_clipboard-left' as CabinetModuleNodeType['id']
|
|
25
|
+
const rightModuleId = 'cabinet-module_clipboard-right' as CabinetModuleNodeType['id']
|
|
26
|
+
|
|
27
|
+
function makeLevel(id: AnyNodeId, children: AnyNodeId[] = []): AnyNode {
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
type: 'level',
|
|
31
|
+
object: 'node',
|
|
32
|
+
visible: true,
|
|
33
|
+
name: '',
|
|
34
|
+
metadata: {},
|
|
35
|
+
position: [0, 0, 0],
|
|
36
|
+
rotation: 0,
|
|
37
|
+
parentId: null,
|
|
38
|
+
level: 0,
|
|
39
|
+
children,
|
|
40
|
+
} as unknown as AnyNode
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function seedCabinetRun() {
|
|
44
|
+
const sourceLevel = makeLevel(sourceLevelId, [runId])
|
|
45
|
+
const targetLevel = makeLevel(targetLevelId)
|
|
46
|
+
const run = CabinetNode.parse({
|
|
47
|
+
id: runId,
|
|
48
|
+
parentId: sourceLevelId,
|
|
49
|
+
position: [1, 0, 2],
|
|
50
|
+
rotation: 0,
|
|
51
|
+
children: [leftModuleId, rightModuleId],
|
|
52
|
+
withCountertop: true,
|
|
53
|
+
showPlinth: true,
|
|
54
|
+
})
|
|
55
|
+
const leftModule = CabinetModuleNode.parse({
|
|
56
|
+
id: leftModuleId,
|
|
57
|
+
parentId: runId,
|
|
58
|
+
position: [-0.45, 0.1, 0],
|
|
59
|
+
width: 0.9,
|
|
60
|
+
showPlinth: false,
|
|
61
|
+
withCountertop: false,
|
|
62
|
+
})
|
|
63
|
+
const rightModule = CabinetModuleNode.parse({
|
|
64
|
+
id: rightModuleId,
|
|
65
|
+
parentId: runId,
|
|
66
|
+
position: [0.45, 0.1, 0],
|
|
67
|
+
width: 0.9,
|
|
68
|
+
showPlinth: false,
|
|
69
|
+
withCountertop: false,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
useScene.setState({
|
|
73
|
+
nodes: {
|
|
74
|
+
[sourceLevel.id]: sourceLevel,
|
|
75
|
+
[targetLevel.id]: targetLevel,
|
|
76
|
+
[run.id]: run as AnyNode,
|
|
77
|
+
[leftModule.id]: leftModule as AnyNode,
|
|
78
|
+
[rightModule.id]: rightModule as AnyNode,
|
|
79
|
+
},
|
|
80
|
+
rootNodeIds: [sourceLevel.id, targetLevel.id],
|
|
81
|
+
} as never)
|
|
82
|
+
useViewer.getState().setSelection({
|
|
83
|
+
levelId: sourceLevelId,
|
|
84
|
+
selectedIds: [],
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isPastedCabinetRun(node: AnyNode): node is CabinetNodeType {
|
|
89
|
+
return node.type === 'cabinet' && node.id !== runId
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function pastedCabinetRun() {
|
|
93
|
+
return Object.values(useScene.getState().nodes).find(isPastedCabinetRun)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('scene clipboard', () => {
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
seedCabinetRun()
|
|
99
|
+
useScene.temporal.getState().clear()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('copies a selected cabinet run as one subtree instead of independent modules', () => {
|
|
103
|
+
const copied = copySelectedNodesToEditorClipboard([runId, leftModuleId, rightModuleId])
|
|
104
|
+
|
|
105
|
+
expect(copied).toBe(true)
|
|
106
|
+
expect(getEditorClipboardSnapshot()?.rootIds).toEqual([runId])
|
|
107
|
+
|
|
108
|
+
const result = pasteEditorClipboardToLevel(targetLevelId)
|
|
109
|
+
expect(result?.pastedIds).toHaveLength(1)
|
|
110
|
+
|
|
111
|
+
const pastedRun = pastedCabinetRun()
|
|
112
|
+
expect(pastedRun).toBeDefined()
|
|
113
|
+
expect(pastedRun?.parentId).toBe(targetLevelId)
|
|
114
|
+
expect(pastedRun?.children).toHaveLength(2)
|
|
115
|
+
|
|
116
|
+
for (const childId of pastedRun?.children ?? []) {
|
|
117
|
+
const child = useScene.getState().nodes[childId as AnyNodeId]
|
|
118
|
+
expect(child?.type).toBe('cabinet-module')
|
|
119
|
+
expect(child?.parentId).toBe(pastedRun?.id)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sourceRun = useScene.getState().nodes[runId]
|
|
123
|
+
expect(sourceRun?.type).toBe('cabinet')
|
|
124
|
+
expect((sourceRun as CabinetNodeType | undefined)?.children).toEqual([
|
|
125
|
+
leftModuleId,
|
|
126
|
+
rightModuleId,
|
|
127
|
+
] satisfies CabinetModuleNodeType['id'][])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('promotes a complete module selection to the cabinet run before copying', () => {
|
|
131
|
+
const copied = copySelectedNodesToEditorClipboard([leftModuleId, rightModuleId])
|
|
132
|
+
|
|
133
|
+
expect(copied).toBe(true)
|
|
134
|
+
expect(getEditorClipboardSnapshot()?.rootIds).toEqual([runId])
|
|
135
|
+
|
|
136
|
+
const result = pasteEditorClipboardToLevel(targetLevelId)
|
|
137
|
+
expect(result?.pastedIds).toHaveLength(1)
|
|
138
|
+
expect(pastedCabinetRun()?.children).toHaveLength(2)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('remaps a measurement association when its host is copied with it', () => {
|
|
142
|
+
const wall = WallNode.parse({
|
|
143
|
+
id: 'wall_clipboard-host',
|
|
144
|
+
type: 'wall',
|
|
145
|
+
parentId: sourceLevelId,
|
|
146
|
+
start: [0, 0],
|
|
147
|
+
end: [3, 0],
|
|
148
|
+
})
|
|
149
|
+
const measurement = MeasurementNode.parse({
|
|
150
|
+
id: 'measurement_clipboard-associated',
|
|
151
|
+
type: 'measurement',
|
|
152
|
+
parentId: sourceLevelId,
|
|
153
|
+
measurement: {
|
|
154
|
+
kind: 'distance',
|
|
155
|
+
points: [
|
|
156
|
+
{
|
|
157
|
+
kind: 'feature',
|
|
158
|
+
reference: { nodeId: wall.id, featureId: 'wall:face:left', parameters: { t: 0 } },
|
|
159
|
+
fallback: [0, 0, 0],
|
|
160
|
+
},
|
|
161
|
+
[3, 0, 0],
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
useScene.setState((state) => ({
|
|
166
|
+
nodes: {
|
|
167
|
+
...state.nodes,
|
|
168
|
+
[sourceLevelId]: makeLevel(sourceLevelId, [wall.id, measurement.id]),
|
|
169
|
+
[wall.id]: wall,
|
|
170
|
+
[measurement.id]: measurement,
|
|
171
|
+
},
|
|
172
|
+
}))
|
|
173
|
+
|
|
174
|
+
expect(copySelectedNodesToEditorClipboard([wall.id, measurement.id])).toBe(true)
|
|
175
|
+
expect(pasteEditorClipboardToLevel(targetLevelId)?.pastedIds).toHaveLength(2)
|
|
176
|
+
|
|
177
|
+
const pastedWall = Object.values(useScene.getState().nodes).find(
|
|
178
|
+
(node) => node.type === 'wall' && node.id !== wall.id,
|
|
179
|
+
)
|
|
180
|
+
const pastedMeasurement = Object.values(useScene.getState().nodes).find(
|
|
181
|
+
(node) => node.type === 'measurement' && node.id !== measurement.id,
|
|
182
|
+
)
|
|
183
|
+
expect(pastedWall?.type).toBe('wall')
|
|
184
|
+
expect(pastedMeasurement?.type).toBe('measurement')
|
|
185
|
+
if (
|
|
186
|
+
pastedWall?.type !== 'wall' ||
|
|
187
|
+
pastedMeasurement?.type !== 'measurement' ||
|
|
188
|
+
pastedMeasurement.measurement.kind !== 'distance'
|
|
189
|
+
) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
const anchor = pastedMeasurement.measurement.points[0]
|
|
193
|
+
expect(Array.isArray(anchor)).toBe(false)
|
|
194
|
+
if (!Array.isArray(anchor)) expect(anchor.reference.nodeId).toBe(pastedWall.id)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
generateId,
|
|
5
|
+
type LevelNode,
|
|
6
|
+
remapMeasurementReferences,
|
|
7
|
+
type StairNode,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
+
|
|
12
|
+
type ClipboardPayload = {
|
|
13
|
+
copiedAt: number
|
|
14
|
+
nodes: AnyNode[]
|
|
15
|
+
rootIds: AnyNodeId[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PasteResult = {
|
|
19
|
+
pastedIds: AnyNodeId[]
|
|
20
|
+
skippedIds: AnyNodeId[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const COPYABLE_ROOT_TYPES = new Set<AnyNode['type']>([
|
|
24
|
+
'wall',
|
|
25
|
+
'fence',
|
|
26
|
+
'column',
|
|
27
|
+
'item',
|
|
28
|
+
'slab',
|
|
29
|
+
'ceiling',
|
|
30
|
+
'roof',
|
|
31
|
+
'stair',
|
|
32
|
+
'spawn',
|
|
33
|
+
'zone',
|
|
34
|
+
'cabinet',
|
|
35
|
+
'cabinet-module',
|
|
36
|
+
'measurement',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
let clipboardPayload: ClipboardPayload | null = null
|
|
40
|
+
const subscribers = new Set<() => void>()
|
|
41
|
+
|
|
42
|
+
function notifySubscribers() {
|
|
43
|
+
for (const subscriber of subscribers) {
|
|
44
|
+
subscriber()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function subscribeEditorClipboard(subscriber: () => void) {
|
|
49
|
+
subscribers.add(subscriber)
|
|
50
|
+
return () => {
|
|
51
|
+
subscribers.delete(subscriber)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getEditorClipboardSnapshot() {
|
|
56
|
+
return clipboardPayload
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function hasEditorClipboard() {
|
|
60
|
+
return !!clipboardPayload && clipboardPayload.rootIds.length > 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractIdPrefix(id: string) {
|
|
64
|
+
const underscoreIndex = id.indexOf('_')
|
|
65
|
+
return underscoreIndex === -1 ? 'node' : id.slice(0, underscoreIndex)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function collectSubtreeIds(
|
|
69
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
70
|
+
rootId: AnyNodeId,
|
|
71
|
+
ids: Set<AnyNodeId>,
|
|
72
|
+
) {
|
|
73
|
+
if (ids.has(rootId)) return
|
|
74
|
+
const node = nodes[rootId]
|
|
75
|
+
if (!node) return
|
|
76
|
+
ids.add(rootId)
|
|
77
|
+
|
|
78
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
79
|
+
for (const childId of node.children as AnyNodeId[]) {
|
|
80
|
+
collectSubtreeIds(nodes, childId, ids)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hasSelectedAncestor(
|
|
86
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
87
|
+
id: AnyNodeId,
|
|
88
|
+
selectedIds: Set<AnyNodeId>,
|
|
89
|
+
) {
|
|
90
|
+
let parentId = nodes[id]?.parentId as AnyNodeId | null
|
|
91
|
+
|
|
92
|
+
while (parentId) {
|
|
93
|
+
if (selectedIds.has(parentId)) return true
|
|
94
|
+
parentId = nodes[parentId]?.parentId as AnyNodeId | null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isLevelChildRoot(nodes: Record<AnyNodeId, AnyNode>, node: AnyNode) {
|
|
101
|
+
const parentId = node.parentId as AnyNodeId | null
|
|
102
|
+
if (!parentId) return true
|
|
103
|
+
return nodes[parentId]?.type === 'level'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getPromotedCabinetRunId(
|
|
107
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
108
|
+
node: AnyNode,
|
|
109
|
+
selectedIds: Set<AnyNodeId>,
|
|
110
|
+
) {
|
|
111
|
+
if (node.type !== 'cabinet-module') return null
|
|
112
|
+
|
|
113
|
+
const parentId = node.parentId as AnyNodeId | null
|
|
114
|
+
const parent = parentId ? nodes[parentId] : null
|
|
115
|
+
if (parent?.type !== 'cabinet') return null
|
|
116
|
+
if (!parentId) return null
|
|
117
|
+
if (selectedIds.has(parentId)) return parentId
|
|
118
|
+
|
|
119
|
+
const siblingIds = Array.isArray(parent.children) ? (parent.children as AnyNodeId[]) : []
|
|
120
|
+
const hasOnlySelectedModules =
|
|
121
|
+
siblingIds.length > 0 &&
|
|
122
|
+
siblingIds.every((childId) => {
|
|
123
|
+
const child = nodes[childId]
|
|
124
|
+
return child?.type === 'cabinet-module' && selectedIds.has(childId)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return hasOnlySelectedModules ? parentId : null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getPasteTargetLevel(targetLevelId?: AnyNodeId) {
|
|
131
|
+
const scene = useScene.getState()
|
|
132
|
+
const resolvedLevelId =
|
|
133
|
+
targetLevelId ?? (useViewer.getState().selection.levelId as AnyNodeId | null)
|
|
134
|
+
if (!resolvedLevelId) return null
|
|
135
|
+
|
|
136
|
+
const level = scene.nodes[resolvedLevelId]
|
|
137
|
+
return level?.type === 'level' ? level : null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getNextLevelId(level: LevelNode, nodes: Record<AnyNodeId, AnyNode>) {
|
|
141
|
+
const parentId = level.parentId as AnyNodeId | null
|
|
142
|
+
if (!parentId) return null
|
|
143
|
+
|
|
144
|
+
const building = nodes[parentId]
|
|
145
|
+
if (building?.type !== 'building') return null
|
|
146
|
+
|
|
147
|
+
const siblingLevels = building.children
|
|
148
|
+
.map((childId) => nodes[childId as AnyNodeId])
|
|
149
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
siblingLevels
|
|
153
|
+
.filter((candidate) => candidate.level > level.level)
|
|
154
|
+
.sort((a, b) => a.level - b.level)[0]?.id ?? null
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function remapNodeReferences(
|
|
159
|
+
node: AnyNode,
|
|
160
|
+
oldId: AnyNodeId,
|
|
161
|
+
targetLevel: LevelNode,
|
|
162
|
+
idMap: Map<AnyNodeId, AnyNodeId>,
|
|
163
|
+
rootIds: Set<AnyNodeId>,
|
|
164
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
165
|
+
) {
|
|
166
|
+
const clone = JSON.parse(JSON.stringify(node)) as AnyNode
|
|
167
|
+
;(clone as Record<string, unknown>).id = idMap.get(oldId)
|
|
168
|
+
|
|
169
|
+
if (rootIds.has(oldId)) {
|
|
170
|
+
clone.parentId = targetLevel.id
|
|
171
|
+
} else if (clone.parentId && typeof clone.parentId === 'string') {
|
|
172
|
+
clone.parentId = idMap.get(clone.parentId as AnyNodeId) ?? clone.parentId
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if ('children' in clone && Array.isArray(clone.children)) {
|
|
176
|
+
;(clone as Record<string, unknown>).children = (clone.children as AnyNodeId[])
|
|
177
|
+
.map((childId) => idMap.get(childId))
|
|
178
|
+
.filter((childId): childId is AnyNodeId => !!childId)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if ('wallId' in clone && typeof clone.wallId === 'string') {
|
|
182
|
+
const nextWallId = idMap.get(clone.wallId as AnyNodeId)
|
|
183
|
+
if (nextWallId) {
|
|
184
|
+
;(clone as Record<string, unknown>).wallId = nextWallId
|
|
185
|
+
} else {
|
|
186
|
+
delete (clone as Record<string, unknown>).wallId
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (clone.type === 'stair') {
|
|
191
|
+
const nextLevelId = getNextLevelId(targetLevel, nodes)
|
|
192
|
+
;(clone as StairNode).fromLevelId = targetLevel.id
|
|
193
|
+
;(clone as StairNode).toLevelId = nextLevelId
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (clone.type === 'measurement') {
|
|
197
|
+
clone.measurement = remapMeasurementReferences(clone.measurement, idMap)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const metadata =
|
|
201
|
+
clone.metadata && typeof clone.metadata === 'object' && !Array.isArray(clone.metadata)
|
|
202
|
+
? { ...(clone.metadata as Record<string, unknown>) }
|
|
203
|
+
: {}
|
|
204
|
+
delete metadata.isNew
|
|
205
|
+
delete metadata.isTransient
|
|
206
|
+
;(clone as Record<string, unknown>).metadata = metadata
|
|
207
|
+
|
|
208
|
+
return AnyNode.parse(clone)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildClipboardPayload(ids: AnyNodeId[]): ClipboardPayload | null {
|
|
212
|
+
const scene = useScene.getState()
|
|
213
|
+
const selectedIdSet = new Set(ids)
|
|
214
|
+
const promotedIds = ids.map((id) => {
|
|
215
|
+
const node = scene.nodes[id]
|
|
216
|
+
return node ? (getPromotedCabinetRunId(scene.nodes, node, selectedIdSet) ?? id) : id
|
|
217
|
+
})
|
|
218
|
+
const rootIds = Array.from(new Set(promotedIds)).filter((id) => {
|
|
219
|
+
const node = scene.nodes[id]
|
|
220
|
+
return (
|
|
221
|
+
node &&
|
|
222
|
+
COPYABLE_ROOT_TYPES.has(node.type) &&
|
|
223
|
+
isLevelChildRoot(scene.nodes, node) &&
|
|
224
|
+
!hasSelectedAncestor(scene.nodes, id, selectedIdSet)
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
if (rootIds.length === 0) {
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const subtreeIds = new Set<AnyNodeId>()
|
|
233
|
+
for (const rootId of rootIds) {
|
|
234
|
+
collectSubtreeIds(scene.nodes, rootId, subtreeIds)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
copiedAt: Date.now(),
|
|
239
|
+
nodes: [...subtreeIds]
|
|
240
|
+
.map((id) => scene.nodes[id])
|
|
241
|
+
.filter((node): node is AnyNode => !!node)
|
|
242
|
+
.map((node) => JSON.parse(JSON.stringify(node)) as AnyNode),
|
|
243
|
+
rootIds,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function copySelectedNodesToEditorClipboard(selectedIds?: AnyNodeId[]) {
|
|
248
|
+
const ids = selectedIds ?? (useViewer.getState().selection.selectedIds as AnyNodeId[])
|
|
249
|
+
const payload = buildClipboardPayload(ids)
|
|
250
|
+
if (!payload) return false
|
|
251
|
+
|
|
252
|
+
clipboardPayload = payload
|
|
253
|
+
notifySubscribers()
|
|
254
|
+
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Clone the given nodes (subtrees included, ids remapped) onto the target /
|
|
260
|
+
* active level in place — the same copy + paste pipeline in one step, WITHOUT
|
|
261
|
+
* touching the user's editor clipboard. Selects the clones. Used by the group
|
|
262
|
+
* action menu's Duplicate.
|
|
263
|
+
*/
|
|
264
|
+
export function duplicateNodesToLevel(
|
|
265
|
+
ids: AnyNodeId[],
|
|
266
|
+
targetLevelId?: AnyNodeId,
|
|
267
|
+
): PasteResult | null {
|
|
268
|
+
const payload = buildClipboardPayload(ids)
|
|
269
|
+
if (!payload) return null
|
|
270
|
+
return applyClipboardPayloadToLevel(payload, targetLevelId)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function pasteEditorClipboardToLevel(targetLevelId?: AnyNodeId): PasteResult | null {
|
|
274
|
+
if (!clipboardPayload) return null
|
|
275
|
+
return applyClipboardPayloadToLevel(clipboardPayload, targetLevelId)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function applyClipboardPayloadToLevel(
|
|
279
|
+
payload: ClipboardPayload,
|
|
280
|
+
targetLevelId?: AnyNodeId,
|
|
281
|
+
): PasteResult | null {
|
|
282
|
+
const targetLevel = getPasteTargetLevel(targetLevelId)
|
|
283
|
+
if (!targetLevel) return null
|
|
284
|
+
|
|
285
|
+
const scene = useScene.getState()
|
|
286
|
+
const idMap = new Map<AnyNodeId, AnyNodeId>()
|
|
287
|
+
|
|
288
|
+
for (const node of payload.nodes) {
|
|
289
|
+
idMap.set(node.id as AnyNodeId, generateId(extractIdPrefix(node.id)) as AnyNodeId)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const rootIdSet = new Set(payload.rootIds)
|
|
293
|
+
const pastedNodes: AnyNode[] = []
|
|
294
|
+
const skippedIds: AnyNodeId[] = []
|
|
295
|
+
|
|
296
|
+
for (const node of payload.nodes) {
|
|
297
|
+
try {
|
|
298
|
+
pastedNodes.push(
|
|
299
|
+
remapNodeReferences(node, node.id as AnyNodeId, targetLevel, idMap, rootIdSet, scene.nodes),
|
|
300
|
+
)
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error('Failed to paste copied node', node.id, error)
|
|
303
|
+
skippedIds.push(node.id as AnyNodeId)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (pastedNodes.length === 0) {
|
|
308
|
+
return { pastedIds: [], skippedIds }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
scene.createNodes(
|
|
312
|
+
pastedNodes.map((node) => ({
|
|
313
|
+
node,
|
|
314
|
+
parentId: (node.parentId as AnyNodeId | null) ?? undefined,
|
|
315
|
+
})),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
const pastedNodeIds = new Set(pastedNodes.map((node) => node.id as AnyNodeId))
|
|
319
|
+
const pastedRootIds = payload.rootIds
|
|
320
|
+
.map((rootId) => idMap.get(rootId))
|
|
321
|
+
.filter((id): id is AnyNodeId => !!id && pastedNodeIds.has(id))
|
|
322
|
+
|
|
323
|
+
useViewer.getState().setSelection({
|
|
324
|
+
levelId: targetLevel.id,
|
|
325
|
+
selectedIds: pastedRootIds,
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
pastedIds: pastedRootIds,
|
|
330
|
+
skippedIds,
|
|
331
|
+
}
|
|
332
|
+
}
|
package/src/lib/scene.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
clearSceneHistory,
|
|
5
|
+
nodeRegistry,
|
|
6
|
+
resolveLevelId,
|
|
7
|
+
sceneRegistry,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
4
10
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
11
|
import useEditor, {
|
|
6
12
|
hasCustomPersistedEditorUiState,
|
|
7
13
|
normalizePersistedEditorUiState,
|
|
8
14
|
type PersistedEditorUiState,
|
|
9
15
|
} from '../store/use-editor'
|
|
16
|
+
import { editorHostPanelRegistry } from './plugin-panels'
|
|
10
17
|
|
|
11
18
|
export type SceneGraph = {
|
|
12
19
|
nodes: Record<string, unknown>
|
|
13
20
|
rootNodeIds: string[]
|
|
21
|
+
// Document-level scene state that travels with the graph. Optional so older
|
|
22
|
+
// payloads (and callers that only build nodes) stay valid.
|
|
23
|
+
collections?: Record<string, unknown>
|
|
24
|
+
materials?: Record<string, unknown>
|
|
25
|
+
installedPlugins?: string[]
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
type PersistedSelectionPath = {
|
|
@@ -220,7 +232,11 @@ function getValidatedSelectionForScene(
|
|
|
220
232
|
|
|
221
233
|
const selectedIds = selection.selectedIds.filter((id) => {
|
|
222
234
|
const node = sceneNodes[id]
|
|
223
|
-
|
|
235
|
+
if (!node) return false
|
|
236
|
+
if (resolveLevelId(node, sceneNodes) === levelId) return true
|
|
237
|
+
|
|
238
|
+
const def = nodeRegistry.get(node.type)
|
|
239
|
+
return def?.floorplanScope === 'building' && node.parentId === buildingId
|
|
224
240
|
})
|
|
225
241
|
|
|
226
242
|
return {
|
|
@@ -283,7 +299,11 @@ export function syncEditorSelectionFromCurrentScene() {
|
|
|
283
299
|
|
|
284
300
|
if (shouldRestoreEditorUiState) {
|
|
285
301
|
if (restoredSelection) {
|
|
286
|
-
|
|
302
|
+
// PersistedSelectionPath carries plain `string` ids (read from
|
|
303
|
+
// localStorage, no branded-template-literal guarantee). The viewer's
|
|
304
|
+
// SelectionPath expects branded ids. The runtime values match the
|
|
305
|
+
// brand; the cast bridges the static gap.
|
|
306
|
+
useViewer.getState().setSelection(restoredSelection as never)
|
|
287
307
|
useEditor.setState(
|
|
288
308
|
restoredEditorUiState.phase === 'site'
|
|
289
309
|
? (selectionDrivenEditorUiState ?? restoredEditorUiState)
|
|
@@ -305,7 +325,7 @@ export function syncEditorSelectionFromCurrentScene() {
|
|
|
305
325
|
}
|
|
306
326
|
|
|
307
327
|
if (restoredSelection) {
|
|
308
|
-
useViewer.getState().setSelection(restoredSelection)
|
|
328
|
+
useViewer.getState().setSelection(restoredSelection as never)
|
|
309
329
|
if (selectionDrivenEditorUiState) {
|
|
310
330
|
useEditor.setState(selectionDrivenEditorUiState)
|
|
311
331
|
}
|
|
@@ -347,10 +367,9 @@ function resetEditorInteractionState() {
|
|
|
347
367
|
structureLayer: 'elements',
|
|
348
368
|
catalogCategory: null,
|
|
349
369
|
selectedItem: null,
|
|
350
|
-
movingNode: null,
|
|
351
370
|
selectedReferenceId: null,
|
|
352
371
|
spaces: {},
|
|
353
|
-
|
|
372
|
+
hoveredHole: null,
|
|
354
373
|
isPreviewMode: false,
|
|
355
374
|
})
|
|
356
375
|
}
|
|
@@ -364,13 +383,26 @@ function hasUsableSceneGraph(sceneGraph?: SceneGraph | null): sceneGraph is Scen
|
|
|
364
383
|
}
|
|
365
384
|
|
|
366
385
|
export function applySceneGraphToEditor(sceneGraph?: SceneGraph | null) {
|
|
386
|
+
const defaultInstalledPlugins = editorHostPanelRegistry.getDefaultInstalledPluginIds()
|
|
367
387
|
if (hasUsableSceneGraph(sceneGraph)) {
|
|
368
|
-
const { nodes, rootNodeIds } = sceneGraph
|
|
369
|
-
useScene.getState().setScene(nodes as any, rootNodeIds as any
|
|
388
|
+
const { nodes, rootNodeIds, collections, materials, installedPlugins } = sceneGraph
|
|
389
|
+
useScene.getState().setScene(nodes as any, rootNodeIds as any, {
|
|
390
|
+
collections: collections as any,
|
|
391
|
+
materials: materials as any,
|
|
392
|
+
installedPlugins: installedPlugins ?? defaultInstalledPlugins,
|
|
393
|
+
hasExplicitPluginInstallState: installedPlugins !== undefined,
|
|
394
|
+
})
|
|
370
395
|
} else {
|
|
371
396
|
useScene.getState().clearScene()
|
|
397
|
+
useScene.getState().setInstalledPlugins(defaultInstalledPlugins, { explicit: false })
|
|
372
398
|
}
|
|
373
399
|
|
|
400
|
+
// The loaded scene is the undo floor. Loading records history entries of
|
|
401
|
+
// its own (`unloadScene` + `setScene`/`clearScene` are tracked writes), so
|
|
402
|
+
// without this reset a few Ctrl+Z presses could step past the load into the
|
|
403
|
+
// pre-load — often empty — state and wipe the whole project.
|
|
404
|
+
clearSceneHistory()
|
|
405
|
+
|
|
374
406
|
syncEditorSelectionFromCurrentScene()
|
|
375
407
|
}
|
|
376
408
|
|