@pascal-app/editor 1.0.3 → 1.0.5
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/README.md +106 -0
- package/package.json +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +275 -57
- 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 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -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-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- 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 +269 -197
- package/src/components/editor/index.tsx +409 -197
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- 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 +80 -1
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- 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 +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -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 +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- 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 +663 -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.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- 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 +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- 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 +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- 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-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- 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/roof/roof-edit-system.tsx +1 -0
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
- 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 +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
- 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/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +14 -19
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- 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 +28 -22
- 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 +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- 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-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
- 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 +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- 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 +790 -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/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- 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 +181 -223
- 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 +13 -37
- 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 +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- 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.tsx +5 -6
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -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 +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- 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 +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -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/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- 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 +284 -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 +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -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 +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -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/pdfkit-standard-fonts.d.ts +5 -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 +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -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 +230 -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 +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -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 +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -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 +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- 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 +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- 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 +187 -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 +392 -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 +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -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 +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -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 +122 -0
- package/src/lib/paint-pass-through.ts +78 -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 +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- 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 +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- 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 +265 -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 +1017 -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 +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- 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 +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -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 +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -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 +580 -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-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -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-changes.ts +35 -0
- package/src/lib/window-interaction.ts +14 -0
- 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/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- 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 +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -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 +201 -0
- package/src/store/view-layout.ts +199 -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/render-current-markup.tsx +16 -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/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- 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,58 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
collectionIdsOf,
|
|
4
5
|
generateId,
|
|
5
6
|
generateSceneMaterialId,
|
|
7
|
+
getArtifactStore,
|
|
8
|
+
isDerivedNode,
|
|
9
|
+
isStoreyContent,
|
|
6
10
|
type LevelNode,
|
|
7
11
|
nodeRegistry,
|
|
12
|
+
type ParsedBuildJson,
|
|
8
13
|
remapMeasurementReferences,
|
|
14
|
+
runAsSingleSceneHistoryStep,
|
|
9
15
|
SceneMaterial,
|
|
10
16
|
type SceneMaterialId,
|
|
11
17
|
type StairNode,
|
|
18
|
+
scriptedSize,
|
|
19
|
+
seatRoofs,
|
|
20
|
+
storeyOf,
|
|
12
21
|
useScene,
|
|
13
22
|
} from '@pascal-app/core'
|
|
23
|
+
import { clampDoorToWall, clampWindowToWall } from '@pascal-app/core/building'
|
|
14
24
|
import { useViewer } from '@pascal-app/viewer'
|
|
25
|
+
import { referencedSceneMaterialIds, remapSceneMaterialRefs } from './scene-material-refs'
|
|
15
26
|
|
|
16
27
|
type ClipboardPayload = {
|
|
17
28
|
copiedAt: number
|
|
18
29
|
materials: SceneMaterial[]
|
|
19
30
|
nodes: AnyNode[]
|
|
31
|
+
/** The project copied from, where the scripted nodes' artifacts live. */
|
|
32
|
+
projectId: string | null
|
|
20
33
|
rootIds: AnyNodeId[]
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export type PasteResult = {
|
|
24
37
|
createdMaterialIds: SceneMaterialId[]
|
|
25
38
|
pastedIds: AnyNodeId[]
|
|
39
|
+
/** Scripted nodes left out (with what they host): their artifacts could not come from the project they were copied from. */
|
|
40
|
+
refusedIds: AnyNodeId[]
|
|
41
|
+
/** Why they were left out, when any were. */
|
|
42
|
+
refusal: PasteRefusal | null
|
|
26
43
|
skippedIds: AnyNodeId[]
|
|
27
44
|
}
|
|
28
45
|
|
|
46
|
+
/**
|
|
47
|
+
* `no-access`: the source project cannot be read any more (removed from it,
|
|
48
|
+
* deleted, made private, another account). `no-copies`: its geometry is
|
|
49
|
+
* readable but this script version cannot be copied. `failed`: the copy itself failed.
|
|
50
|
+
*/
|
|
51
|
+
export type PasteRefusal = 'no-access' | 'no-copies' | 'failed'
|
|
52
|
+
|
|
53
|
+
type Refused = { ids: ReadonlySet<AnyNodeId>; reason: PasteRefusal | null }
|
|
54
|
+
const NONE_REFUSED: Refused = { ids: new Set(), reason: null }
|
|
55
|
+
|
|
29
56
|
const SYSTEM_CLIPBOARD_KIND = 'pascal.scene-nodes'
|
|
30
57
|
const SYSTEM_CLIPBOARD_VERSION = 1
|
|
31
58
|
|
|
@@ -84,7 +111,7 @@ function collectSubtreeIds(
|
|
|
84
111
|
) {
|
|
85
112
|
if (ids.has(rootId)) return
|
|
86
113
|
const node = nodes[rootId]
|
|
87
|
-
if (!node) return
|
|
114
|
+
if (!node || isDerivedNode(node)) return
|
|
88
115
|
ids.add(rootId)
|
|
89
116
|
|
|
90
117
|
if ('children' in node && Array.isArray(node.children)) {
|
|
@@ -129,6 +156,7 @@ function isClipboardRoot(
|
|
|
129
156
|
}
|
|
130
157
|
|
|
131
158
|
function isCopyableRootType(node: AnyNode) {
|
|
159
|
+
if (isDerivedNode(node)) return false
|
|
132
160
|
if (COPYABLE_ROOT_TYPES.has(node.type)) return true
|
|
133
161
|
const definition = nodeRegistry.get(node.type)
|
|
134
162
|
return !!definition && definition.capabilities?.duplicable !== false
|
|
@@ -195,10 +223,7 @@ function remapNodeReferences(
|
|
|
195
223
|
rootIds: Set<AnyNodeId>,
|
|
196
224
|
nodes: Record<AnyNodeId, AnyNode>,
|
|
197
225
|
) {
|
|
198
|
-
const clone =
|
|
199
|
-
JSON.parse(JSON.stringify(node)),
|
|
200
|
-
materialIdMap,
|
|
201
|
-
) as AnyNode
|
|
226
|
+
const clone = remapSceneMaterialRefs(JSON.parse(JSON.stringify(node)) as AnyNode, materialIdMap)
|
|
202
227
|
;(clone as Record<string, unknown>).id = idMap.get(oldId)
|
|
203
228
|
|
|
204
229
|
if (rootIds.has(oldId)) {
|
|
@@ -217,6 +242,11 @@ function remapNodeReferences(
|
|
|
217
242
|
clone.parentId = idMap.get(clone.parentId as AnyNodeId) ?? clone.parentId
|
|
218
243
|
}
|
|
219
244
|
|
|
245
|
+
if (clone.type === 'zone' && node.parentId !== clone.parentId && clone.floor?.footprint) {
|
|
246
|
+
const { footprint: _footprint, ...floor } = clone.floor
|
|
247
|
+
clone.floor = floor
|
|
248
|
+
}
|
|
249
|
+
|
|
220
250
|
if ('children' in clone && Array.isArray(clone.children)) {
|
|
221
251
|
;(clone as Record<string, unknown>).children = (clone.children as AnyNodeId[])
|
|
222
252
|
.map((childId) => idMap.get(childId))
|
|
@@ -232,6 +262,13 @@ function remapNodeReferences(
|
|
|
232
262
|
}
|
|
233
263
|
}
|
|
234
264
|
|
|
265
|
+
// A corner window pasted with its partner joins the partner's copy; pasted alone, it is plain.
|
|
266
|
+
if (clone.type === 'window' && clone.corner) {
|
|
267
|
+
const partnerId = idMap.get(clone.corner.partnerId as AnyNodeId)
|
|
268
|
+
if (partnerId) clone.corner = { ...clone.corner, partnerId }
|
|
269
|
+
else delete clone.corner
|
|
270
|
+
}
|
|
271
|
+
|
|
235
272
|
if (clone.type === 'stair') {
|
|
236
273
|
const nextLevelId = getNextLevelId(targetLevel, nodes)
|
|
237
274
|
;(clone as StairNode).fromLevelId = targetLevel.id
|
|
@@ -253,43 +290,6 @@ function remapNodeReferences(
|
|
|
253
290
|
return AnyNode.parse(clone)
|
|
254
291
|
}
|
|
255
292
|
|
|
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
293
|
function buildClipboardPayload(ids: AnyNodeId[]): ClipboardPayload | null {
|
|
294
294
|
const scene = useScene.getState()
|
|
295
295
|
const selectedIdSet = new Set(ids)
|
|
@@ -321,8 +321,7 @@ function buildClipboardPayload(ids: AnyNodeId[]): ClipboardPayload | null {
|
|
|
321
321
|
.map((id) => scene.nodes[id])
|
|
322
322
|
.filter((node): node is AnyNode => !!node)
|
|
323
323
|
.map((node) => JSON.parse(JSON.stringify(node)) as AnyNode)
|
|
324
|
-
const materialIds =
|
|
325
|
-
collectReferencedSceneMaterialIds(copiedNodes, materialIds)
|
|
324
|
+
const materialIds = referencedSceneMaterialIds(copiedNodes)
|
|
326
325
|
|
|
327
326
|
return {
|
|
328
327
|
copiedAt: Date.now(),
|
|
@@ -331,6 +330,7 @@ function buildClipboardPayload(ids: AnyNodeId[]): ClipboardPayload | null {
|
|
|
331
330
|
.filter((material): material is SceneMaterial => !!material)
|
|
332
331
|
.map((material) => JSON.parse(JSON.stringify(material)) as SceneMaterial),
|
|
333
332
|
nodes: copiedNodes,
|
|
333
|
+
projectId: useViewer.getState().projectId,
|
|
334
334
|
rootIds,
|
|
335
335
|
}
|
|
336
336
|
}
|
|
@@ -363,6 +363,7 @@ function parseClipboardPayload(text: string): ClipboardPayload | null {
|
|
|
363
363
|
copiedAt?: unknown
|
|
364
364
|
materials?: unknown
|
|
365
365
|
nodes?: unknown
|
|
366
|
+
projectId?: unknown
|
|
366
367
|
rootIds?: unknown
|
|
367
368
|
}
|
|
368
369
|
if (
|
|
@@ -390,6 +391,7 @@ function parseClipboardPayload(text: string): ClipboardPayload | null {
|
|
|
390
391
|
copiedAt: candidate.copiedAt,
|
|
391
392
|
materials: materials.filter((result) => result.success).map((result) => result.data),
|
|
392
393
|
nodes: parsedNodes,
|
|
394
|
+
projectId: typeof candidate.projectId === 'string' ? candidate.projectId : null,
|
|
393
395
|
rootIds,
|
|
394
396
|
}
|
|
395
397
|
} catch {
|
|
@@ -470,23 +472,154 @@ export function pasteEditorClipboardToLevel(targetLevelId?: AnyNodeId): PasteRes
|
|
|
470
472
|
export async function pasteSystemEditorClipboardToLevel(
|
|
471
473
|
targetLevelId?: AnyNodeId,
|
|
472
474
|
): Promise<PasteResult | null> {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
+
const projectId = useViewer.getState().projectId
|
|
476
|
+
const store = getArtifactStore()
|
|
477
|
+
if (!(await readEditorClipboardFromSystem()) || !clipboardPayload) return null
|
|
478
|
+
if (projectId !== useViewer.getState().projectId || store !== getArtifactStore()) return null
|
|
479
|
+
const payload = clipboardPayload
|
|
480
|
+
const targetLevel = getPasteTargetLevel(targetLevelId)
|
|
481
|
+
if (!targetLevel) return null
|
|
482
|
+
const refused =
|
|
483
|
+
payload.projectId && payload.projectId === projectId
|
|
484
|
+
? NONE_REFUSED
|
|
485
|
+
: await copyArtifactsHere(payload)
|
|
486
|
+
// Navigation can replace the scene while the server copies its artifacts.
|
|
487
|
+
if (projectId !== useViewer.getState().projectId || store !== getArtifactStore()) return null
|
|
488
|
+
return applyClipboardPayloadToLevel(payload, targetLevel.id, refused)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** The script and GLB a scripted node, a room's floor script or a wall's face scripts reference. */
|
|
492
|
+
function artifactHashes(node: AnyNode): string[] {
|
|
493
|
+
const sources =
|
|
494
|
+
node.type === 'zone'
|
|
495
|
+
? [node.floor?.script]
|
|
496
|
+
: node.type === 'wall'
|
|
497
|
+
? [node.finishScripts?.a, node.finishScripts?.b]
|
|
498
|
+
: ['source' in node ? node.source : undefined]
|
|
499
|
+
return sources.flatMap((source) =>
|
|
500
|
+
typeof source === 'object' && source?.kind === 'script' ? [source.script, source.artifact] : [],
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* A paste from another project first brings the artifacts its scripted nodes
|
|
506
|
+
* reference into this one. Returns the nodes whose artifacts could not come,
|
|
507
|
+
* which the paste leaves out with everything they host, and why.
|
|
508
|
+
*/
|
|
509
|
+
async function copyArtifactsHere(
|
|
510
|
+
payload: Pick<ClipboardPayload, 'nodes' | 'projectId'>,
|
|
511
|
+
): Promise<Refused> {
|
|
512
|
+
const store = getArtifactStore()
|
|
513
|
+
const hashes = [...new Set(payload.nodes.flatMap(artifactHashes))]
|
|
514
|
+
if (hashes.length === 0) return NONE_REFUSED
|
|
515
|
+
|
|
516
|
+
let failed = false
|
|
517
|
+
const missing = new Set(
|
|
518
|
+
store.copyFrom
|
|
519
|
+
? payload.projectId
|
|
520
|
+
? await store.copyFrom(payload.projectId, hashes).catch(() => {
|
|
521
|
+
failed = true
|
|
522
|
+
return hashes
|
|
523
|
+
})
|
|
524
|
+
: hashes
|
|
525
|
+
: hashes.filter((sha) => !store.url(sha)),
|
|
526
|
+
)
|
|
527
|
+
const ids = new Set<AnyNodeId>()
|
|
528
|
+
let geometryMissing = false
|
|
529
|
+
for (const node of payload.nodes) {
|
|
530
|
+
const [script, artifact] = artifactHashes(node)
|
|
531
|
+
if (!script || !artifact || !(missing.has(script) || missing.has(artifact))) continue
|
|
532
|
+
ids.add(node.id as AnyNodeId)
|
|
533
|
+
geometryMissing ||= missing.has(artifact)
|
|
534
|
+
}
|
|
535
|
+
if (ids.size === 0) return NONE_REFUSED
|
|
536
|
+
return { ids, reason: failed ? 'failed' : geometryMissing ? 'no-access' : 'no-copies' }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* A build file loaded into another project first brings the artifacts its
|
|
541
|
+
* scripted nodes reference from the project it was saved in, as a paste does.
|
|
542
|
+
* Answers the build without the nodes whose artifacts could not come, nor
|
|
543
|
+
* anything they host.
|
|
544
|
+
*/
|
|
545
|
+
export async function bringBuildArtifacts(build: ParsedBuildJson): Promise<{
|
|
546
|
+
build: ParsedBuildJson
|
|
547
|
+
refusedIds: AnyNodeId[]
|
|
548
|
+
refusal: PasteRefusal | null
|
|
549
|
+
}> {
|
|
550
|
+
const nodes = Object.values(build.nodes) as AnyNode[]
|
|
551
|
+
// A file's origin is untrusted, even when it names this project. Older files can
|
|
552
|
+
// still use artifacts already registered here, but cannot introduce missing ones.
|
|
553
|
+
const refused = await copyArtifactsHere({
|
|
554
|
+
nodes,
|
|
555
|
+
projectId: build.projectId ?? useViewer.getState().projectId,
|
|
556
|
+
})
|
|
557
|
+
if (refused.ids.size === 0) return { build, refusedIds: [], refusal: null }
|
|
558
|
+
|
|
559
|
+
const excluded = new Set<string>(refused.ids)
|
|
560
|
+
for (let grew = true; grew; ) {
|
|
561
|
+
grew = false
|
|
562
|
+
for (const node of nodes) {
|
|
563
|
+
if (node.parentId && excluded.has(node.parentId) && !excluded.has(node.id)) {
|
|
564
|
+
excluded.add(node.id)
|
|
565
|
+
grew = true
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const kept: Record<string, unknown> = {}
|
|
570
|
+
for (const node of nodes) {
|
|
571
|
+
if (excluded.has(node.id)) continue
|
|
572
|
+
const children = (node as { children?: unknown }).children
|
|
573
|
+
kept[node.id] = Array.isArray(children)
|
|
574
|
+
? { ...node, children: children.filter((id) => !excluded.has(id as string)) }
|
|
575
|
+
: node
|
|
576
|
+
}
|
|
577
|
+
const collections =
|
|
578
|
+
build.collections &&
|
|
579
|
+
Object.fromEntries(
|
|
580
|
+
Object.entries(build.collections).map(([id, collection]) => [
|
|
581
|
+
id,
|
|
582
|
+
{ ...collection, nodeIds: collection.nodeIds.filter((nodeId) => !excluded.has(nodeId)) },
|
|
583
|
+
]),
|
|
584
|
+
)
|
|
585
|
+
return {
|
|
586
|
+
build: {
|
|
587
|
+
...build,
|
|
588
|
+
nodes: kept,
|
|
589
|
+
rootNodeIds: build.rootNodeIds.filter((id) => !excluded.has(id)),
|
|
590
|
+
...(collections ? { collections } : {}),
|
|
591
|
+
},
|
|
592
|
+
refusedIds: [...refused.ids],
|
|
593
|
+
refusal: refused.reason,
|
|
594
|
+
}
|
|
475
595
|
}
|
|
476
596
|
|
|
477
597
|
function applyClipboardPayloadToLevel(
|
|
478
|
-
|
|
598
|
+
copied: ClipboardPayload,
|
|
479
599
|
targetLevelId?: AnyNodeId,
|
|
600
|
+
{ ids: refused, reason: refusal }: Refused = NONE_REFUSED,
|
|
480
601
|
): PasteResult | null {
|
|
481
|
-
const
|
|
482
|
-
if (!
|
|
602
|
+
const aimedLevel = getPasteTargetLevel(targetLevelId)
|
|
603
|
+
if (!aimedLevel) return null
|
|
604
|
+
const payload = refused.size > 0 ? withoutRefusedSubtrees(copied, refused) : copied
|
|
483
605
|
|
|
484
606
|
const scene = useScene.getState()
|
|
607
|
+
// A paste lands on one level. On a roof level, one carrying anything a storey holds (walls,
|
|
608
|
+
// rooms, items, cabinets, …) goes to the storey it covers, whole: the row in view was the roof's,
|
|
609
|
+
// the plan is the storey's, and a roof in it that bears there is seated back up.
|
|
610
|
+
const rootIdSet = new Set(payload.rootIds)
|
|
611
|
+
const targetLevel = payload.nodes.some(
|
|
612
|
+
(node) => rootIdSet.has(node.id as AnyNodeId) && isStoreyContent(node.type, node.metadata),
|
|
613
|
+
)
|
|
614
|
+
? ((scene.nodes[storeyOf(scene.nodes, aimedLevel.id) as AnyNodeId] as LevelNode | undefined) ??
|
|
615
|
+
aimedLevel)
|
|
616
|
+
: aimedLevel
|
|
485
617
|
const idMap = new Map<AnyNodeId, AnyNodeId>()
|
|
486
618
|
const materialIdMap = new Map<SceneMaterialId, SceneMaterialId>()
|
|
487
619
|
const materialsToCreate: SceneMaterial[] = []
|
|
488
620
|
|
|
489
621
|
for (const node of payload.nodes) {
|
|
622
|
+
if (isDerivedNode(node)) continue
|
|
490
623
|
idMap.set(node.id as AnyNodeId, generateId(extractIdPrefix(node.id)) as AnyNodeId)
|
|
491
624
|
}
|
|
492
625
|
for (const material of payload.materials) {
|
|
@@ -501,11 +634,14 @@ function applyClipboardPayloadToLevel(
|
|
|
501
634
|
materialsToCreate.push({ ...material, id: nextId })
|
|
502
635
|
}
|
|
503
636
|
|
|
504
|
-
const rootIdSet = new Set(payload.rootIds)
|
|
505
637
|
const pastedNodes: AnyNode[] = []
|
|
506
638
|
const skippedIds: AnyNodeId[] = []
|
|
507
639
|
|
|
508
640
|
for (const node of payload.nodes) {
|
|
641
|
+
if (isDerivedNode(node)) {
|
|
642
|
+
skippedIds.push(node.id as AnyNodeId)
|
|
643
|
+
continue
|
|
644
|
+
}
|
|
509
645
|
try {
|
|
510
646
|
pastedNodes.push(
|
|
511
647
|
remapNodeReferences(
|
|
@@ -524,33 +660,108 @@ function applyClipboardPayloadToLevel(
|
|
|
524
660
|
}
|
|
525
661
|
}
|
|
526
662
|
|
|
663
|
+
fitOpeningsToPastedWalls(pastedNodes, scene.nodes)
|
|
664
|
+
|
|
527
665
|
if (pastedNodes.length === 0) {
|
|
528
|
-
return {
|
|
666
|
+
return {
|
|
667
|
+
createdMaterialIds: [],
|
|
668
|
+
pastedIds: [],
|
|
669
|
+
refusedIds: [...refused],
|
|
670
|
+
refusal,
|
|
671
|
+
skippedIds,
|
|
672
|
+
}
|
|
529
673
|
}
|
|
530
674
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
pastedNodes.map((node) => ({
|
|
675
|
+
const sourceIds = new Map([...idMap].map(([source, copy]) => [copy, source]))
|
|
676
|
+
// A roof pasted onto the walls that carry it goes to the level above them, in the paste itself.
|
|
677
|
+
const { changes } = seatRoofs(scene.nodes, {
|
|
678
|
+
create: pastedNodes.map((node) => ({
|
|
536
679
|
node,
|
|
537
680
|
parentId: (node.parentId as AnyNodeId | null) ?? undefined,
|
|
681
|
+
// A copy is in its source's collections; an item brings them in its own `collectionIds`.
|
|
682
|
+
...(node.type !== 'item' && {
|
|
683
|
+
collectionIds: collectionIdsOf(scene.collections, sourceIds.get(node.id as AnyNodeId)!),
|
|
684
|
+
}),
|
|
538
685
|
})),
|
|
539
|
-
)
|
|
686
|
+
})
|
|
687
|
+
runAsSingleSceneHistoryStep(useScene, () => {
|
|
688
|
+
for (const material of materialsToCreate) scene.addSceneMaterial(material)
|
|
689
|
+
scene.applyNodeChanges(changes)
|
|
690
|
+
})
|
|
540
691
|
|
|
692
|
+
const nodes = useScene.getState().nodes
|
|
541
693
|
const pastedNodeIds = new Set(pastedNodes.map((node) => node.id as AnyNodeId))
|
|
542
694
|
const pastedRootIds = payload.rootIds
|
|
543
695
|
.map((rootId) => idMap.get(rootId))
|
|
544
696
|
.filter((id): id is AnyNodeId => !!id && pastedNodeIds.has(id))
|
|
697
|
+
const rootLevel = nodes[(nodes[pastedRootIds[0]!]?.parentId ?? '') as AnyNodeId]
|
|
545
698
|
|
|
546
699
|
useViewer.getState().setSelection({
|
|
547
|
-
levelId: targetLevel.id,
|
|
700
|
+
levelId: rootLevel?.type === 'level' ? rootLevel.id : targetLevel.id,
|
|
548
701
|
selectedIds: pastedRootIds,
|
|
549
702
|
})
|
|
550
703
|
|
|
551
704
|
return {
|
|
552
705
|
createdMaterialIds: materialsToCreate.map((material) => material.id as SceneMaterialId),
|
|
553
706
|
pastedIds: pastedRootIds,
|
|
707
|
+
refusedIds: [...refused],
|
|
708
|
+
refusal,
|
|
554
709
|
skippedIds,
|
|
555
710
|
}
|
|
556
711
|
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* A door or window pasted with its wall is held inside it the way the door and window tools place
|
|
715
|
+
* one: a wall pasted under a lower storey would otherwise leave a tall window poking above it.
|
|
716
|
+
*/
|
|
717
|
+
function fitOpeningsToPastedWalls(pasted: AnyNode[], sceneNodes: Record<AnyNodeId, AnyNode>) {
|
|
718
|
+
const byId = Object.fromEntries(pasted.map((node) => [node.id, node])) as Record<
|
|
719
|
+
AnyNodeId,
|
|
720
|
+
AnyNode
|
|
721
|
+
>
|
|
722
|
+
let nodes: Record<AnyNodeId, AnyNode> | undefined
|
|
723
|
+
pasted.forEach((node, index) => {
|
|
724
|
+
if (node.type !== 'door' && node.type !== 'window') return
|
|
725
|
+
const wall = node.parentId ? byId[node.parentId as AnyNodeId] : undefined
|
|
726
|
+
if (wall?.type !== 'wall') return
|
|
727
|
+
// A scripted opening is as tall as what its script built, as the wall cuts it.
|
|
728
|
+
const [width, height] = node.source
|
|
729
|
+
? scriptedSize(node.source.manifest)
|
|
730
|
+
: [node.width, node.height]
|
|
731
|
+
nodes ??= { ...sceneNodes, ...byId }
|
|
732
|
+
const [x, y, z] = node.position
|
|
733
|
+
const { clampedY } =
|
|
734
|
+
node.type === 'door'
|
|
735
|
+
? clampDoorToWall(wall, x, width, height)
|
|
736
|
+
: clampWindowToWall(wall, x, y, width, height, nodes)
|
|
737
|
+
if (clampedY !== y) pasted[index] = { ...node, position: [x, clampedY, z] }
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function withoutRefusedSubtrees(
|
|
742
|
+
payload: ClipboardPayload,
|
|
743
|
+
refused: ReadonlySet<AnyNodeId>,
|
|
744
|
+
): ClipboardPayload {
|
|
745
|
+
const children = new Map<string, AnyNodeId[]>()
|
|
746
|
+
for (const node of payload.nodes) {
|
|
747
|
+
if (!node.parentId) continue
|
|
748
|
+
const siblings = children.get(node.parentId) ?? []
|
|
749
|
+
siblings.push(node.id as AnyNodeId)
|
|
750
|
+
children.set(node.parentId, siblings)
|
|
751
|
+
}
|
|
752
|
+
const excluded = new Set(refused)
|
|
753
|
+
for (const id of excluded) {
|
|
754
|
+
for (const childId of children.get(id) ?? []) excluded.add(childId)
|
|
755
|
+
}
|
|
756
|
+
const nodes = payload.nodes.filter((node) => !excluded.has(node.id as AnyNodeId))
|
|
757
|
+
const kept = new Set(nodes.map((node) => node.id))
|
|
758
|
+
const materialIds = referencedSceneMaterialIds(nodes)
|
|
759
|
+
return {
|
|
760
|
+
...payload,
|
|
761
|
+
materials: payload.materials.filter((material) =>
|
|
762
|
+
materialIds.has(material.id as SceneMaterialId),
|
|
763
|
+
),
|
|
764
|
+
nodes,
|
|
765
|
+
rootIds: payload.rootIds.filter((id) => kept.has(id)),
|
|
766
|
+
}
|
|
767
|
+
}
|
|
@@ -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
|
+
}
|