@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type IconRef, nodeRegistry, type TerrainVerb } from '@pascal-app/core'
|
|
2
|
+
import type { ContinuationMode } from './continuation'
|
|
3
|
+
import { getWallDrawVariantInfo, wallDrawVariantOf } from './wall-draw-variant'
|
|
4
|
+
|
|
5
|
+
/** The contextual helper panel's header: what is in hand, and its key. */
|
|
6
|
+
export type HudTitle = {
|
|
7
|
+
label: string
|
|
8
|
+
icon?: IconRef
|
|
9
|
+
shortcut?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const url = (src: string): IconRef => ({ kind: 'url', src })
|
|
13
|
+
|
|
14
|
+
/** Tools with a global arming key (`use-keyboard.ts`). */
|
|
15
|
+
const TOOL_SHORTCUTS: Record<string, string> = {
|
|
16
|
+
wall: 'B',
|
|
17
|
+
measurement: 'M',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SELECT_HUD_TITLE: HudTitle = {
|
|
21
|
+
label: 'Select',
|
|
22
|
+
icon: url('/icons/select.webp'),
|
|
23
|
+
shortcut: 'V',
|
|
24
|
+
}
|
|
25
|
+
export const PAINT_HUD_TITLE: HudTitle = { label: 'Paint', icon: url('/icons/paint.webp') }
|
|
26
|
+
export const ERASE_HUD_TITLE: HudTitle = {
|
|
27
|
+
label: 'Erase',
|
|
28
|
+
icon: { kind: 'iconify', name: 'lucide:eraser' },
|
|
29
|
+
}
|
|
30
|
+
export const PICK_MATERIAL_HUD_TITLE: HudTitle = {
|
|
31
|
+
label: 'Pick material',
|
|
32
|
+
icon: { kind: 'iconify', name: 'lucide:pipette' },
|
|
33
|
+
}
|
|
34
|
+
export const ROTATE_HUD_TITLE: HudTitle = { label: 'Rotate', icon: url('/icons/rotate.webp') }
|
|
35
|
+
export const MOVE_SELECTION_HUD_TITLE: HudTitle = {
|
|
36
|
+
label: 'Move selection',
|
|
37
|
+
icon: url('/icons/pan.webp'),
|
|
38
|
+
}
|
|
39
|
+
export const MOVE_ROOM_HUD_TITLE: HudTitle = { label: 'Move room', icon: url('/icons/room.webp') }
|
|
40
|
+
export const RESIZE_HUD_TITLE: HudTitle = {
|
|
41
|
+
label: 'Resize',
|
|
42
|
+
icon: url('/icons/resize.webp'),
|
|
43
|
+
}
|
|
44
|
+
export const DIVIDE_ROOM_HUD_TITLE: HudTitle = {
|
|
45
|
+
label: 'Divide room',
|
|
46
|
+
icon: url('/icons/divide-room.webp'),
|
|
47
|
+
}
|
|
48
|
+
export const MEZZANINE_HUD_TITLE: HudTitle = {
|
|
49
|
+
label: 'Mezzanine',
|
|
50
|
+
icon: url('/icons/mezzanine.webp'),
|
|
51
|
+
}
|
|
52
|
+
export const OPENING_HUD_TITLE: HudTitle = {
|
|
53
|
+
label: 'Cut opening',
|
|
54
|
+
icon: { kind: 'iconify', name: 'lucide:square-dashed' },
|
|
55
|
+
}
|
|
56
|
+
export const TERRACE_HUD_TITLE: HudTitle = {
|
|
57
|
+
label: 'Terrace',
|
|
58
|
+
icon: url('/icons/floor.webp'),
|
|
59
|
+
}
|
|
60
|
+
export const SMART_MEASURE_HUD_TITLE: HudTitle = {
|
|
61
|
+
label: 'Smart measure',
|
|
62
|
+
icon: url('/icons/measure.webp'),
|
|
63
|
+
shortcut: 'M',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const PAINT_REGION_LABELS = {
|
|
67
|
+
rectangle: 'Paint a rectangle',
|
|
68
|
+
polygon: 'Paint a shape',
|
|
69
|
+
} as const
|
|
70
|
+
|
|
71
|
+
export function paintRegionHudTitle(mode: keyof typeof PAINT_REGION_LABELS): HudTitle {
|
|
72
|
+
return { label: PAINT_REGION_LABELS[mode], icon: url('/icons/paint.webp') }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function terrainHudTitle(verb: TerrainVerb): HudTitle {
|
|
76
|
+
const label =
|
|
77
|
+
verb === 'raise'
|
|
78
|
+
? 'Raise terrain'
|
|
79
|
+
: verb === 'lower'
|
|
80
|
+
? 'Lower terrain'
|
|
81
|
+
: verb === 'flatten'
|
|
82
|
+
? 'Level terrain'
|
|
83
|
+
: 'Smooth terrain'
|
|
84
|
+
return { label, icon: url(`/icons/terrain-${verb}.webp`) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A kind's registry name and icon, optionally behind a verb ("Move door"). */
|
|
88
|
+
export function nodeKindHudTitle(kind: string, verb?: string): HudTitle | null {
|
|
89
|
+
const presentation = nodeRegistry.get(kind)?.presentation
|
|
90
|
+
if (!presentation) return null
|
|
91
|
+
const name = presentation.label
|
|
92
|
+
return {
|
|
93
|
+
label: verb ? `${verb} ${name.toLowerCase()}` : name,
|
|
94
|
+
icon: presentation.icon,
|
|
95
|
+
shortcut: verb ? undefined : TOOL_SHORTCUTS[kind],
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The armed build tool. The wall tool is named after the Rooms variant it draws
|
|
101
|
+
* (Rectangle room / Polygon room / Walls) — the tile the user picked.
|
|
102
|
+
*/
|
|
103
|
+
export function toolHudTitle(tool: string, wallMode: ContinuationMode): HudTitle | null {
|
|
104
|
+
if (tool === 'wall') {
|
|
105
|
+
const variant = getWallDrawVariantInfo(wallDrawVariantOf(wallMode))
|
|
106
|
+
return { label: variant.title, icon: url(variant.iconSrc), shortcut: TOOL_SHORTCUTS.wall }
|
|
107
|
+
}
|
|
108
|
+
return nodeKindHudTitle(tool)
|
|
109
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createJSONStorage, type PersistStorage } from 'zustand/middleware'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* localStorage for a persisted store that rehydrates on mount (`skipHydration`).
|
|
5
|
+
*
|
|
6
|
+
* `persist` saves the whole stored state on every `set`. A save before the
|
|
7
|
+
* store has hydrated would replace the stored preferences with the defaults
|
|
8
|
+
* before they are read (a host component's own mount effect runs first), so
|
|
9
|
+
* those writes stay in memory and rehydrate merges the stored blob over them:
|
|
10
|
+
* host code that sets a persisted field before then has to wait for
|
|
11
|
+
* `persist.onFinishHydration`. After that, a save of the blob this tab already
|
|
12
|
+
* holds is skipped, so an update that changes no preference never replaces
|
|
13
|
+
* what another tab saved since. A blob that cannot be read counts as none:
|
|
14
|
+
* hydration still finishes, on the defaults, and the next save replaces it.
|
|
15
|
+
*/
|
|
16
|
+
export function hydrationGatedStorage<S>(
|
|
17
|
+
hasHydrated: () => boolean,
|
|
18
|
+
): PersistStorage<S> | undefined {
|
|
19
|
+
let held: string | null = null
|
|
20
|
+
const storage = createJSONStorage<S>(() => {
|
|
21
|
+
const local = window.localStorage
|
|
22
|
+
return {
|
|
23
|
+
getItem: (name) => {
|
|
24
|
+
held = local.getItem(name)
|
|
25
|
+
return held
|
|
26
|
+
},
|
|
27
|
+
setItem: (name, value) => {
|
|
28
|
+
if (!hasHydrated() || value === held) return
|
|
29
|
+
held = value
|
|
30
|
+
local.setItem(name, value)
|
|
31
|
+
},
|
|
32
|
+
removeItem: (name) => {
|
|
33
|
+
held = null
|
|
34
|
+
local.removeItem(name)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
if (!storage) return undefined
|
|
39
|
+
return {
|
|
40
|
+
...storage,
|
|
41
|
+
getItem: (name) => {
|
|
42
|
+
try {
|
|
43
|
+
return storage.getItem(name)
|
|
44
|
+
} catch {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { type AnyNode, sceneRegistry } from '@pascal-app/core'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import { collectIfcMeshes, exportPreparedSceneToIfc, ifcFileName } from './ifc-export'
|
|
5
|
+
|
|
6
|
+
const node = (id: string, type: string, parentId: string | null = null) =>
|
|
7
|
+
({
|
|
8
|
+
object: 'node',
|
|
9
|
+
id,
|
|
10
|
+
type,
|
|
11
|
+
parentId,
|
|
12
|
+
visible: true,
|
|
13
|
+
metadata: {},
|
|
14
|
+
children: [],
|
|
15
|
+
}) as unknown as AnyNode
|
|
16
|
+
|
|
17
|
+
function identity(object: THREE.Object3D, id: string) {
|
|
18
|
+
object.userData = { pascalId: id }
|
|
19
|
+
return object
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('collectIfcMeshes', () => {
|
|
23
|
+
const nodes: Record<string, AnyNode> = {
|
|
24
|
+
level_a: node('level_a', 'level'),
|
|
25
|
+
wall_a: { ...node('wall_a', 'wall', 'level_a'), start: [0, 0], end: [1, 0] } as AnyNode,
|
|
26
|
+
item_a: node('item_a', 'item', 'level_a'),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function scene() {
|
|
30
|
+
const root = new THREE.Group()
|
|
31
|
+
const level = identity(new THREE.Group(), 'level_a')
|
|
32
|
+
level.position.y = 3
|
|
33
|
+
root.add(level)
|
|
34
|
+
|
|
35
|
+
const wall = identity(
|
|
36
|
+
new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial()),
|
|
37
|
+
'wall_a',
|
|
38
|
+
)
|
|
39
|
+
level.add(wall)
|
|
40
|
+
|
|
41
|
+
const item = identity(new THREE.Group(), 'item_a')
|
|
42
|
+
item.position.set(2, 0, 0)
|
|
43
|
+
level.add(item)
|
|
44
|
+
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
|
45
|
+
// BoxGeometry has six material groups (one per face).
|
|
46
|
+
const red = new THREE.MeshStandardMaterial({ color: new THREE.Color('#ff0000') })
|
|
47
|
+
const glass = new THREE.MeshStandardMaterial({ transparent: true, opacity: 0.25 })
|
|
48
|
+
const materials = [red, red, red, red, glass, glass]
|
|
49
|
+
const child = new THREE.Mesh(geometry, materials)
|
|
50
|
+
item.add(child)
|
|
51
|
+
const hidden = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial())
|
|
52
|
+
hidden.visible = false
|
|
53
|
+
item.add(hidden)
|
|
54
|
+
return root
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('bakes world transforms per owning node and skips parametric kinds', () => {
|
|
58
|
+
const { meshes: parts, renderedNodeIds } = collectIfcMeshes(scene(), nodes)
|
|
59
|
+
expect([...parts.keys()]).toEqual(['item_a'])
|
|
60
|
+
expect([...renderedNodeIds].sort()).toEqual(['item_a', 'wall_a'])
|
|
61
|
+
const itemParts = parts.get('item_a')!
|
|
62
|
+
expect(itemParts).toHaveLength(6)
|
|
63
|
+
const xs: number[] = []
|
|
64
|
+
const ys: number[] = []
|
|
65
|
+
for (const part of itemParts) {
|
|
66
|
+
expect(part.indices!.length % 3).toBe(0)
|
|
67
|
+
expect(part.positions.length).toBe(4 * 3)
|
|
68
|
+
for (let i = 0; i < part.positions.length; i += 3) {
|
|
69
|
+
xs.push(part.positions[i]!)
|
|
70
|
+
ys.push(part.positions[i + 1]!)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
expect(Math.min(...xs)).toBeCloseTo(1.5)
|
|
74
|
+
expect(Math.max(...xs)).toBeCloseTo(2.5)
|
|
75
|
+
expect(Math.min(...ys)).toBeCloseTo(2.5)
|
|
76
|
+
expect(Math.max(...ys)).toBeCloseTo(3.5)
|
|
77
|
+
const [r, g, b] = itemParts[0]!.color!
|
|
78
|
+
expect([r, g, b].map((channel) => Math.round(channel * 1000) / 1000)).toEqual([1, 0, 0])
|
|
79
|
+
expect(itemParts[5]!.opacity).toBe(0.25)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('feeds the IFC writer', () => {
|
|
83
|
+
const { data, warnings } = exportPreparedSceneToIfc(scene(), nodes, { projectName: 'Demo' })
|
|
84
|
+
expect(data).toContain("FILE_SCHEMA(('IFC4'));")
|
|
85
|
+
expect(data).toContain('IFCFURNISHINGELEMENT(')
|
|
86
|
+
expect(data).toContain('IFCTRIANGULATEDFACESET(')
|
|
87
|
+
expect(warnings).toEqual([])
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('export warnings', () => {
|
|
92
|
+
test('flag nodes drawn in the editor that reached the file without geometry', () => {
|
|
93
|
+
const nodes: Record<string, AnyNode> = {
|
|
94
|
+
level_a: node('level_a', 'level'),
|
|
95
|
+
item_ghost: node('item_ghost', 'item', 'level_a'),
|
|
96
|
+
marker_a: node('marker_a', 'item', 'level_a'),
|
|
97
|
+
}
|
|
98
|
+
// Live editor objects: the item draws on the scene layer; the marker only
|
|
99
|
+
// on an overlay layer, like the spawn point.
|
|
100
|
+
const drawn = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial())
|
|
101
|
+
const overlay = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial())
|
|
102
|
+
overlay.layers.set(5)
|
|
103
|
+
sceneRegistry.nodes.set('item_ghost', drawn)
|
|
104
|
+
sceneRegistry.nodes.set('marker_a', overlay)
|
|
105
|
+
try {
|
|
106
|
+
const { warnings } = exportPreparedSceneToIfc(new THREE.Group(), nodes, {})
|
|
107
|
+
expect(warnings).toEqual([
|
|
108
|
+
'1 object has no exportable geometry and was left out of the IFC file.',
|
|
109
|
+
])
|
|
110
|
+
} finally {
|
|
111
|
+
sceneRegistry.nodes.delete('item_ghost')
|
|
112
|
+
sceneRegistry.nodes.delete('marker_a')
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('visibility default', () => {
|
|
118
|
+
test('a hidden wall is left out when no option is given, as scene preparation does', () => {
|
|
119
|
+
const nodes: Record<string, AnyNode> = {
|
|
120
|
+
level_a: node('level_a', 'level'),
|
|
121
|
+
wall_hidden: {
|
|
122
|
+
...node('wall_hidden', 'wall', 'level_a'),
|
|
123
|
+
visible: false,
|
|
124
|
+
start: [0, 0],
|
|
125
|
+
end: [3, 0],
|
|
126
|
+
} as AnyNode,
|
|
127
|
+
}
|
|
128
|
+
expect(exportPreparedSceneToIfc(new THREE.Group(), nodes, {}).data).not.toContain('IFCWALL(')
|
|
129
|
+
expect(
|
|
130
|
+
exportPreparedSceneToIfc(new THREE.Group(), nodes, { onlyVisible: false }).data,
|
|
131
|
+
).toContain('IFCWALL(')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('ifcFileName', () => {
|
|
136
|
+
test('uses a filesystem-safe project name', () => {
|
|
137
|
+
expect(ifcFileName('House: v2/final', 'model')).toBe('House- v2-final.ifc')
|
|
138
|
+
expect(ifcFileName(' ', 'model_2026-09-30')).toBe('model_2026-09-30.ifc')
|
|
139
|
+
expect(ifcFileName(undefined, 'model')).toBe('model.ifc')
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('material groups retain their surface owner and semantic subpart', () => {
|
|
144
|
+
const owner = node('item_owner', 'item')
|
|
145
|
+
const child = node('item_child', 'item', owner.id)
|
|
146
|
+
const root = identity(new THREE.Group(), owner.id)
|
|
147
|
+
const geometry = new THREE.BoxGeometry()
|
|
148
|
+
const material = new THREE.MeshStandardMaterial()
|
|
149
|
+
const mesh = new THREE.Mesh(
|
|
150
|
+
geometry,
|
|
151
|
+
Array.from({ length: 6 }, () => material),
|
|
152
|
+
)
|
|
153
|
+
mesh.userData = {
|
|
154
|
+
surfaceNodeIds: [child.id, child.id, owner.id, owner.id, owner.id, owner.id],
|
|
155
|
+
pascalIfcRole: 'railing',
|
|
156
|
+
pascalIfcParts: [
|
|
157
|
+
{ start: 0, count: 6, role: 'railing' },
|
|
158
|
+
{ start: 6, count: 30, role: 'handrail' },
|
|
159
|
+
],
|
|
160
|
+
}
|
|
161
|
+
root.add(mesh)
|
|
162
|
+
const { meshes, renderedNodeIds } = collectIfcMeshes(root, {
|
|
163
|
+
[owner.id]: owner,
|
|
164
|
+
[child.id]: child,
|
|
165
|
+
})
|
|
166
|
+
expect(meshes.get(child.id)).toHaveLength(2)
|
|
167
|
+
expect(meshes.get(owner.id)).toHaveLength(4)
|
|
168
|
+
expect(meshes.get(child.id)![0]!.role).toBe('railing')
|
|
169
|
+
expect(meshes.get(child.id)![1]!.role).toBe('handrail')
|
|
170
|
+
expect(meshes.get(owner.id)!.every((part) => part.role === 'handrail')).toBe(true)
|
|
171
|
+
expect(renderedNodeIds.has(child.id)).toBe(true)
|
|
172
|
+
geometry.dispose()
|
|
173
|
+
material.dispose()
|
|
174
|
+
})
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { type AnyNode, bakePolicyOf, type Collection, sceneRegistry } from '@pascal-app/core'
|
|
2
|
+
import { buildIfcExport, type IfcMeshPart } from '@pascal-app/ifc-converter/export'
|
|
3
|
+
import { SCENE_LAYER } from '@pascal-app/viewer'
|
|
4
|
+
import * as THREE from 'three'
|
|
5
|
+
import { isIfcRolePart } from './ifc-parts'
|
|
6
|
+
|
|
7
|
+
// Kinds the IFC writer always rebuilds parametrically from node data; their
|
|
8
|
+
// rendered triangles are never read, so skip copying them. (Slabs are copied:
|
|
9
|
+
// pools and legacy zero-thickness slabs export their rendered shape.)
|
|
10
|
+
const PARAMETRIC_KINDS = new Set(['site', 'building', 'level', 'wall', 'ceiling', 'zone'])
|
|
11
|
+
|
|
12
|
+
function owningNodeId(object: THREE.Object3D): string | null {
|
|
13
|
+
for (let current: THREE.Object3D | null = object; current; current = current.parent) {
|
|
14
|
+
const id = current.userData?.pascalId
|
|
15
|
+
if (typeof id === 'string') return id
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function materialColor(
|
|
21
|
+
material: THREE.Material | undefined,
|
|
22
|
+
): Pick<IfcMeshPart, 'color' | 'opacity'> {
|
|
23
|
+
const color = (material as THREE.MeshStandardMaterial | undefined)?.color
|
|
24
|
+
const rgb = color ? color.getRGB(new THREE.Color(), THREE.SRGBColorSpace) : null
|
|
25
|
+
return {
|
|
26
|
+
color: rgb ? [rgb.r, rgb.g, rgb.b] : undefined,
|
|
27
|
+
opacity: material?.transparent ? material.opacity : 1,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* World-space triangles per node from a prepared export scene (identity
|
|
33
|
+
* stamped, instancing expanded, deformation frozen, reflected winding fixed).
|
|
34
|
+
* Each material group becomes one part with only the vertices it uses.
|
|
35
|
+
* `renderedNodeIds` lists every node that drew at least one triangle.
|
|
36
|
+
*/
|
|
37
|
+
export function collectIfcMeshes(
|
|
38
|
+
root: THREE.Object3D,
|
|
39
|
+
nodes: Record<string, AnyNode>,
|
|
40
|
+
): { meshes: Map<string, IfcMeshPart[]>; renderedNodeIds: Set<string> } {
|
|
41
|
+
root.updateMatrixWorld(true)
|
|
42
|
+
const parts = new Map<string, IfcMeshPart[]>()
|
|
43
|
+
const renderedNodeIds = new Set<string>()
|
|
44
|
+
const vertex = new THREE.Vector3()
|
|
45
|
+
|
|
46
|
+
root.traverseVisible((object) => {
|
|
47
|
+
const mesh = object as THREE.Mesh
|
|
48
|
+
if (!mesh.isMesh) return
|
|
49
|
+
const position = mesh.geometry?.getAttribute('position')
|
|
50
|
+
if (!position || position.count < 3) return
|
|
51
|
+
const nodeId = owningNodeId(mesh)
|
|
52
|
+
const node = nodeId ? nodes[nodeId] : undefined
|
|
53
|
+
if (!node) return
|
|
54
|
+
renderedNodeIds.add(node.id)
|
|
55
|
+
if (PARAMETRIC_KINDS.has(node.type)) return
|
|
56
|
+
|
|
57
|
+
const geometry = mesh.geometry
|
|
58
|
+
const drawStart = geometry.drawRange.start
|
|
59
|
+
const drawEnd = Math.min(
|
|
60
|
+
geometry.index ? geometry.index.count : position.count,
|
|
61
|
+
drawStart + geometry.drawRange.count,
|
|
62
|
+
)
|
|
63
|
+
const indexAt = (i: number) => (geometry.index ? geometry.index.getX(i) : i)
|
|
64
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
|
65
|
+
const groups =
|
|
66
|
+
Array.isArray(mesh.material) && geometry.groups.length > 0
|
|
67
|
+
? geometry.groups
|
|
68
|
+
: [{ start: drawStart, count: drawEnd - drawStart, materialIndex: 0 }]
|
|
69
|
+
|
|
70
|
+
const roleParts = Array.isArray(mesh.userData.pascalIfcParts)
|
|
71
|
+
? mesh.userData.pascalIfcParts.filter(isIfcRolePart)
|
|
72
|
+
: []
|
|
73
|
+
const semanticGroups = groups.flatMap((group) => {
|
|
74
|
+
const end = group.start + group.count
|
|
75
|
+
const boundaries = [
|
|
76
|
+
...new Set([
|
|
77
|
+
group.start,
|
|
78
|
+
end,
|
|
79
|
+
...roleParts
|
|
80
|
+
.flatMap((part: { start: number; count: number }) => [
|
|
81
|
+
part.start,
|
|
82
|
+
part.start + part.count,
|
|
83
|
+
])
|
|
84
|
+
.filter((offset: number) => offset > group.start && offset < end),
|
|
85
|
+
]),
|
|
86
|
+
].sort((a, b) => a - b)
|
|
87
|
+
return boundaries.slice(0, -1).map((start, index) => ({
|
|
88
|
+
...group,
|
|
89
|
+
start,
|
|
90
|
+
count: boundaries[index + 1]! - start,
|
|
91
|
+
role: roleParts.find(
|
|
92
|
+
(part: { start: number; count: number }) =>
|
|
93
|
+
start >= part.start && start < part.start + part.count,
|
|
94
|
+
)?.role as string | undefined,
|
|
95
|
+
}))
|
|
96
|
+
})
|
|
97
|
+
for (const group of semanticGroups) {
|
|
98
|
+
const surfaceId = mesh.userData.surfaceNodeIds?.[group.materialIndex ?? 0]
|
|
99
|
+
const owner = typeof surfaceId === 'string' && nodes[surfaceId] ? nodes[surfaceId]! : node
|
|
100
|
+
let role: string | undefined = group.role
|
|
101
|
+
for (let current: THREE.Object3D | null = mesh; current; current = current.parent) {
|
|
102
|
+
if (role === undefined && typeof current.userData.pascalIfcRole === 'string') {
|
|
103
|
+
role = current.userData.pascalIfcRole
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const material = materials[group.materialIndex ?? 0]
|
|
108
|
+
if (!material || material.visible === false) continue
|
|
109
|
+
const start = Math.max(group.start, drawStart)
|
|
110
|
+
const end = Math.min(group.start + group.count, drawEnd)
|
|
111
|
+
const remap = new Map<number, number>()
|
|
112
|
+
const positions: number[] = []
|
|
113
|
+
const indices: number[] = []
|
|
114
|
+
for (let i = start; i + 2 < end; i += 3) {
|
|
115
|
+
for (let corner = 0; corner < 3; corner++) {
|
|
116
|
+
const source = indexAt(i + corner)
|
|
117
|
+
let target = remap.get(source)
|
|
118
|
+
if (target === undefined) {
|
|
119
|
+
target = remap.size
|
|
120
|
+
remap.set(source, target)
|
|
121
|
+
vertex.fromBufferAttribute(position, source).applyMatrix4(mesh.matrixWorld)
|
|
122
|
+
positions.push(vertex.x, vertex.y, vertex.z)
|
|
123
|
+
}
|
|
124
|
+
indices.push(target)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (indices.length === 0) continue
|
|
128
|
+
const list = parts.get(owner.id) ?? []
|
|
129
|
+
list.push({
|
|
130
|
+
role,
|
|
131
|
+
positions: new Float32Array(positions),
|
|
132
|
+
indices: new Uint32Array(indices),
|
|
133
|
+
...materialColor(material),
|
|
134
|
+
})
|
|
135
|
+
parts.set(owner.id, list)
|
|
136
|
+
renderedNodeIds.add(owner.id)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
return { meshes: parts, renderedNodeIds }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Whether the live editor draws `nodeId`: a visible scene-layer mesh under its
|
|
144
|
+
* registered object, or a collectively rendered (`bake: 'replace'`) kind whose
|
|
145
|
+
* instances live outside that object. Overlay-only markers (spawn point,
|
|
146
|
+
* camera shots) draw nothing here.
|
|
147
|
+
*/
|
|
148
|
+
function drawnInEditor(nodeId: string, nodes: Record<string, AnyNode>): boolean {
|
|
149
|
+
const node = nodes[nodeId]
|
|
150
|
+
const live = sceneRegistry.nodes.get(nodeId)
|
|
151
|
+
if (!node || !live) return false
|
|
152
|
+
if (bakePolicyOf(node.type) === 'replace') return true
|
|
153
|
+
let drawn = false
|
|
154
|
+
live.traverseVisible((object) => {
|
|
155
|
+
const mesh = object as THREE.Mesh
|
|
156
|
+
if (drawn || !mesh.isMesh || !object.layers.isEnabled(SCENE_LAYER)) return
|
|
157
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
|
158
|
+
const count = mesh.geometry?.getAttribute('position')?.count ?? 0
|
|
159
|
+
if (count >= 3 && materials.some((material) => material?.visible !== false)) drawn = true
|
|
160
|
+
})
|
|
161
|
+
return drawn
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function ifcFileName(projectName: string | undefined, fallback: string): string {
|
|
165
|
+
const base = projectName?.replace(/[\\/:*?"<>|]+/g, '-').trim()
|
|
166
|
+
return `${base || fallback}.ifc`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function exportPreparedSceneToIfc(
|
|
170
|
+
root: THREE.Object3D,
|
|
171
|
+
nodes: Record<string, AnyNode>,
|
|
172
|
+
options: {
|
|
173
|
+
collections?: Readonly<Record<string, Collection>>
|
|
174
|
+
projectName?: string
|
|
175
|
+
onlyVisible?: boolean
|
|
176
|
+
excludedNodeTypes?: readonly string[]
|
|
177
|
+
},
|
|
178
|
+
): { data: string; warnings: string[] } {
|
|
179
|
+
const { meshes, renderedNodeIds } = collectIfcMeshes(root, nodes)
|
|
180
|
+
const { ifc, summary } = buildIfcExport({
|
|
181
|
+
nodes,
|
|
182
|
+
collections: options.collections,
|
|
183
|
+
meshes,
|
|
184
|
+
projectName: options.projectName,
|
|
185
|
+
// Same default as scene preparation, which already pruned hidden meshes.
|
|
186
|
+
onlyVisible: options.onlyVisible ?? true,
|
|
187
|
+
excludedNodeTypes: options.excludedNodeTypes,
|
|
188
|
+
})
|
|
189
|
+
// Report everything that is on screen yet did not reach the file; markers
|
|
190
|
+
// that draw nothing (spawn point, camera shots) are skipped silently.
|
|
191
|
+
const missing = summary.skipped.filter(
|
|
192
|
+
(skip) => renderedNodeIds.has(skip.nodeId) || drawnInEditor(skip.nodeId, nodes),
|
|
193
|
+
).length
|
|
194
|
+
const warnings =
|
|
195
|
+
missing > 0
|
|
196
|
+
? [
|
|
197
|
+
`${missing} ${missing === 1 ? 'object has' : 'objects have'} no exportable geometry and ${missing === 1 ? 'was' : 'were'} left out of the IFC file.`,
|
|
198
|
+
]
|
|
199
|
+
: []
|
|
200
|
+
return { data: ifc, warnings }
|
|
201
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type IfcRolePart = { start: number; count: number; role: string }
|
|
2
|
+
|
|
3
|
+
export function isIfcRolePart(value: unknown): value is IfcRolePart {
|
|
4
|
+
if (!value || typeof value !== 'object') return false
|
|
5
|
+
const part = value as IfcRolePart
|
|
6
|
+
return (
|
|
7
|
+
Number.isInteger(part.start) &&
|
|
8
|
+
part.start >= 0 &&
|
|
9
|
+
part.start % 3 === 0 &&
|
|
10
|
+
Number.isInteger(part.count) &&
|
|
11
|
+
part.count > 0 &&
|
|
12
|
+
part.count % 3 === 0 &&
|
|
13
|
+
typeof part.role === 'string'
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
const saved = Object.getOwnPropertyDescriptor(globalThis, 'localStorage')
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
if (saved) Object.defineProperty(globalThis, 'localStorage', saved)
|
|
6
|
+
else Reflect.deleteProperty(globalThis, 'localStorage')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
function stubStorage(storage: Partial<Storage>) {
|
|
10
|
+
Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: storage })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let fresh = 0
|
|
14
|
+
/** A new instance of the module, reading storage as a page load would. */
|
|
15
|
+
const load = () => import(`./inspector-expanded.ts?load=${++fresh}`)
|
|
16
|
+
|
|
17
|
+
test('expanding once opens every later panel expanded, across a reload, until folded', async () => {
|
|
18
|
+
const values = new Map<string, string>()
|
|
19
|
+
stubStorage({
|
|
20
|
+
getItem: (key) => values.get(key) ?? null,
|
|
21
|
+
setItem: (key, value) => void values.set(key, value),
|
|
22
|
+
})
|
|
23
|
+
const first = await load()
|
|
24
|
+
expect(first.useInspectorExpanded.getState().expanded).toBe(false)
|
|
25
|
+
first.useInspectorExpanded.getState().setExpanded(true)
|
|
26
|
+
expect((await load()).useInspectorExpanded.getState().expanded).toBe(true)
|
|
27
|
+
first.useInspectorExpanded.getState().setExpanded(false)
|
|
28
|
+
expect((await load()).useInspectorExpanded.getState().expanded).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('without usable storage the panel opens folded and expanding still works', async () => {
|
|
32
|
+
stubStorage({
|
|
33
|
+
getItem: () => {
|
|
34
|
+
throw new Error('blocked')
|
|
35
|
+
},
|
|
36
|
+
setItem: () => {
|
|
37
|
+
throw new Error('blocked')
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
const { useInspectorExpanded } = await load()
|
|
41
|
+
expect(useInspectorExpanded.getState().expanded).toBe(false)
|
|
42
|
+
useInspectorExpanded.getState().setExpanded(true)
|
|
43
|
+
expect(useInspectorExpanded.getState().expanded).toBe(true)
|
|
44
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
// One choice for every right-side inspector panel (room, wall, item…): opened
|
|
4
|
+
// expanded or folded to its header, kept per viewer until they fold it again.
|
|
5
|
+
// Its own key, read when the module loads: the editor's persisted preferences
|
|
6
|
+
// rehydrate after the first render, too late for a panel that opens with the
|
|
7
|
+
// editor. Storage can be missing or refuse (private windows, blocked site
|
|
8
|
+
// data); the panel then opens folded, as it always has.
|
|
9
|
+
const STORAGE_KEY = 'pascal-editor:inspector-expanded'
|
|
10
|
+
|
|
11
|
+
function readExpanded(): boolean {
|
|
12
|
+
try {
|
|
13
|
+
return globalThis.localStorage?.getItem(STORAGE_KEY) === 'true'
|
|
14
|
+
} catch {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeExpanded(expanded: boolean) {
|
|
20
|
+
try {
|
|
21
|
+
globalThis.localStorage?.setItem(STORAGE_KEY, String(expanded))
|
|
22
|
+
} catch {
|
|
23
|
+
// Not remembered this time; the panel still follows the choice until reload.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const useInspectorExpanded = create<{
|
|
28
|
+
expanded: boolean
|
|
29
|
+
setExpanded: (expanded: boolean) => void
|
|
30
|
+
}>((set) => ({
|
|
31
|
+
expanded: readExpanded(),
|
|
32
|
+
setExpanded: (expanded) => {
|
|
33
|
+
writeExpanded(expanded)
|
|
34
|
+
set({ expanded })
|
|
35
|
+
},
|
|
36
|
+
}))
|
|
@@ -19,10 +19,35 @@ export type ReshapeDriver = 'tool' | 'floorplan'
|
|
|
19
19
|
// node, one in-flight reshape. Grouping them as sub-states of `reshaping`
|
|
20
20
|
// (rather than four sibling scopes) keeps the union small while still making
|
|
21
21
|
// "curving and hole-editing at once" unrepresentable.
|
|
22
|
-
export type ReshapeKind =
|
|
22
|
+
export type ReshapeKind =
|
|
23
|
+
| 'curve'
|
|
24
|
+
| 'hole'
|
|
25
|
+
| 'endpoint'
|
|
26
|
+
| 'boundary'
|
|
27
|
+
| 'control-point'
|
|
28
|
+
| 'tangent'
|
|
29
|
+
// A kind's own reshape (wall 'split'): mounted from `def.affordanceTools[reshape]`.
|
|
30
|
+
| 'split'
|
|
23
31
|
|
|
24
32
|
export type InteractionScope =
|
|
25
33
|
| { kind: 'idle' }
|
|
34
|
+
| {
|
|
35
|
+
kind: 'room-divide'
|
|
36
|
+
nodeId: string
|
|
37
|
+
levelId: string
|
|
38
|
+
// The placed points. The first lies on the room's boundary (an open cut)
|
|
39
|
+
// or inside the room (an island loop); the rest are free points.
|
|
40
|
+
points: [number, number][]
|
|
41
|
+
// The boundary the first point sits on, when it sits on one.
|
|
42
|
+
startBoundaryId?: string
|
|
43
|
+
// The live point under the pointer and what clicking it would do: start
|
|
44
|
+
// the path, add a point, finish on the boundary (`edge`), or close the loop.
|
|
45
|
+
end: [number, number] | null
|
|
46
|
+
endKind?: 'start' | 'point' | 'edge' | 'close'
|
|
47
|
+
endBoundaryId?: string
|
|
48
|
+
valid: boolean
|
|
49
|
+
message?: string
|
|
50
|
+
}
|
|
26
51
|
// Placing a fresh node (catalog/preset/build tool). `pressDrag` is the
|
|
27
52
|
// gizmo press-drag flavour (commit on release) vs click-to-place.
|
|
28
53
|
| {
|
|
@@ -105,6 +130,7 @@ export function isActive(scope: InteractionScope): scope is ActiveInteractionSco
|
|
|
105
130
|
// target no single existing node.
|
|
106
131
|
export function scopeNodeId(scope: InteractionScope): string | null {
|
|
107
132
|
switch (scope.kind) {
|
|
133
|
+
case 'room-divide':
|
|
108
134
|
case 'placing':
|
|
109
135
|
case 'moving':
|
|
110
136
|
case 'handle-drag':
|