@pascal-app/editor 1.0.1 → 1.0.4
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 +16 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +221 -128
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +132 -0
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +82 -23
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +7 -19
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +191 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +87 -0
- package/src/lib/scene.ts +39 -45
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -1,31 +1,54 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
collectionIdsOf,
|
|
4
5
|
generateId,
|
|
5
6
|
generateSceneMaterialId,
|
|
7
|
+
getArtifactStore,
|
|
8
|
+
isDerivedNode,
|
|
6
9
|
type LevelNode,
|
|
7
10
|
nodeRegistry,
|
|
11
|
+
type ParsedBuildJson,
|
|
8
12
|
remapMeasurementReferences,
|
|
9
13
|
SceneMaterial,
|
|
10
14
|
type SceneMaterialId,
|
|
11
15
|
type StairNode,
|
|
16
|
+
scriptedSize,
|
|
12
17
|
useScene,
|
|
13
18
|
} from '@pascal-app/core'
|
|
19
|
+
import { clampDoorToWall, clampWindowToWall } from '@pascal-app/core/building'
|
|
14
20
|
import { useViewer } from '@pascal-app/viewer'
|
|
21
|
+
import { referencedSceneMaterialIds, remapSceneMaterialRefs } from './scene-material-refs'
|
|
15
22
|
|
|
16
23
|
type ClipboardPayload = {
|
|
17
24
|
copiedAt: number
|
|
18
25
|
materials: SceneMaterial[]
|
|
19
26
|
nodes: AnyNode[]
|
|
27
|
+
/** The project copied from, where the scripted nodes' artifacts live. */
|
|
28
|
+
projectId: string | null
|
|
20
29
|
rootIds: AnyNodeId[]
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
export type PasteResult = {
|
|
24
33
|
createdMaterialIds: SceneMaterialId[]
|
|
25
34
|
pastedIds: AnyNodeId[]
|
|
35
|
+
/** Scripted nodes left out (with what they host): their artifacts could not come from the project they were copied from. */
|
|
36
|
+
refusedIds: AnyNodeId[]
|
|
37
|
+
/** Why they were left out, when any were. */
|
|
38
|
+
refusal: PasteRefusal | null
|
|
26
39
|
skippedIds: AnyNodeId[]
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
/**
|
|
43
|
+
* `no-access`: the source project cannot be read any more (removed from it,
|
|
44
|
+
* deleted, made private, another account). `no-copies`: its geometry is
|
|
45
|
+
* readable but this script version cannot be copied. `failed`: the copy itself failed.
|
|
46
|
+
*/
|
|
47
|
+
export type PasteRefusal = 'no-access' | 'no-copies' | 'failed'
|
|
48
|
+
|
|
49
|
+
type Refused = { ids: ReadonlySet<AnyNodeId>; reason: PasteRefusal | null }
|
|
50
|
+
const NONE_REFUSED: Refused = { ids: new Set(), reason: null }
|
|
51
|
+
|
|
29
52
|
const SYSTEM_CLIPBOARD_KIND = 'pascal.scene-nodes'
|
|
30
53
|
const SYSTEM_CLIPBOARD_VERSION = 1
|
|
31
54
|
|
|
@@ -84,7 +107,7 @@ function collectSubtreeIds(
|
|
|
84
107
|
) {
|
|
85
108
|
if (ids.has(rootId)) return
|
|
86
109
|
const node = nodes[rootId]
|
|
87
|
-
if (!node) return
|
|
110
|
+
if (!node || isDerivedNode(node)) return
|
|
88
111
|
ids.add(rootId)
|
|
89
112
|
|
|
90
113
|
if ('children' in node && Array.isArray(node.children)) {
|
|
@@ -129,6 +152,7 @@ function isClipboardRoot(
|
|
|
129
152
|
}
|
|
130
153
|
|
|
131
154
|
function isCopyableRootType(node: AnyNode) {
|
|
155
|
+
if (isDerivedNode(node)) return false
|
|
132
156
|
if (COPYABLE_ROOT_TYPES.has(node.type)) return true
|
|
133
157
|
const definition = nodeRegistry.get(node.type)
|
|
134
158
|
return !!definition && definition.capabilities?.duplicable !== false
|
|
@@ -195,10 +219,7 @@ function remapNodeReferences(
|
|
|
195
219
|
rootIds: Set<AnyNodeId>,
|
|
196
220
|
nodes: Record<AnyNodeId, AnyNode>,
|
|
197
221
|
) {
|
|
198
|
-
const clone =
|
|
199
|
-
JSON.parse(JSON.stringify(node)),
|
|
200
|
-
materialIdMap,
|
|
201
|
-
) as AnyNode
|
|
222
|
+
const clone = remapSceneMaterialRefs(JSON.parse(JSON.stringify(node)) as AnyNode, materialIdMap)
|
|
202
223
|
;(clone as Record<string, unknown>).id = idMap.get(oldId)
|
|
203
224
|
|
|
204
225
|
if (rootIds.has(oldId)) {
|
|
@@ -217,6 +238,11 @@ function remapNodeReferences(
|
|
|
217
238
|
clone.parentId = idMap.get(clone.parentId as AnyNodeId) ?? clone.parentId
|
|
218
239
|
}
|
|
219
240
|
|
|
241
|
+
if (clone.type === 'zone' && node.parentId !== clone.parentId && clone.floor?.footprint) {
|
|
242
|
+
const { footprint: _footprint, ...floor } = clone.floor
|
|
243
|
+
clone.floor = floor
|
|
244
|
+
}
|
|
245
|
+
|
|
220
246
|
if ('children' in clone && Array.isArray(clone.children)) {
|
|
221
247
|
;(clone as Record<string, unknown>).children = (clone.children as AnyNodeId[])
|
|
222
248
|
.map((childId) => idMap.get(childId))
|
|
@@ -253,43 +279,6 @@ function remapNodeReferences(
|
|
|
253
279
|
return AnyNode.parse(clone)
|
|
254
280
|
}
|
|
255
281
|
|
|
256
|
-
function remapSceneMaterialReferences(
|
|
257
|
-
value: unknown,
|
|
258
|
-
materialIdMap: Map<SceneMaterialId, SceneMaterialId>,
|
|
259
|
-
): unknown {
|
|
260
|
-
if (typeof value === 'string' && value.startsWith('scene:')) {
|
|
261
|
-
const oldId = value.slice('scene:'.length) as SceneMaterialId
|
|
262
|
-
const nextId = materialIdMap.get(oldId)
|
|
263
|
-
return nextId ? `scene:${nextId}` : value
|
|
264
|
-
}
|
|
265
|
-
if (Array.isArray(value)) {
|
|
266
|
-
return value.map((entry) => remapSceneMaterialReferences(entry, materialIdMap))
|
|
267
|
-
}
|
|
268
|
-
if (value && typeof value === 'object') {
|
|
269
|
-
return Object.fromEntries(
|
|
270
|
-
Object.entries(value).map(([key, entry]) => [
|
|
271
|
-
key,
|
|
272
|
-
remapSceneMaterialReferences(entry, materialIdMap),
|
|
273
|
-
]),
|
|
274
|
-
)
|
|
275
|
-
}
|
|
276
|
-
return value
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function collectReferencedSceneMaterialIds(value: unknown, ids: Set<SceneMaterialId>) {
|
|
280
|
-
if (typeof value === 'string' && value.startsWith('scene:')) {
|
|
281
|
-
ids.add(value.slice('scene:'.length) as SceneMaterialId)
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
if (Array.isArray(value)) {
|
|
285
|
-
for (const entry of value) collectReferencedSceneMaterialIds(entry, ids)
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
if (value && typeof value === 'object') {
|
|
289
|
-
for (const entry of Object.values(value)) collectReferencedSceneMaterialIds(entry, ids)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
282
|
function buildClipboardPayload(ids: AnyNodeId[]): ClipboardPayload | null {
|
|
294
283
|
const scene = useScene.getState()
|
|
295
284
|
const selectedIdSet = new Set(ids)
|
|
@@ -321,8 +310,7 @@ function buildClipboardPayload(ids: AnyNodeId[]): ClipboardPayload | null {
|
|
|
321
310
|
.map((id) => scene.nodes[id])
|
|
322
311
|
.filter((node): node is AnyNode => !!node)
|
|
323
312
|
.map((node) => JSON.parse(JSON.stringify(node)) as AnyNode)
|
|
324
|
-
const materialIds =
|
|
325
|
-
collectReferencedSceneMaterialIds(copiedNodes, materialIds)
|
|
313
|
+
const materialIds = referencedSceneMaterialIds(copiedNodes)
|
|
326
314
|
|
|
327
315
|
return {
|
|
328
316
|
copiedAt: Date.now(),
|
|
@@ -331,6 +319,7 @@ function buildClipboardPayload(ids: AnyNodeId[]): ClipboardPayload | null {
|
|
|
331
319
|
.filter((material): material is SceneMaterial => !!material)
|
|
332
320
|
.map((material) => JSON.parse(JSON.stringify(material)) as SceneMaterial),
|
|
333
321
|
nodes: copiedNodes,
|
|
322
|
+
projectId: useViewer.getState().projectId,
|
|
334
323
|
rootIds,
|
|
335
324
|
}
|
|
336
325
|
}
|
|
@@ -363,6 +352,7 @@ function parseClipboardPayload(text: string): ClipboardPayload | null {
|
|
|
363
352
|
copiedAt?: unknown
|
|
364
353
|
materials?: unknown
|
|
365
354
|
nodes?: unknown
|
|
355
|
+
projectId?: unknown
|
|
366
356
|
rootIds?: unknown
|
|
367
357
|
}
|
|
368
358
|
if (
|
|
@@ -390,6 +380,7 @@ function parseClipboardPayload(text: string): ClipboardPayload | null {
|
|
|
390
380
|
copiedAt: candidate.copiedAt,
|
|
391
381
|
materials: materials.filter((result) => result.success).map((result) => result.data),
|
|
392
382
|
nodes: parsedNodes,
|
|
383
|
+
projectId: typeof candidate.projectId === 'string' ? candidate.projectId : null,
|
|
393
384
|
rootIds,
|
|
394
385
|
}
|
|
395
386
|
} catch {
|
|
@@ -470,16 +461,130 @@ export function pasteEditorClipboardToLevel(targetLevelId?: AnyNodeId): PasteRes
|
|
|
470
461
|
export async function pasteSystemEditorClipboardToLevel(
|
|
471
462
|
targetLevelId?: AnyNodeId,
|
|
472
463
|
): Promise<PasteResult | null> {
|
|
473
|
-
|
|
474
|
-
|
|
464
|
+
const projectId = useViewer.getState().projectId
|
|
465
|
+
const store = getArtifactStore()
|
|
466
|
+
if (!(await readEditorClipboardFromSystem()) || !clipboardPayload) return null
|
|
467
|
+
if (projectId !== useViewer.getState().projectId || store !== getArtifactStore()) return null
|
|
468
|
+
const payload = clipboardPayload
|
|
469
|
+
const targetLevel = getPasteTargetLevel(targetLevelId)
|
|
470
|
+
if (!targetLevel) return null
|
|
471
|
+
const refused =
|
|
472
|
+
payload.projectId && payload.projectId === projectId
|
|
473
|
+
? NONE_REFUSED
|
|
474
|
+
: await copyArtifactsHere(payload)
|
|
475
|
+
// Navigation can replace the scene while the server copies its artifacts.
|
|
476
|
+
if (projectId !== useViewer.getState().projectId || store !== getArtifactStore()) return null
|
|
477
|
+
return applyClipboardPayloadToLevel(payload, targetLevel.id, refused)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function artifactHashes(node: AnyNode): string[] {
|
|
481
|
+
const source = 'source' in node ? node.source : undefined
|
|
482
|
+
return typeof source === 'object' && source.kind === 'script'
|
|
483
|
+
? [source.script, source.artifact]
|
|
484
|
+
: []
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* A paste from another project first brings the artifacts its scripted nodes
|
|
489
|
+
* reference into this one. Returns the nodes whose artifacts could not come,
|
|
490
|
+
* which the paste leaves out with everything they host, and why.
|
|
491
|
+
*/
|
|
492
|
+
async function copyArtifactsHere(
|
|
493
|
+
payload: Pick<ClipboardPayload, 'nodes' | 'projectId'>,
|
|
494
|
+
): Promise<Refused> {
|
|
495
|
+
const store = getArtifactStore()
|
|
496
|
+
const hashes = [...new Set(payload.nodes.flatMap(artifactHashes))]
|
|
497
|
+
if (hashes.length === 0) return NONE_REFUSED
|
|
498
|
+
|
|
499
|
+
let failed = false
|
|
500
|
+
const missing = new Set(
|
|
501
|
+
store.copyFrom
|
|
502
|
+
? payload.projectId
|
|
503
|
+
? await store.copyFrom(payload.projectId, hashes).catch(() => {
|
|
504
|
+
failed = true
|
|
505
|
+
return hashes
|
|
506
|
+
})
|
|
507
|
+
: hashes
|
|
508
|
+
: hashes.filter((sha) => !store.url(sha)),
|
|
509
|
+
)
|
|
510
|
+
const ids = new Set<AnyNodeId>()
|
|
511
|
+
let geometryMissing = false
|
|
512
|
+
for (const node of payload.nodes) {
|
|
513
|
+
const [script, artifact] = artifactHashes(node)
|
|
514
|
+
if (!script || !artifact || !(missing.has(script) || missing.has(artifact))) continue
|
|
515
|
+
ids.add(node.id as AnyNodeId)
|
|
516
|
+
geometryMissing ||= missing.has(artifact)
|
|
517
|
+
}
|
|
518
|
+
if (ids.size === 0) return NONE_REFUSED
|
|
519
|
+
return { ids, reason: failed ? 'failed' : geometryMissing ? 'no-access' : 'no-copies' }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* A build file loaded into another project first brings the artifacts its
|
|
524
|
+
* scripted nodes reference from the project it was saved in, as a paste does.
|
|
525
|
+
* Answers the build without the nodes whose artifacts could not come, nor
|
|
526
|
+
* anything they host.
|
|
527
|
+
*/
|
|
528
|
+
export async function bringBuildArtifacts(build: ParsedBuildJson): Promise<{
|
|
529
|
+
build: ParsedBuildJson
|
|
530
|
+
refusedIds: AnyNodeId[]
|
|
531
|
+
refusal: PasteRefusal | null
|
|
532
|
+
}> {
|
|
533
|
+
const nodes = Object.values(build.nodes) as AnyNode[]
|
|
534
|
+
// A file's origin is untrusted, even when it names this project. Older files can
|
|
535
|
+
// still use artifacts already registered here, but cannot introduce missing ones.
|
|
536
|
+
const refused = await copyArtifactsHere({
|
|
537
|
+
nodes,
|
|
538
|
+
projectId: build.projectId ?? useViewer.getState().projectId,
|
|
539
|
+
})
|
|
540
|
+
if (refused.ids.size === 0) return { build, refusedIds: [], refusal: null }
|
|
541
|
+
|
|
542
|
+
const excluded = new Set<string>(refused.ids)
|
|
543
|
+
for (let grew = true; grew; ) {
|
|
544
|
+
grew = false
|
|
545
|
+
for (const node of nodes) {
|
|
546
|
+
if (node.parentId && excluded.has(node.parentId) && !excluded.has(node.id)) {
|
|
547
|
+
excluded.add(node.id)
|
|
548
|
+
grew = true
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const kept: Record<string, unknown> = {}
|
|
553
|
+
for (const node of nodes) {
|
|
554
|
+
if (excluded.has(node.id)) continue
|
|
555
|
+
const children = (node as { children?: unknown }).children
|
|
556
|
+
kept[node.id] = Array.isArray(children)
|
|
557
|
+
? { ...node, children: children.filter((id) => !excluded.has(id as string)) }
|
|
558
|
+
: node
|
|
559
|
+
}
|
|
560
|
+
const collections =
|
|
561
|
+
build.collections &&
|
|
562
|
+
Object.fromEntries(
|
|
563
|
+
Object.entries(build.collections).map(([id, collection]) => [
|
|
564
|
+
id,
|
|
565
|
+
{ ...collection, nodeIds: collection.nodeIds.filter((nodeId) => !excluded.has(nodeId)) },
|
|
566
|
+
]),
|
|
567
|
+
)
|
|
568
|
+
return {
|
|
569
|
+
build: {
|
|
570
|
+
...build,
|
|
571
|
+
nodes: kept,
|
|
572
|
+
rootNodeIds: build.rootNodeIds.filter((id) => !excluded.has(id)),
|
|
573
|
+
...(collections ? { collections } : {}),
|
|
574
|
+
},
|
|
575
|
+
refusedIds: [...refused.ids],
|
|
576
|
+
refusal: refused.reason,
|
|
577
|
+
}
|
|
475
578
|
}
|
|
476
579
|
|
|
477
580
|
function applyClipboardPayloadToLevel(
|
|
478
|
-
|
|
581
|
+
copied: ClipboardPayload,
|
|
479
582
|
targetLevelId?: AnyNodeId,
|
|
583
|
+
{ ids: refused, reason: refusal }: Refused = NONE_REFUSED,
|
|
480
584
|
): PasteResult | null {
|
|
481
585
|
const targetLevel = getPasteTargetLevel(targetLevelId)
|
|
482
586
|
if (!targetLevel) return null
|
|
587
|
+
const payload = refused.size > 0 ? withoutRefusedSubtrees(copied, refused) : copied
|
|
483
588
|
|
|
484
589
|
const scene = useScene.getState()
|
|
485
590
|
const idMap = new Map<AnyNodeId, AnyNodeId>()
|
|
@@ -487,6 +592,7 @@ function applyClipboardPayloadToLevel(
|
|
|
487
592
|
const materialsToCreate: SceneMaterial[] = []
|
|
488
593
|
|
|
489
594
|
for (const node of payload.nodes) {
|
|
595
|
+
if (isDerivedNode(node)) continue
|
|
490
596
|
idMap.set(node.id as AnyNodeId, generateId(extractIdPrefix(node.id)) as AnyNodeId)
|
|
491
597
|
}
|
|
492
598
|
for (const material of payload.materials) {
|
|
@@ -506,6 +612,10 @@ function applyClipboardPayloadToLevel(
|
|
|
506
612
|
const skippedIds: AnyNodeId[] = []
|
|
507
613
|
|
|
508
614
|
for (const node of payload.nodes) {
|
|
615
|
+
if (isDerivedNode(node)) {
|
|
616
|
+
skippedIds.push(node.id as AnyNodeId)
|
|
617
|
+
continue
|
|
618
|
+
}
|
|
509
619
|
try {
|
|
510
620
|
pastedNodes.push(
|
|
511
621
|
remapNodeReferences(
|
|
@@ -524,17 +634,30 @@ function applyClipboardPayloadToLevel(
|
|
|
524
634
|
}
|
|
525
635
|
}
|
|
526
636
|
|
|
637
|
+
fitOpeningsToPastedWalls(pastedNodes, scene.nodes)
|
|
638
|
+
|
|
527
639
|
if (pastedNodes.length === 0) {
|
|
528
|
-
return {
|
|
640
|
+
return {
|
|
641
|
+
createdMaterialIds: [],
|
|
642
|
+
pastedIds: [],
|
|
643
|
+
refusedIds: [...refused],
|
|
644
|
+
refusal,
|
|
645
|
+
skippedIds,
|
|
646
|
+
}
|
|
529
647
|
}
|
|
530
648
|
|
|
531
649
|
for (const material of materialsToCreate) {
|
|
532
650
|
scene.addSceneMaterial(material)
|
|
533
651
|
}
|
|
652
|
+
const sourceIds = new Map([...idMap].map(([source, copy]) => [copy, source]))
|
|
534
653
|
scene.createNodes(
|
|
535
654
|
pastedNodes.map((node) => ({
|
|
536
655
|
node,
|
|
537
656
|
parentId: (node.parentId as AnyNodeId | null) ?? undefined,
|
|
657
|
+
// A copy is in its source's collections; an item brings them in its own `collectionIds`.
|
|
658
|
+
...(node.type !== 'item' && {
|
|
659
|
+
collectionIds: collectionIdsOf(scene.collections, sourceIds.get(node.id as AnyNodeId)!),
|
|
660
|
+
}),
|
|
538
661
|
})),
|
|
539
662
|
)
|
|
540
663
|
|
|
@@ -551,6 +674,64 @@ function applyClipboardPayloadToLevel(
|
|
|
551
674
|
return {
|
|
552
675
|
createdMaterialIds: materialsToCreate.map((material) => material.id as SceneMaterialId),
|
|
553
676
|
pastedIds: pastedRootIds,
|
|
677
|
+
refusedIds: [...refused],
|
|
678
|
+
refusal,
|
|
554
679
|
skippedIds,
|
|
555
680
|
}
|
|
556
681
|
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* A door or window pasted with its wall is held inside it the way the door and window tools place
|
|
685
|
+
* one: a wall pasted under a lower storey would otherwise leave a tall window poking above it.
|
|
686
|
+
*/
|
|
687
|
+
function fitOpeningsToPastedWalls(pasted: AnyNode[], sceneNodes: Record<AnyNodeId, AnyNode>) {
|
|
688
|
+
const byId = Object.fromEntries(pasted.map((node) => [node.id, node])) as Record<
|
|
689
|
+
AnyNodeId,
|
|
690
|
+
AnyNode
|
|
691
|
+
>
|
|
692
|
+
let nodes: Record<AnyNodeId, AnyNode> | undefined
|
|
693
|
+
pasted.forEach((node, index) => {
|
|
694
|
+
if (node.type !== 'door' && node.type !== 'window') return
|
|
695
|
+
const wall = node.parentId ? byId[node.parentId as AnyNodeId] : undefined
|
|
696
|
+
if (wall?.type !== 'wall') return
|
|
697
|
+
// A scripted opening is as tall as what its script built, as the wall cuts it.
|
|
698
|
+
const [width, height] = node.source
|
|
699
|
+
? scriptedSize(node.source.manifest)
|
|
700
|
+
: [node.width, node.height]
|
|
701
|
+
nodes ??= { ...sceneNodes, ...byId }
|
|
702
|
+
const [x, y, z] = node.position
|
|
703
|
+
const { clampedY } =
|
|
704
|
+
node.type === 'door'
|
|
705
|
+
? clampDoorToWall(wall, x, width, height)
|
|
706
|
+
: clampWindowToWall(wall, x, y, width, height, nodes)
|
|
707
|
+
if (clampedY !== y) pasted[index] = { ...node, position: [x, clampedY, z] }
|
|
708
|
+
})
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function withoutRefusedSubtrees(
|
|
712
|
+
payload: ClipboardPayload,
|
|
713
|
+
refused: ReadonlySet<AnyNodeId>,
|
|
714
|
+
): ClipboardPayload {
|
|
715
|
+
const children = new Map<string, AnyNodeId[]>()
|
|
716
|
+
for (const node of payload.nodes) {
|
|
717
|
+
if (!node.parentId) continue
|
|
718
|
+
const siblings = children.get(node.parentId) ?? []
|
|
719
|
+
siblings.push(node.id as AnyNodeId)
|
|
720
|
+
children.set(node.parentId, siblings)
|
|
721
|
+
}
|
|
722
|
+
const excluded = new Set(refused)
|
|
723
|
+
for (const id of excluded) {
|
|
724
|
+
for (const childId of children.get(id) ?? []) excluded.add(childId)
|
|
725
|
+
}
|
|
726
|
+
const nodes = payload.nodes.filter((node) => !excluded.has(node.id as AnyNodeId))
|
|
727
|
+
const kept = new Set(nodes.map((node) => node.id))
|
|
728
|
+
const materialIds = referencedSceneMaterialIds(nodes)
|
|
729
|
+
return {
|
|
730
|
+
...payload,
|
|
731
|
+
materials: payload.materials.filter((material) =>
|
|
732
|
+
materialIds.has(material.id as SceneMaterialId),
|
|
733
|
+
),
|
|
734
|
+
nodes,
|
|
735
|
+
rootIds: payload.rootIds.filter((id) => kept.has(id)),
|
|
736
|
+
}
|
|
737
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
3
|
+
import {
|
|
4
|
+
referencedSceneMaterialIds,
|
|
5
|
+
remapSceneMaterialRefs,
|
|
6
|
+
sceneMaterialUsageCounts,
|
|
7
|
+
} from './scene-material-refs'
|
|
8
|
+
|
|
9
|
+
const ref = (id: string) => `scene:${id}`
|
|
10
|
+
|
|
11
|
+
// One node per place a finish can live.
|
|
12
|
+
const nodes = {
|
|
13
|
+
zone_room: {
|
|
14
|
+
id: 'zone_room',
|
|
15
|
+
type: 'zone',
|
|
16
|
+
floor: {
|
|
17
|
+
finish: ref('floor'),
|
|
18
|
+
regions: [{ id: 'r1', polygon: [], finish: ref('rug') }],
|
|
19
|
+
},
|
|
20
|
+
wallMaterial: ref('walls'),
|
|
21
|
+
wallOverrides: [{ wallId: 'wall_1', face: 'a', finish: ref('accent') }],
|
|
22
|
+
floorStepFinish: ref('step'),
|
|
23
|
+
floorStepOverrides: [{ key: 'door_1', finish: ref('doorstep') }],
|
|
24
|
+
floorEdgeFinish: ref('edge'),
|
|
25
|
+
},
|
|
26
|
+
wall_1: {
|
|
27
|
+
id: 'wall_1',
|
|
28
|
+
type: 'wall',
|
|
29
|
+
slots: { a: ref('walls') },
|
|
30
|
+
faceRegions: [{ id: 'w1', face: 'a', finish: ref('wainscot') }],
|
|
31
|
+
},
|
|
32
|
+
slab_base: {
|
|
33
|
+
id: 'slab_base',
|
|
34
|
+
type: 'slab',
|
|
35
|
+
plateRole: 'base',
|
|
36
|
+
slots: { edge: ref('band') },
|
|
37
|
+
foundation: { type: 'solid', material: ref('foundation') },
|
|
38
|
+
},
|
|
39
|
+
item_1: { id: 'item_1', type: 'item', slots: { seat: 'library:oak' } },
|
|
40
|
+
} as unknown as Record<string, AnyNode>
|
|
41
|
+
|
|
42
|
+
describe('scene material refs', () => {
|
|
43
|
+
test('counts every finish location, one per use', () => {
|
|
44
|
+
const counts = sceneMaterialUsageCounts(nodes)
|
|
45
|
+
expect(Object.fromEntries(counts)).toEqual({
|
|
46
|
+
floor: 1,
|
|
47
|
+
rug: 1,
|
|
48
|
+
walls: 2,
|
|
49
|
+
accent: 1,
|
|
50
|
+
step: 1,
|
|
51
|
+
doorstep: 1,
|
|
52
|
+
edge: 1,
|
|
53
|
+
wainscot: 1,
|
|
54
|
+
band: 1,
|
|
55
|
+
foundation: 1,
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('collects the materials a set of nodes carries, ignoring library refs', () => {
|
|
60
|
+
expect(
|
|
61
|
+
[...referencedSceneMaterialIds([nodes.zone_room, nodes.item_1])].map(String).sort(),
|
|
62
|
+
).toEqual(['accent', 'doorstep', 'edge', 'floor', 'rug', 'step', 'walls'])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('remaps refs wherever they are and keeps untouched nodes as they were', () => {
|
|
66
|
+
const map = new Map([
|
|
67
|
+
['floor', 'floor2'],
|
|
68
|
+
['foundation', 'stone'],
|
|
69
|
+
['doorstep', 'tile'],
|
|
70
|
+
])
|
|
71
|
+
const zone = remapSceneMaterialRefs(nodes.zone_room, map) as Record<string, any>
|
|
72
|
+
expect(zone.floor.finish).toBe(ref('floor2'))
|
|
73
|
+
expect(zone.floor.regions).toBe((nodes.zone_room as Record<string, any>).floor.regions)
|
|
74
|
+
expect(zone.floorStepOverrides).toEqual([{ key: 'door_1', finish: ref('tile') }])
|
|
75
|
+
const slab = remapSceneMaterialRefs(nodes.slab_base, map) as Record<string, any>
|
|
76
|
+
expect(slab.foundation).toEqual({ type: 'solid', material: ref('stone') })
|
|
77
|
+
expect(remapSceneMaterialRefs(nodes.item_1, map)).toBe(nodes.item_1)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
getSceneMaterialIdFromRef,
|
|
4
|
+
type SceneMaterialId,
|
|
5
|
+
toSceneMaterialRef,
|
|
6
|
+
} from '@pascal-app/core'
|
|
7
|
+
|
|
8
|
+
// Where a scene material (`scene:<id>`) is used. A node carries refs in more
|
|
9
|
+
// places than its `slots`: a room's floor finish and floor regions, its wall
|
|
10
|
+
// material and per-wall overrides, its floor step finishes (room-wide and per
|
|
11
|
+
// doorway) and edge finish, a wall's face paint regions, a footprint's edge
|
|
12
|
+
// band (`slots.edge`) and foundation.
|
|
13
|
+
// Every string ref anywhere in a node counts, so a new finish location is
|
|
14
|
+
// covered without touching this walker. The usage count, preset/room saving
|
|
15
|
+
// and pasting all walk the same way, so they always agree.
|
|
16
|
+
|
|
17
|
+
/** Calls `visit` with the material id of every `scene:` ref inside `value`, once per use. */
|
|
18
|
+
export function forEachSceneMaterialRef(value: unknown, visit: (id: SceneMaterialId) => void) {
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
const id = getSceneMaterialIdFromRef(value)
|
|
21
|
+
if (id) visit(id as SceneMaterialId)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
for (const entry of value) forEachSceneMaterialRef(entry, visit)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
if (value && typeof value === 'object')
|
|
29
|
+
for (const entry of Object.values(value)) forEachSceneMaterialRef(entry, visit)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The scene materials `value` references (a node, a list of nodes, …). */
|
|
33
|
+
export function referencedSceneMaterialIds(value: unknown): Set<SceneMaterialId> {
|
|
34
|
+
const ids = new Set<SceneMaterialId>()
|
|
35
|
+
forEachSceneMaterialRef(value, (id) => ids.add(id))
|
|
36
|
+
return ids
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** How many parts use each scene material across `nodes` (one per finish or slot). */
|
|
40
|
+
export function sceneMaterialUsageCounts(
|
|
41
|
+
nodes: Record<string, AnyNode>,
|
|
42
|
+
): Map<SceneMaterialId, number> {
|
|
43
|
+
const counts = new Map<SceneMaterialId, number>()
|
|
44
|
+
for (const node of Object.values(nodes))
|
|
45
|
+
forEachSceneMaterialRef(node, (id) => counts.set(id, (counts.get(id) ?? 0) + 1))
|
|
46
|
+
return counts
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `value` with every `scene:<old>` ref the map knows rewritten to `scene:<new>`.
|
|
51
|
+
* Unchanged branches keep their identity, so an untouched node comes back as-is.
|
|
52
|
+
*/
|
|
53
|
+
export function remapSceneMaterialRefs<T>(value: T, idMap: ReadonlyMap<string, string>): T {
|
|
54
|
+
if (typeof value === 'string') {
|
|
55
|
+
const id = getSceneMaterialIdFromRef(value)
|
|
56
|
+
const next = id ? idMap.get(id) : undefined
|
|
57
|
+
return (next && next !== id ? toSceneMaterialRef(next) : value) as T
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
let changed = false
|
|
61
|
+
const next = value.map((entry) => {
|
|
62
|
+
const mapped = remapSceneMaterialRefs(entry, idMap)
|
|
63
|
+
if (mapped !== entry) changed = true
|
|
64
|
+
return mapped
|
|
65
|
+
})
|
|
66
|
+
return (changed ? next : value) as T
|
|
67
|
+
}
|
|
68
|
+
if (value && typeof value === 'object') {
|
|
69
|
+
let changed = false
|
|
70
|
+
const next: Record<string, unknown> = {}
|
|
71
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
72
|
+
const mapped = remapSceneMaterialRefs(entry, idMap)
|
|
73
|
+
if (mapped !== entry) changed = true
|
|
74
|
+
next[key] = mapped
|
|
75
|
+
}
|
|
76
|
+
return (changed ? next : value) as T
|
|
77
|
+
}
|
|
78
|
+
return value
|
|
79
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { useScene } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import useEditor from '../store/use-editor'
|
|
5
|
+
import { syncEditorSelectionFromCurrentScene } from './scene'
|
|
6
|
+
|
|
7
|
+
const building = {
|
|
8
|
+
children: ['level_scene-root'],
|
|
9
|
+
id: 'building_scene-root',
|
|
10
|
+
object: 'node',
|
|
11
|
+
parentId: null,
|
|
12
|
+
position: [0, 0, 0],
|
|
13
|
+
rotation: [0, 0, 0],
|
|
14
|
+
type: 'building',
|
|
15
|
+
visible: true,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const level = {
|
|
19
|
+
children: ['wall_scene-root'],
|
|
20
|
+
id: 'level_scene-root',
|
|
21
|
+
level: 0,
|
|
22
|
+
object: 'node',
|
|
23
|
+
parentId: building.id,
|
|
24
|
+
type: 'level',
|
|
25
|
+
visible: true,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const wall = {
|
|
29
|
+
children: [],
|
|
30
|
+
end: [4, 0],
|
|
31
|
+
id: 'wall_scene-root',
|
|
32
|
+
object: 'node',
|
|
33
|
+
parentId: level.id,
|
|
34
|
+
start: [0, 0],
|
|
35
|
+
type: 'wall',
|
|
36
|
+
visible: true,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('scene selection synchronization', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
useViewer.getState().resetSelection()
|
|
42
|
+
useEditor.setState({ mode: 'select', phase: 'site', tool: null })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('enters the first level when a scene graph is rooted at a building', () => {
|
|
46
|
+
useScene.setState({
|
|
47
|
+
nodes: {
|
|
48
|
+
[building.id]: building,
|
|
49
|
+
[level.id]: level,
|
|
50
|
+
[wall.id]: wall,
|
|
51
|
+
},
|
|
52
|
+
rootNodeIds: [building.id],
|
|
53
|
+
} as never)
|
|
54
|
+
|
|
55
|
+
syncEditorSelectionFromCurrentScene()
|
|
56
|
+
|
|
57
|
+
expect(useViewer.getState().selection).toMatchObject({
|
|
58
|
+
buildingId: building.id,
|
|
59
|
+
levelId: level.id,
|
|
60
|
+
})
|
|
61
|
+
expect(useEditor.getState().phase).toBe('building')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('opening another project disarms the tool the last one left armed', () => {
|
|
65
|
+
// This runs on every scene load, so a mid-session project switch reads the
|
|
66
|
+
// live state of the project the user just left. A project always opens in
|
|
67
|
+
// select mode — the first click on the new scene selects, it does not build.
|
|
68
|
+
useScene.setState({
|
|
69
|
+
nodes: {
|
|
70
|
+
[building.id]: building,
|
|
71
|
+
[level.id]: level,
|
|
72
|
+
[wall.id]: wall,
|
|
73
|
+
},
|
|
74
|
+
rootNodeIds: [building.id],
|
|
75
|
+
} as never)
|
|
76
|
+
useEditor.getState().setPhase('building')
|
|
77
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
78
|
+
|
|
79
|
+
syncEditorSelectionFromCurrentScene()
|
|
80
|
+
|
|
81
|
+
expect(useEditor.getState().toolMode).toEqual({ mode: 'select' })
|
|
82
|
+
expect(useEditor.getState().mode).toBe('select')
|
|
83
|
+
expect(useEditor.getState().tool).toBeNull()
|
|
84
|
+
// The phase is a preference and stays put.
|
|
85
|
+
expect(useEditor.getState().phase).toBe('building')
|
|
86
|
+
})
|
|
87
|
+
})
|