@pascal-app/editor 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +221 -128
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +132 -0
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +82 -23
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +7 -19
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +191 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +87 -0
- package/src/lib/scene.ts +39 -45
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
beginSceneHistoryPauseSession,
|
|
5
|
+
type CloneNodesIntoResult,
|
|
4
6
|
cloneNodesInto,
|
|
7
|
+
collectionIdsOf,
|
|
5
8
|
collectSubtree,
|
|
9
|
+
createSceneApi,
|
|
6
10
|
type DuplicableConfig,
|
|
11
|
+
getSceneHistoryPauseDepth,
|
|
12
|
+
getSurfaceProvider,
|
|
7
13
|
nodeRegistry,
|
|
14
|
+
resolveSurfacePlacement,
|
|
15
|
+
runSceneHistoryDraftWrite,
|
|
16
|
+
type SurfaceRejectReason,
|
|
8
17
|
useScene,
|
|
9
18
|
} from '@pascal-app/core'
|
|
19
|
+
import { evaluateRecipe, isProceduralItem } from '@pascal-app/core/procedural-items'
|
|
20
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
21
|
+
import usePlacementPreview from '../store/use-placement-preview'
|
|
10
22
|
import { getPlacementMetadataRecord, stripPlacementMetadataFlags } from './placement-metadata'
|
|
11
23
|
import {
|
|
12
24
|
surfaceAttachmentId,
|
|
13
25
|
surfaceAttachmentUpdates,
|
|
26
|
+
surfaceFramePose,
|
|
14
27
|
updateSurfaceNode,
|
|
15
28
|
} from './surface-attachment'
|
|
16
29
|
|
|
@@ -26,13 +39,33 @@ function parentIdOf(node: AnyNode): AnyNodeId | undefined {
|
|
|
26
39
|
return parentId ?? undefined
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Create ops for a clone of scene nodes: each copy joins the collections its
|
|
44
|
+
* source is in, whatever its kind.
|
|
45
|
+
*/
|
|
46
|
+
export function copyCreateOps(cloned: CloneNodesIntoResult, parentId: AnyNodeId | undefined) {
|
|
47
|
+
const collections = useScene.getState().collections
|
|
48
|
+
const sourceIds = new Map([...cloned.idMap].map(([source, copy]) => [copy, source]))
|
|
49
|
+
return cloned.nodes.map((node, index) => ({
|
|
50
|
+
node,
|
|
51
|
+
...(index === 0 && parentId ? { parentId } : {}),
|
|
52
|
+
collectionIds: collectionIdsOf(collections, sourceIds.get(node.id as AnyNodeId)!),
|
|
53
|
+
}))
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
function duplicableConfigFor(node: AnyNode): DuplicableConfig | null {
|
|
30
57
|
const duplicable = nodeRegistry.get(node.type)?.capabilities?.duplicable
|
|
31
58
|
return duplicable && typeof duplicable === 'object' ? duplicable : null
|
|
32
59
|
}
|
|
33
60
|
|
|
34
61
|
export function duplicatesAsFreshSubtree(node: AnyNode): boolean {
|
|
35
|
-
|
|
62
|
+
// A surface-local root needs the same attachment lifecycle even without descendants.
|
|
63
|
+
if (surfaceAttachmentId(node) !== null) return true
|
|
64
|
+
const policy = duplicableConfigFor(node)?.subtree
|
|
65
|
+
return (
|
|
66
|
+
policy === true ||
|
|
67
|
+
(policy === 'with-children' && 'children' in node && node.children.length > 0)
|
|
68
|
+
)
|
|
36
69
|
}
|
|
37
70
|
|
|
38
71
|
/**
|
|
@@ -99,57 +132,179 @@ export function createFreshPlacementSubtree(
|
|
|
99
132
|
parentId,
|
|
100
133
|
})
|
|
101
134
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.
|
|
105
|
-
|
|
106
|
-
)
|
|
135
|
+
scene.applyNodeChanges({
|
|
136
|
+
create: copyCreateOps(cloned, parentId),
|
|
137
|
+
update: surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceAttachmentId(subtree.root)),
|
|
138
|
+
})
|
|
107
139
|
|
|
140
|
+
const created = useScene.getState().nodes[cloned.rootId]
|
|
141
|
+
if (!created) return null
|
|
142
|
+
useInteractionScope.getState().noteSubtreeCreation(created)
|
|
108
143
|
return cloned.rootId
|
|
109
144
|
}
|
|
110
145
|
|
|
146
|
+
export function discardFreshPlacementSubtree(rootId: AnyNodeId): void {
|
|
147
|
+
const scene = useScene.getState()
|
|
148
|
+
if (!scene.nodes[rootId]) return
|
|
149
|
+
scene.applyNodeChanges({
|
|
150
|
+
delete: [rootId],
|
|
151
|
+
update: surfaceAttachmentUpdates(rootId, null, null),
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function namedSurfaceRejection(
|
|
156
|
+
root: AnyNode,
|
|
157
|
+
surfaceId: string | null,
|
|
158
|
+
): SurfaceRejectReason | null {
|
|
159
|
+
if (surfaceId === null) return null
|
|
160
|
+
const scene = createSceneApi(useScene)
|
|
161
|
+
const host = root.parentId ? scene.get(root.parentId as AnyNodeId) : undefined
|
|
162
|
+
if (!host || !('position' in root) || !Array.isArray(root.rotation)) return 'no-surface'
|
|
163
|
+
const surface = getSurfaceProvider(host)
|
|
164
|
+
.surfaces?.(host, { scene })
|
|
165
|
+
.find((s) => s.id === surfaceId)
|
|
166
|
+
if (!surface) return 'no-surface'
|
|
167
|
+
const pose = surfaceFramePose(
|
|
168
|
+
root.parentId,
|
|
169
|
+
surfaceId,
|
|
170
|
+
root as {
|
|
171
|
+
position: [number, number, number]
|
|
172
|
+
rotation: [number, number, number]
|
|
173
|
+
},
|
|
174
|
+
false,
|
|
175
|
+
)
|
|
176
|
+
const capabilities = nodeRegistry.get(root.type)?.capabilities
|
|
177
|
+
const evaluated = isProceduralItem(root) ? evaluateRecipe(root.recipe, root.parameters) : null
|
|
178
|
+
const bounds =
|
|
179
|
+
capabilities?.dragBounds?.(root, scene.nodes()) ??
|
|
180
|
+
(evaluated
|
|
181
|
+
? {
|
|
182
|
+
size: evaluated.dimensions,
|
|
183
|
+
center: evaluated.min.map((v, i) => (v + evaluated.max[i]!) / 2) as [
|
|
184
|
+
number,
|
|
185
|
+
number,
|
|
186
|
+
number,
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
: undefined)
|
|
190
|
+
const size = bounds?.size ??
|
|
191
|
+
capabilities?.floorPlaced?.footprint?.(root, { nodes: scene.nodes() }).dimensions ?? [0, 0, 0]
|
|
192
|
+
const center = bounds?.center
|
|
193
|
+
const localBounds = center
|
|
194
|
+
? {
|
|
195
|
+
min: center.map((value, axis) => value - size[axis]! / 2) as [number, number, number],
|
|
196
|
+
max: center.map((value, axis) => value + size[axis]! / 2) as [number, number, number],
|
|
197
|
+
}
|
|
198
|
+
: undefined
|
|
199
|
+
let reason: SurfaceRejectReason | null = null
|
|
200
|
+
const placement = resolveSurfacePlacement({
|
|
201
|
+
host,
|
|
202
|
+
surface,
|
|
203
|
+
childKind: root.type,
|
|
204
|
+
childId: root.id,
|
|
205
|
+
childFootprint: {
|
|
206
|
+
size,
|
|
207
|
+
rotationY: pose.rotation[1],
|
|
208
|
+
rotation: pose.rotation,
|
|
209
|
+
localBounds,
|
|
210
|
+
},
|
|
211
|
+
hit: { point: pose.position, normalWorldY: 1 },
|
|
212
|
+
origin: pose.position,
|
|
213
|
+
scene,
|
|
214
|
+
onReject: (value) => {
|
|
215
|
+
reason = value
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
if (!placement) return reason ?? 'no-surface'
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Runs a placement's one committing write as a history step. The moving node's gesture
|
|
224
|
+
* sessions (the 2D move overlay's) are lifted for it; any other owner's pause still holds, so
|
|
225
|
+
* the write then records nothing. A legacy caller's raw `temporal.pause()` is kept afterwards.
|
|
226
|
+
*/
|
|
227
|
+
function recordPlacementStep(rootId: AnyNodeId, wasTracking: boolean, write: () => void): void {
|
|
228
|
+
const step = beginSceneHistoryPauseSession(useScene, { gesture: rootId })
|
|
229
|
+
try {
|
|
230
|
+
step.commitStep(write)
|
|
231
|
+
} finally {
|
|
232
|
+
step.end()
|
|
233
|
+
if (!wasTracking && getSceneHistoryPauseDepth() === 0) useScene.temporal.getState().pause()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
111
237
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* Fresh drafts already exist in the scene so renderers and move tools can
|
|
115
|
-
* preview real geometry. On commit we delete that draft while history is
|
|
116
|
-
* paused, then create a clean clone at the final cursor position with history
|
|
117
|
-
* resumed. Undo therefore removes the placed node instead of resurrecting the
|
|
118
|
-
* hidden draft at its origin.
|
|
238
|
+
* Replace the draft in one validated write. History already excludes fresh
|
|
239
|
+
* subtrees, so this records one creation without first deleting the preview.
|
|
119
240
|
*/
|
|
120
241
|
export function commitFreshPlacementSubtree(
|
|
121
242
|
rootId: AnyNodeId,
|
|
122
243
|
rootPatch: Partial<AnyNode>,
|
|
244
|
+
onReject?: (reason: SurfaceRejectReason) => void,
|
|
123
245
|
): AnyNodeId | null {
|
|
124
246
|
const scene = useScene.getState()
|
|
125
247
|
const subtree = collectSubtree(scene.nodes, rootId)
|
|
126
|
-
if (!subtree) return null
|
|
248
|
+
if (!subtree || scene.readOnly) return null
|
|
127
249
|
|
|
128
|
-
const root = cleanPlacementMetadata({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
250
|
+
const root = cleanPlacementMetadata({ ...subtree.root, ...rootPatch } as AnyNode)
|
|
251
|
+
const surfaceId =
|
|
252
|
+
root.parentId === subtree.root.parentId ? surfaceAttachmentId(subtree.root) : null
|
|
253
|
+
const rejection = namedSurfaceRejection(root, surfaceId)
|
|
254
|
+
if (rejection) {
|
|
255
|
+
onReject?.(rejection)
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
132
258
|
const descendants = subtree.descendants.map((node) => cleanPlacementMetadata(node))
|
|
133
259
|
const parentId = parentIdOf(root)
|
|
134
|
-
const cloned = cloneNodesInto([root, ...descendants], {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
260
|
+
const cloned = cloneNodesInto([root, ...descendants], { rootId, parentId })
|
|
261
|
+
// The drafts' memberships, read before they are deleted.
|
|
262
|
+
const create = copyCreateOps(cloned, parentId)
|
|
263
|
+
const updates = surfaceAttachmentUpdates(rootId, null, null)
|
|
264
|
+
for (const update of surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceId)) {
|
|
265
|
+
const attachments = { ...(update.data as { attachments: Record<string, string> }).attachments }
|
|
266
|
+
delete attachments[rootId]
|
|
267
|
+
const previous = updates.findIndex((entry) => entry.id === update.id)
|
|
268
|
+
const merged = { id: update.id, data: { attachments } }
|
|
269
|
+
if (previous === -1) updates.push(merged)
|
|
270
|
+
else updates[previous] = merged
|
|
271
|
+
}
|
|
138
272
|
|
|
139
|
-
const
|
|
273
|
+
const scope = useInteractionScope.getState()
|
|
274
|
+
const factoryCreated =
|
|
275
|
+
scope.ownedSubtree?.creation.rootId === rootId || scope.pendingSubtree?.rootId === rootId
|
|
140
276
|
const temporal = useScene.temporal.getState()
|
|
141
|
-
const wasTracking =
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
277
|
+
const wasTracking = temporal.isTracking
|
|
278
|
+
if (!factoryCreated && descendants.length === 0 && surfaceId === null) {
|
|
279
|
+
// Preserve the established root-only lifecycle for placements outside the subtree factory.
|
|
280
|
+
runSceneHistoryDraftWrite(() => {
|
|
281
|
+
updateSurfaceNode(rootId, {}, null)
|
|
282
|
+
useScene.getState().deleteNode(rootId)
|
|
283
|
+
})
|
|
284
|
+
recordPlacementStep(rootId, wasTracking, () =>
|
|
285
|
+
useScene.getState().applyNodeChanges({ create, update: updates }),
|
|
286
|
+
)
|
|
287
|
+
} else {
|
|
288
|
+
recordPlacementStep(rootId, wasTracking, () => {
|
|
289
|
+
try {
|
|
290
|
+
// applyNodeChanges validates the complete proposed graph before publishing any part of it.
|
|
291
|
+
scene.applyNodeChanges({ delete: [rootId], create, update: updates })
|
|
292
|
+
for (const node of [subtree.root, ...subtree.descendants]) scene.clearDirty(node.id)
|
|
293
|
+
} catch (error) {
|
|
294
|
+
// Zustand publishes before notifying subscribers. A subscriber error must restore
|
|
295
|
+
// the draft and its ownership/history, never masquerade as a placement refusal.
|
|
296
|
+
useScene.temporal.getState().pause()
|
|
297
|
+
try {
|
|
298
|
+
if (useScene.getState() !== scene) useScene.setState(scene, true)
|
|
299
|
+
} finally {
|
|
300
|
+
useInteractionScope.setState(scope)
|
|
301
|
+
useScene.temporal.setState(temporal)
|
|
302
|
+
}
|
|
303
|
+
throw error
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
useInteractionScope.getState().finishSubtree(rootId)
|
|
308
|
+
if (usePlacementPreview.getState().node?.id === rootId) usePlacementPreview.getState().clear()
|
|
154
309
|
return cloned.rootId
|
|
155
310
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
type CompiledGeometryScript,
|
|
4
|
+
GEOMETRY_SCRIPT_MIME_TYPES,
|
|
5
|
+
GeometryArtifactMetadata,
|
|
6
|
+
GeometryReuseFields,
|
|
7
|
+
type GeometryScriptLanguage,
|
|
8
|
+
type GeometryScriptParamValue,
|
|
9
|
+
generateId,
|
|
10
|
+
getArtifactStore,
|
|
11
|
+
isScriptedNode,
|
|
12
|
+
runAsSingleSceneHistoryStep,
|
|
13
|
+
scriptedObjectMeta,
|
|
14
|
+
useScene,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
import {
|
|
17
|
+
addColumn,
|
|
18
|
+
addObject,
|
|
19
|
+
authoredObject,
|
|
20
|
+
editedScriptLanguage,
|
|
21
|
+
editedScriptParams,
|
|
22
|
+
rescriptOpening,
|
|
23
|
+
} from '@pascal-app/core/agent-operations'
|
|
24
|
+
import { compileGeometryScriptInWorker } from './client'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds a bpy module and stores its GLB and text, returning their hashes.
|
|
28
|
+
* Blender runs outside the browser, so the host provides this (the hosted app
|
|
29
|
+
* calls its Blender service); without one, bpy scripts are refused.
|
|
30
|
+
*/
|
|
31
|
+
export type BlenderScriptBuilder = (input: {
|
|
32
|
+
code: string
|
|
33
|
+
params?: Record<string, GeometryScriptParamValue>
|
|
34
|
+
nodeId: string
|
|
35
|
+
metadata: GeometryArtifactMetadata
|
|
36
|
+
}) => Promise<CompiledGeometryScript>
|
|
37
|
+
|
|
38
|
+
let blenderBuilder: BlenderScriptBuilder | null = null
|
|
39
|
+
|
|
40
|
+
export function configureBlenderScriptBuilder(next: BlenderScriptBuilder | null): void {
|
|
41
|
+
blenderBuilder = next
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The editor's compile step for `add_object`: runs the module (a three.js one
|
|
46
|
+
* in the worker, a bpy one through the host's Blender builder) and stores the
|
|
47
|
+
* GLB and the module text, so the core operation can reference both by hash.
|
|
48
|
+
* Without `code`, the node's stored script is rebuilt; either way an edit keeps
|
|
49
|
+
* the node's param values it does not override.
|
|
50
|
+
*/
|
|
51
|
+
export async function compileAndStoreGeometryScript(input: {
|
|
52
|
+
code?: string
|
|
53
|
+
language?: GeometryScriptLanguage
|
|
54
|
+
nodeId?: string
|
|
55
|
+
params?: Record<string, GeometryScriptParamValue>
|
|
56
|
+
kind?: 'item' | 'door' | 'window' | 'column'
|
|
57
|
+
name?: string
|
|
58
|
+
description?: string
|
|
59
|
+
category?: string
|
|
60
|
+
tags?: string[]
|
|
61
|
+
provenance?: GeometryArtifactMetadata
|
|
62
|
+
}): Promise<CompiledGeometryScript> {
|
|
63
|
+
const code = input.code ?? (await storedScript(input.nodeId))
|
|
64
|
+
const node = input.nodeId ? useScene.getState().nodes[input.nodeId as AnyNodeId] : undefined
|
|
65
|
+
const language = editedScriptLanguage(node, input)
|
|
66
|
+
const params = editedScriptParams(node, input.params)
|
|
67
|
+
const kind =
|
|
68
|
+
input.kind ??
|
|
69
|
+
(node?.type === 'door' || node?.type === 'window' || node?.type === 'column'
|
|
70
|
+
? node.type
|
|
71
|
+
: 'item')
|
|
72
|
+
const nodeId = input.nodeId ?? generateId(kind)
|
|
73
|
+
const metadata = GeometryArtifactMetadata.parse({
|
|
74
|
+
...(isScriptedNode(node) ? scriptedObjectMeta(node) : {}),
|
|
75
|
+
...input.provenance,
|
|
76
|
+
...GeometryReuseFields.parse(input),
|
|
77
|
+
kind,
|
|
78
|
+
})
|
|
79
|
+
if (language === 'bpy') {
|
|
80
|
+
if (!blenderBuilder) throw new Error('This editor cannot run Blender (bpy) scripts')
|
|
81
|
+
return { ...(await blenderBuilder({ code, params, nodeId, metadata })), nodeId }
|
|
82
|
+
}
|
|
83
|
+
const { glb, ...compiled } = await compileGeometryScriptInWorker({ code, params })
|
|
84
|
+
const context = { nodeId, metadata: { ...metadata, mount: compiled.mount } }
|
|
85
|
+
const store = getArtifactStore()
|
|
86
|
+
const [sha256] = await Promise.all([
|
|
87
|
+
store.put(compiled.sha256, glb, 'model/gltf-binary', context),
|
|
88
|
+
store.put(
|
|
89
|
+
compiled.script,
|
|
90
|
+
new TextEncoder().encode(code),
|
|
91
|
+
GEOMETRY_SCRIPT_MIME_TYPES.three,
|
|
92
|
+
context,
|
|
93
|
+
),
|
|
94
|
+
])
|
|
95
|
+
return { ...compiled, sha256, nodeId }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The module text of an authored object, read back from the artifact store. */
|
|
99
|
+
export async function storedScript(nodeId: string | undefined): Promise<string> {
|
|
100
|
+
if (!nodeId) throw new Error('Pass the code to build a new object')
|
|
101
|
+
const node = authoredObject(useScene.getState().nodes, nodeId)
|
|
102
|
+
const code = await getArtifactStore().text(node.source.script)
|
|
103
|
+
if (code === null) throw new Error(`The script of ${nodeId} could not be read`)
|
|
104
|
+
return code
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const rebuildGeneration = new Map<string, number>()
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Re-runs a scripted node's script with new param values, the inspector's
|
|
111
|
+
* and resize handles' path: same code, new artifact, one undo step. A slower earlier rebuild of
|
|
112
|
+
* the same node never overwrites a newer one.
|
|
113
|
+
*/
|
|
114
|
+
export async function rebuildAuthoredObject(
|
|
115
|
+
nodeId: string,
|
|
116
|
+
params: Record<string, GeometryScriptParamValue>,
|
|
117
|
+
/** Where it ends up, when a resize also moves it (a side arrow keeps the opposite edge). */
|
|
118
|
+
position?: [number, number, number],
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const generation = (rebuildGeneration.get(nodeId) ?? 0) + 1
|
|
121
|
+
rebuildGeneration.set(nodeId, generation)
|
|
122
|
+
const compiled = await compileAndStoreGeometryScript({ nodeId, params })
|
|
123
|
+
if (rebuildGeneration.get(nodeId) !== generation) return
|
|
124
|
+
const changes = rebuildChanges(nodeId, compiled, params, position)
|
|
125
|
+
runAsSingleSceneHistoryStep(useScene, () => {
|
|
126
|
+
for (const { id, data } of changes?.update ?? []) {
|
|
127
|
+
useScene.getState().updateNode(id as AnyNodeId, data)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** What a rebuild changes, through the operation of the node's kind. */
|
|
133
|
+
function rebuildChanges(
|
|
134
|
+
nodeId: string,
|
|
135
|
+
compiled: CompiledGeometryScript,
|
|
136
|
+
params: Record<string, GeometryScriptParamValue>,
|
|
137
|
+
position: [number, number, number] | undefined,
|
|
138
|
+
) {
|
|
139
|
+
const nodes = useScene.getState().nodes
|
|
140
|
+
const context = { activeLevelId: null }
|
|
141
|
+
switch (nodes[nodeId as AnyNodeId]?.type) {
|
|
142
|
+
case 'column': {
|
|
143
|
+
const at = position && { x: position[0], y: position[1], z: position[2] }
|
|
144
|
+
return addColumn(nodes, { nodeId, compiled, ...at }, context).changes
|
|
145
|
+
}
|
|
146
|
+
case 'window':
|
|
147
|
+
case 'door':
|
|
148
|
+
return rescriptOpening(nodes, { nodeId, compiled, position }, context).changes
|
|
149
|
+
default:
|
|
150
|
+
return addObject(nodes, { params, nodeId, compiled, position }, context).changes
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { GeometryScriptParamValue } from '@pascal-app/core'
|
|
2
|
+
import type { GeometryScriptCompileOutput } from '@pascal-app/geometry-script'
|
|
3
|
+
import type { GeometryScriptWorkerRequest, GeometryScriptWorkerResponse } from './protocol'
|
|
4
|
+
|
|
5
|
+
const COMPILE_TIMEOUT_MS = 20_000
|
|
6
|
+
const START_TIMEOUT_MS = 15_000
|
|
7
|
+
|
|
8
|
+
// Model-written code runs in a worker inside a sandboxed frame: an opaque
|
|
9
|
+
// origin (no cookies, storage or access to the editor) whose CSP allows only
|
|
10
|
+
// its own scripts and blob workers, so the code cannot reach the network,
|
|
11
|
+
// load fonts or images, or open frames. The worker inherits that policy.
|
|
12
|
+
const FRAME_CSP =
|
|
13
|
+
"default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' blob:; worker-src blob:"
|
|
14
|
+
const FRAME_HTML = `<!doctype html><meta http-equiv="Content-Security-Policy" content="${FRAME_CSP}"><script>
|
|
15
|
+
let worker
|
|
16
|
+
addEventListener('message', (event) => {
|
|
17
|
+
if (event.source !== parent) return
|
|
18
|
+
const data = event.data
|
|
19
|
+
if (data && data.type === 'start') {
|
|
20
|
+
worker = new Worker(URL.createObjectURL(new Blob([data.source], { type: 'text/javascript' })))
|
|
21
|
+
worker.onmessage = (e) => parent.postMessage(e.data, '*', e.data && e.data.output ? [e.data.output.glb] : [])
|
|
22
|
+
worker.onerror = (e) => parent.postMessage({ crashed: e.message || 'The compile worker crashed' }, '*')
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
if (worker) worker.postMessage(data)
|
|
26
|
+
})
|
|
27
|
+
parent.postMessage({ frame: true }, '*')
|
|
28
|
+
</script>`
|
|
29
|
+
|
|
30
|
+
type Pending = {
|
|
31
|
+
code: string
|
|
32
|
+
resolve: (output: GeometryScriptCompileOutput) => void
|
|
33
|
+
reject: (error: Error) => void
|
|
34
|
+
timeout: ReturnType<typeof setTimeout>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type FrameMessage =
|
|
38
|
+
| { frame: true }
|
|
39
|
+
| { ready: true }
|
|
40
|
+
| { crashed: string }
|
|
41
|
+
| GeometryScriptWorkerResponse
|
|
42
|
+
|
|
43
|
+
let frame: HTMLIFrameElement | null = null
|
|
44
|
+
let started: Promise<Window> | null = null
|
|
45
|
+
let nextId = 1
|
|
46
|
+
const pending = new Map<number, Pending>()
|
|
47
|
+
let onFrameMessage: ((message: FrameMessage) => void) | null = null
|
|
48
|
+
|
|
49
|
+
function reset(error: Error) {
|
|
50
|
+
frame?.remove()
|
|
51
|
+
frame = null
|
|
52
|
+
started = null
|
|
53
|
+
for (const entry of pending.values()) {
|
|
54
|
+
clearTimeout(entry.timeout)
|
|
55
|
+
entry.reject(error)
|
|
56
|
+
}
|
|
57
|
+
pending.clear()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof window !== 'undefined') {
|
|
61
|
+
window.addEventListener('message', (event: MessageEvent<FrameMessage>) => {
|
|
62
|
+
if (!frame || event.source !== frame.contentWindow) return
|
|
63
|
+
const message = event.data
|
|
64
|
+
if ('crashed' in message) return reset(new Error(message.crashed))
|
|
65
|
+
if ('id' in message) {
|
|
66
|
+
const entry = pending.get(message.id)
|
|
67
|
+
if (!entry) return
|
|
68
|
+
pending.delete(message.id)
|
|
69
|
+
clearTimeout(entry.timeout)
|
|
70
|
+
if (message.ok) withOwnHashes(message.output, entry.code).then(entry.resolve, entry.reject)
|
|
71
|
+
else entry.reject(new Error(message.error))
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
onFrameMessage?.(message)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function sha256(bytes: ArrayBuffer | Uint8Array): Promise<string> {
|
|
79
|
+
const hash = await crypto.subtle.digest('SHA-256', bytes as BufferSource)
|
|
80
|
+
return Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The artifact and script hashes and the language, set here: nothing the sandboxed code reports about itself is trusted. */
|
|
84
|
+
async function withOwnHashes(
|
|
85
|
+
output: GeometryScriptCompileOutput,
|
|
86
|
+
code: string,
|
|
87
|
+
): Promise<GeometryScriptCompileOutput> {
|
|
88
|
+
const [artifact, script] = await Promise.all([
|
|
89
|
+
sha256(output.glb),
|
|
90
|
+
sha256(new TextEncoder().encode(code)),
|
|
91
|
+
])
|
|
92
|
+
return { ...output, sha256: artifact, script, language: 'three' }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Starts the sandbox frame and its compile worker once; a crash or timeout starts a fresh one. */
|
|
96
|
+
function startSandbox(): Promise<Window> {
|
|
97
|
+
if (started) return started
|
|
98
|
+
started = (async () => {
|
|
99
|
+
if (typeof document === 'undefined') throw new Error('Geometry scripts compile in a browser')
|
|
100
|
+
const { sandboxWorkerSource } = await import('@pascal-app/geometry-script/sandbox')
|
|
101
|
+
const iframe = document.createElement('iframe')
|
|
102
|
+
iframe.sandbox.add('allow-scripts')
|
|
103
|
+
iframe.setAttribute('aria-hidden', 'true')
|
|
104
|
+
iframe.tabIndex = -1
|
|
105
|
+
iframe.style.display = 'none'
|
|
106
|
+
iframe.srcdoc = FRAME_HTML
|
|
107
|
+
frame = iframe
|
|
108
|
+
const ready = new Promise<Window>((resolve, reject) => {
|
|
109
|
+
const timer = setTimeout(
|
|
110
|
+
() => reject(new Error('The script sandbox did not start')),
|
|
111
|
+
START_TIMEOUT_MS,
|
|
112
|
+
)
|
|
113
|
+
onFrameMessage = (message) => {
|
|
114
|
+
const target = iframe.contentWindow
|
|
115
|
+
if (!target) return
|
|
116
|
+
if ('frame' in message)
|
|
117
|
+
target.postMessage({ type: 'start', source: sandboxWorkerSource }, '*')
|
|
118
|
+
if ('ready' in message) {
|
|
119
|
+
clearTimeout(timer)
|
|
120
|
+
onFrameMessage = null
|
|
121
|
+
resolve(target)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
document.body.appendChild(iframe)
|
|
126
|
+
return ready
|
|
127
|
+
})()
|
|
128
|
+
started.catch((error: unknown) =>
|
|
129
|
+
reset(error instanceof Error ? error : new Error(String(error))),
|
|
130
|
+
)
|
|
131
|
+
return started
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Runs a geometry script in the sandbox and returns its GLB + manifest. A
|
|
136
|
+
* script that hangs is stopped with its sandbox; the next compile starts fresh.
|
|
137
|
+
*/
|
|
138
|
+
export async function compileGeometryScriptInWorker(input: {
|
|
139
|
+
code: string
|
|
140
|
+
params?: Record<string, GeometryScriptParamValue>
|
|
141
|
+
}): Promise<GeometryScriptCompileOutput> {
|
|
142
|
+
const target = await startSandbox()
|
|
143
|
+
const id = nextId++
|
|
144
|
+
const request: GeometryScriptWorkerRequest = { id, code: input.code, params: input.params }
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const timeout = setTimeout(() => {
|
|
147
|
+
reset(new Error(`The script ran longer than ${COMPILE_TIMEOUT_MS / 1000} s and was stopped`))
|
|
148
|
+
}, COMPILE_TIMEOUT_MS)
|
|
149
|
+
pending.set(id, { code: input.code, resolve, reject, timeout })
|
|
150
|
+
target.postMessage(request, '*')
|
|
151
|
+
})
|
|
152
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GeometryScriptParamValue } from '@pascal-app/core'
|
|
2
|
+
import type { GeometryScriptCompileOutput } from '@pascal-app/geometry-script'
|
|
3
|
+
|
|
4
|
+
export type GeometryScriptWorkerRequest = {
|
|
5
|
+
id: number
|
|
6
|
+
code: string
|
|
7
|
+
params?: Record<string, GeometryScriptParamValue>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type GeometryScriptWorkerResponse =
|
|
11
|
+
| { id: number; ok: true; output: GeometryScriptCompileOutput }
|
|
12
|
+
| { id: number; ok: false; error: string }
|