@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
package/src/lib/glb-export.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
findLevelAncestorId,
|
|
8
8
|
type GeometryContext,
|
|
9
9
|
getLevelDisplayName,
|
|
10
|
+
hidesDescendants,
|
|
10
11
|
isNodeKindEnabled,
|
|
11
12
|
isOperationDoorType,
|
|
12
13
|
itemClipRegistry,
|
|
@@ -18,7 +19,9 @@ import {
|
|
|
18
19
|
type WindowNode,
|
|
19
20
|
type ZoneNode,
|
|
20
21
|
} from '@pascal-app/core'
|
|
22
|
+
import { evaluateRecipe } from '@pascal-app/core/procedural-items'
|
|
21
23
|
import {
|
|
24
|
+
decorateProceduralEmission,
|
|
22
25
|
getPascalTextureRef,
|
|
23
26
|
isViewerPresentationTextureBorrowed,
|
|
24
27
|
poseDoorMovingParts,
|
|
@@ -37,6 +40,8 @@ import {
|
|
|
37
40
|
} from 'three/examples/jsm/exporters/GLTFExporter.js'
|
|
38
41
|
import { createExportTextureUtils, type ExportTextureUtils } from './export-texture-utils'
|
|
39
42
|
import { cloneExportUserData } from './export-user-data'
|
|
43
|
+
import { isIfcRolePart } from './ifc-parts'
|
|
44
|
+
import { nodeDisplayLabel } from './node-display-label'
|
|
40
45
|
import {
|
|
41
46
|
type CompressedTextureDecompressor,
|
|
42
47
|
decompressCanonicalNormalMaps,
|
|
@@ -140,6 +145,115 @@ export function nextFrames(): Promise<void> {
|
|
|
140
145
|
})
|
|
141
146
|
}
|
|
142
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Export visibility, shared with the clone pruning: a hidden node hides its
|
|
150
|
+
* subtree, except a Site, which hides only its own ground (`hidesDescendants`).
|
|
151
|
+
*/
|
|
152
|
+
export function createExportVisibility(nodes: Record<string, AnyNode>) {
|
|
153
|
+
const visibility = new Map<string, boolean>()
|
|
154
|
+
const isVisible = (id: string, path: Set<string> = new Set()): boolean => {
|
|
155
|
+
const cached = visibility.get(id)
|
|
156
|
+
if (cached !== undefined) return cached
|
|
157
|
+
const node = nodes[id]
|
|
158
|
+
if (!node) return true
|
|
159
|
+
if (node.visible === false) {
|
|
160
|
+
visibility.set(id, false)
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
const parentId = node.parentId
|
|
164
|
+
const parent = parentId ? nodes[parentId] : undefined
|
|
165
|
+
if (!parentId || path.has(id) || (parent && !hidesDescendants(parent))) {
|
|
166
|
+
visibility.set(id, true)
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
path.add(id)
|
|
170
|
+
const visible = isVisible(parentId, path)
|
|
171
|
+
path.delete(id)
|
|
172
|
+
visibility.set(id, visible)
|
|
173
|
+
return visible
|
|
174
|
+
}
|
|
175
|
+
return isVisible
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type ExportGeometryScope = Pick<GlbExportOptions, 'onlyVisible' | 'excludedNodeTypes'>
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Included `bake: 'replace'` nodes whose live object holds no mesh yet. These
|
|
182
|
+
* kinds render collectively in the editor and mount their own geometry only
|
|
183
|
+
* while `isExporting` is set; kinds with bake hooks are rebuilt after the
|
|
184
|
+
* clone instead, and excluded or hidden nodes never reach the clone.
|
|
185
|
+
*/
|
|
186
|
+
export function nodesAwaitingExportGeometry(
|
|
187
|
+
nodes: Record<string, AnyNode>,
|
|
188
|
+
scope: ExportGeometryScope = {},
|
|
189
|
+
): string[] {
|
|
190
|
+
const sceneState = useScene.getState()
|
|
191
|
+
const installedPlugins = sceneState.hasExplicitPluginInstallState
|
|
192
|
+
? sceneState.installedPlugins
|
|
193
|
+
: undefined
|
|
194
|
+
const excluded = new Set(scope.excludedNodeTypes)
|
|
195
|
+
const isVisible = createExportVisibility(nodes)
|
|
196
|
+
const typeExcluded = (node: AnyNode): boolean => {
|
|
197
|
+
for (let current: AnyNode | undefined = node, guard = 0; current && guard < 64; guard++) {
|
|
198
|
+
if (excluded.has(current.type)) return true
|
|
199
|
+
current = current.parentId ? nodes[current.parentId] : undefined
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
const pending: string[] = []
|
|
204
|
+
for (const [id, object] of sceneRegistry.nodes) {
|
|
205
|
+
const node = nodes[id]
|
|
206
|
+
if (!node) continue
|
|
207
|
+
const definition = nodeRegistry.get(node.type)
|
|
208
|
+
if (definition?.bake !== 'replace' || definition.bakeGeometry || definition.bakeGeometryAsync) {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
if (!isNodeKindEnabled(node.type, installedPlugins) || typeExcluded(node)) continue
|
|
212
|
+
if ((scope.onlyVisible ?? true) && !isVisible(id)) continue
|
|
213
|
+
let hasMesh = false
|
|
214
|
+
object.traverse((child) => {
|
|
215
|
+
if ((child as THREE.Mesh).isMesh) hasMesh = true
|
|
216
|
+
})
|
|
217
|
+
if (!hasMesh) pending.push(id)
|
|
218
|
+
}
|
|
219
|
+
return pending
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Two animation frames, or `ms` if frames have stopped (hidden tab, test runner). */
|
|
223
|
+
function nextFramesOrTimeout(ms: number): Promise<void> {
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
const timer = setTimeout(resolve, ms)
|
|
226
|
+
if (typeof requestAnimationFrame !== 'function') return
|
|
227
|
+
requestAnimationFrame(() =>
|
|
228
|
+
requestAnimationFrame(() => {
|
|
229
|
+
clearTimeout(timer)
|
|
230
|
+
resolve()
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* After `setExporting(true)`, let one commit land, then wait until every
|
|
238
|
+
* included replace-kind node has mounted its export geometry. Two frames are
|
|
239
|
+
* not enough: on a large scene with a busy main thread the proxies' re-render
|
|
240
|
+
* lands several frames later, and the clone would silently miss every plant.
|
|
241
|
+
* The deadline runs on its own timer, so stalled frames cannot hang an export.
|
|
242
|
+
*/
|
|
243
|
+
export async function waitForExportGeometry(
|
|
244
|
+
nodes: Record<string, AnyNode>,
|
|
245
|
+
scope: ExportGeometryScope = {},
|
|
246
|
+
timeoutMs = 10_000,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
const deadline = Date.now() + timeoutMs
|
|
249
|
+
await nextFramesOrTimeout(Math.min(500, timeoutMs))
|
|
250
|
+
while (nodesAwaitingExportGeometry(nodes, scope).length > 0) {
|
|
251
|
+
const remaining = deadline - Date.now()
|
|
252
|
+
if (remaining <= 0) return
|
|
253
|
+
await nextFramesOrTimeout(Math.min(50, remaining))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
143
257
|
type GltfExtrasDef = {
|
|
144
258
|
extras?: Record<string, unknown>
|
|
145
259
|
}
|
|
@@ -368,7 +482,10 @@ function startSceneExportPreparation(
|
|
|
368
482
|
|
|
369
483
|
for (const [id, original] of registryEntries) {
|
|
370
484
|
const node = nodes[id]
|
|
371
|
-
if (!node)
|
|
485
|
+
if (!node) {
|
|
486
|
+
excludedObjects.add(original)
|
|
487
|
+
continue
|
|
488
|
+
}
|
|
372
489
|
const installedPlugins = sceneState.hasExplicitPluginInstallState
|
|
373
490
|
? sceneState.installedPlugins
|
|
374
491
|
: undefined
|
|
@@ -481,10 +598,18 @@ function finishSceneExportPreparation(preparation: SceneExportPreparation): GlbE
|
|
|
481
598
|
if (clone) identityNodes.add(clone)
|
|
482
599
|
}
|
|
483
600
|
|
|
484
|
-
pruneNonRenderableMeshes(scene, identityNodes)
|
|
601
|
+
const warnings = pruneNonRenderableMeshes(scene, identityNodes)
|
|
485
602
|
sanitizeMaterialGroups(scene, identityNodes)
|
|
486
603
|
convertMaterials(scene, options.textures ?? 'embed', options.purpose ?? 'viewer')
|
|
487
604
|
|
|
605
|
+
for (const [id, original] of registryEntries) {
|
|
606
|
+
const node = nodes[id]
|
|
607
|
+
const clone = cloneByOriginal.get(original)
|
|
608
|
+
if (node?.type !== 'procedural-item' || !clone) continue
|
|
609
|
+
const lights = evaluateRecipe(node.recipe, node.parameters).lights
|
|
610
|
+
decorateProceduralEmission(clone, lights, true)
|
|
611
|
+
}
|
|
612
|
+
|
|
488
613
|
const retainedCloneByOriginal = retainedClones(scene, cloneByOriginal)
|
|
489
614
|
const keepClips = options.animations
|
|
490
615
|
? options.animations === 'keep'
|
|
@@ -492,13 +617,20 @@ function finishSceneExportPreparation(preparation: SceneExportPreparation): GlbE
|
|
|
492
617
|
const animation = keepClips
|
|
493
618
|
? bakeAnimationClips(retainedCloneByOriginal, nodes, registryEntries)
|
|
494
619
|
: { clips: [], clipNamesByNode: new Map<string, string[]>() }
|
|
620
|
+
if (!keepClips) {
|
|
621
|
+
for (const [id, original] of registryEntries) {
|
|
622
|
+
const node = nodes[id]
|
|
623
|
+
const clone = retainedCloneByOriginal.get(original)
|
|
624
|
+
if (node?.type === 'procedural-item' && clone) bakeRegistryAnimationClips(node, clone)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
495
627
|
stampIdentity(scene, retainedCloneByOriginal, nodes, animation.clipNamesByNode, registryEntries)
|
|
496
628
|
|
|
497
629
|
let disposed = false
|
|
498
630
|
return {
|
|
499
631
|
scene,
|
|
500
632
|
animations: animation.clips,
|
|
501
|
-
warnings
|
|
633
|
+
warnings,
|
|
502
634
|
dispose: () => {
|
|
503
635
|
if (disposed) return
|
|
504
636
|
disposed = true
|
|
@@ -714,45 +846,33 @@ function pruneHiddenSceneNodes(
|
|
|
714
846
|
nodes: Record<string, AnyNode>,
|
|
715
847
|
registryEntries: readonly RegistryEntry[],
|
|
716
848
|
) {
|
|
717
|
-
const
|
|
718
|
-
const declaredSiteParents = new Map<string, string>()
|
|
719
|
-
for (const node of Object.values(nodes)) {
|
|
720
|
-
if (node.type !== 'site' || !('children' in node) || !Array.isArray(node.children)) continue
|
|
721
|
-
for (const childId of node.children) {
|
|
722
|
-
const child = nodes[childId]
|
|
723
|
-
if (child && !child.parentId && !declaredSiteParents.has(childId)) {
|
|
724
|
-
declaredSiteParents.set(childId, node.id)
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
849
|
+
const isVisible = createExportVisibility(nodes)
|
|
728
850
|
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
851
|
+
const nodeClones = new Set<THREE.Object3D>()
|
|
852
|
+
for (const [, original] of registryEntries) {
|
|
853
|
+
const clone = cloneByOriginal.get(original)
|
|
854
|
+
if (clone) nodeClones.add(clone)
|
|
855
|
+
}
|
|
732
856
|
|
|
857
|
+
for (const [id, original] of registryEntries) {
|
|
858
|
+
if (isVisible(id)) continue
|
|
859
|
+
const clone = cloneByOriginal.get(original)
|
|
860
|
+
if (!clone) continue
|
|
733
861
|
const node = nodes[id]
|
|
734
|
-
if (!node)
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
return false
|
|
862
|
+
if (!node || hidesDescendants(node)) {
|
|
863
|
+
clone.removeFromParent()
|
|
864
|
+
continue
|
|
738
865
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
return true
|
|
866
|
+
// Drop the hidden Site's own ground fill and boundary; keep what it hosts.
|
|
867
|
+
for (const child of [...clone.children]) {
|
|
868
|
+
if (!hostsSceneNode(child, nodeClones)) child.removeFromParent()
|
|
743
869
|
}
|
|
744
|
-
|
|
745
|
-
path.add(id)
|
|
746
|
-
const visible = isVisible(parentId, path)
|
|
747
|
-
path.delete(id)
|
|
748
|
-
visibility.set(id, visible)
|
|
749
|
-
return visible
|
|
750
870
|
}
|
|
871
|
+
}
|
|
751
872
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
873
|
+
function hostsSceneNode(object: THREE.Object3D, nodeClones: Set<THREE.Object3D>): boolean {
|
|
874
|
+
if (nodeClones.has(object)) return true
|
|
875
|
+
return object.children.some((child) => hostsSceneNode(child, nodeClones))
|
|
756
876
|
}
|
|
757
877
|
|
|
758
878
|
function retainedClones(
|
|
@@ -889,7 +1009,16 @@ const PLACEHOLDER_MATERIAL = new THREE.MeshBasicMaterial({ visible: false })
|
|
|
889
1009
|
*/
|
|
890
1010
|
function pruneNonRenderableMeshes(root: THREE.Object3D, identityNodes: Set<THREE.Object3D>) {
|
|
891
1011
|
const toRemove: THREE.Object3D[] = []
|
|
1012
|
+
const warnings: string[] = []
|
|
892
1013
|
root.traverse((object) => {
|
|
1014
|
+
if (toRemove.some((ancestor) => isDescendantOf(object, ancestor))) return
|
|
1015
|
+
if (typeof object.userData.pascalExportRefusal === 'string') {
|
|
1016
|
+
warnings.push(
|
|
1017
|
+
`Skipped ${object.name || object.userData.pascalId || 'object'}: ${object.userData.pascalExportRefusal}`,
|
|
1018
|
+
)
|
|
1019
|
+
toRemove.push(object)
|
|
1020
|
+
return
|
|
1021
|
+
}
|
|
893
1022
|
if (object.userData.pascalExport === 'strip') {
|
|
894
1023
|
toRemove.push(object)
|
|
895
1024
|
return
|
|
@@ -948,6 +1077,7 @@ function pruneNonRenderableMeshes(root: THREE.Object3D, identityNodes: Set<THREE
|
|
|
948
1077
|
for (const object of toRemove) {
|
|
949
1078
|
object.removeFromParent()
|
|
950
1079
|
}
|
|
1080
|
+
return warnings
|
|
951
1081
|
}
|
|
952
1082
|
|
|
953
1083
|
/**
|
|
@@ -1329,15 +1459,19 @@ function bakeAnimationClips(
|
|
|
1329
1459
|
const target = cloneByOriginal.get(original)
|
|
1330
1460
|
if (!node || !target) continue
|
|
1331
1461
|
|
|
1462
|
+
// A window or door built from a script carries its clips like an authored item.
|
|
1463
|
+
const scripted = (node.type === 'door' || node.type === 'window') && node.source
|
|
1332
1464
|
const clip =
|
|
1333
1465
|
bakeRegistryAnimationClips(node, target) ??
|
|
1334
|
-
(
|
|
1335
|
-
?
|
|
1336
|
-
: node.type === '
|
|
1337
|
-
?
|
|
1338
|
-
: node.type === '
|
|
1339
|
-
?
|
|
1340
|
-
:
|
|
1466
|
+
(scripted
|
|
1467
|
+
? bakeItemClip(id, target)
|
|
1468
|
+
: node.type === 'door'
|
|
1469
|
+
? bakeDoorClip(id, node, target)
|
|
1470
|
+
: node.type === 'window'
|
|
1471
|
+
? bakeWindowClip(id, node as WindowNode, target)
|
|
1472
|
+
: node.type === 'item'
|
|
1473
|
+
? bakeItemClip(id, target)
|
|
1474
|
+
: null)
|
|
1341
1475
|
|
|
1342
1476
|
if (clip) {
|
|
1343
1477
|
const nodeClips = Array.isArray(clip) ? clip : [clip]
|
|
@@ -1360,44 +1494,47 @@ function bakeRegistryAnimationClips(
|
|
|
1360
1494
|
}
|
|
1361
1495
|
|
|
1362
1496
|
/**
|
|
1363
|
-
* Re-emit
|
|
1364
|
-
* subtree.
|
|
1365
|
-
* since every
|
|
1366
|
-
* cloned node's uuid so
|
|
1367
|
-
* per node (`<id>:
|
|
1497
|
+
* Re-emit an item's clips (a fan's spin, an authored object's motions) onto the
|
|
1498
|
+
* baked subtree. Source clips target the item GLB's nodes by name (`lamp_018`);
|
|
1499
|
+
* since every instance shares those names, we rebind each track to the
|
|
1500
|
+
* specific cloned node's uuid so instances animate independently. Clips are
|
|
1501
|
+
* named per node (`<id>: <name>`; the viewer drives `<id>: loop`).
|
|
1368
1502
|
*/
|
|
1369
|
-
function bakeItemClip(id: string, itemObject: THREE.Object3D): THREE.AnimationClip | null {
|
|
1370
|
-
const
|
|
1371
|
-
if (!
|
|
1503
|
+
function bakeItemClip(id: string, itemObject: THREE.Object3D): THREE.AnimationClip[] | null {
|
|
1504
|
+
const entries = itemClipRegistry.get(id)
|
|
1505
|
+
if (!entries?.length) return null
|
|
1372
1506
|
|
|
1373
|
-
const tracks: THREE.KeyframeTrack[] = []
|
|
1374
1507
|
// The catalog node names (e.g. "lamp_018") repeat across every instance of the
|
|
1375
1508
|
// item, and the glTF export→import roundtrip rebinds clip tracks by node name —
|
|
1376
|
-
// so a shared name would make all fans share one clip. Uniquify
|
|
1377
|
-
// node's name per item once, then bind
|
|
1509
|
+
// so a shared name would make all fans share one clip. Uniquify each targeted
|
|
1510
|
+
// node's name per item once (clips may share targets), then bind by uuid.
|
|
1378
1511
|
const renamed = new Map<string, THREE.Object3D>()
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1512
|
+
const clips: THREE.AnimationClip[] = []
|
|
1513
|
+
for (const entry of entries) {
|
|
1514
|
+
const tracks: THREE.KeyframeTrack[] = []
|
|
1515
|
+
for (const track of entry.clip.tracks) {
|
|
1516
|
+
const dot = track.name.lastIndexOf('.')
|
|
1517
|
+
if (dot < 0) continue
|
|
1518
|
+
const targetName = track.name.slice(0, dot)
|
|
1519
|
+
const property = track.name.slice(dot + 1)
|
|
1520
|
+
let targetNode = renamed.get(targetName)
|
|
1521
|
+
if (!targetNode) {
|
|
1522
|
+
const found = itemObject.getObjectByName(targetName)
|
|
1523
|
+
if (!found) continue
|
|
1524
|
+
found.name = `${id}__${targetName}`
|
|
1525
|
+
renamed.set(targetName, found)
|
|
1526
|
+
targetNode = found
|
|
1527
|
+
}
|
|
1528
|
+
const retargeted = track.clone()
|
|
1529
|
+
retargeted.name = `${targetNode.uuid}.${property}`
|
|
1530
|
+
tracks.push(retargeted)
|
|
1391
1531
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1532
|
+
if (tracks.length === 0) continue
|
|
1533
|
+
const clip = new THREE.AnimationClip(`${id}: ${entry.name}`, entry.clip.duration, tracks)
|
|
1534
|
+
clip.userData = { loop: entry.loop }
|
|
1535
|
+
clips.push(clip)
|
|
1395
1536
|
}
|
|
1396
|
-
|
|
1397
|
-
if (tracks.length === 0) return null
|
|
1398
|
-
const clip = new THREE.AnimationClip(`${id}: loop`, entry.clip.duration, tracks)
|
|
1399
|
-
clip.userData = { loop: entry.loop }
|
|
1400
|
-
return clip
|
|
1537
|
+
return clips.length > 0 ? clips : null
|
|
1401
1538
|
}
|
|
1402
1539
|
|
|
1403
1540
|
/**
|
|
@@ -1636,42 +1773,6 @@ function bakeWindowClip(
|
|
|
1636
1773
|
* (e.g. `pascalSwingLeaf`, cached-material flags) leaks into glTF extras — the
|
|
1637
1774
|
* file describes itself with exactly the fields a consumer needs.
|
|
1638
1775
|
*/
|
|
1639
|
-
/**
|
|
1640
|
-
* Human-readable label for a baked node, mirroring the viewer's `getNodeName`:
|
|
1641
|
-
* an explicit name wins, items fall back to their catalog asset name, other
|
|
1642
|
-
* kinds to a capitalized type. Levels override this with their display name.
|
|
1643
|
-
*/
|
|
1644
|
-
function nodeDisplayLabel(node: AnyNode): string {
|
|
1645
|
-
if (node.name) return node.name
|
|
1646
|
-
switch (node.type) {
|
|
1647
|
-
case 'item':
|
|
1648
|
-
return (node as { asset?: { name?: string } }).asset?.name || 'Item'
|
|
1649
|
-
case 'wall':
|
|
1650
|
-
return 'Wall'
|
|
1651
|
-
case 'door':
|
|
1652
|
-
return 'Door'
|
|
1653
|
-
case 'window':
|
|
1654
|
-
return 'Window'
|
|
1655
|
-
case 'cabinet':
|
|
1656
|
-
case 'cabinet-module':
|
|
1657
|
-
return 'Cabinet'
|
|
1658
|
-
case 'slab':
|
|
1659
|
-
return 'Slab'
|
|
1660
|
-
case 'ceiling':
|
|
1661
|
-
return 'Ceiling'
|
|
1662
|
-
case 'roof':
|
|
1663
|
-
return 'Roof'
|
|
1664
|
-
case 'fence':
|
|
1665
|
-
return 'Fence'
|
|
1666
|
-
case 'column':
|
|
1667
|
-
return 'Column'
|
|
1668
|
-
case 'stair':
|
|
1669
|
-
return 'Stairs'
|
|
1670
|
-
default:
|
|
1671
|
-
return node.type
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
1776
|
function stampIdentity(
|
|
1676
1777
|
scene: THREE.Object3D,
|
|
1677
1778
|
cloneByOriginal: Map<THREE.Object3D, THREE.Object3D>,
|
|
@@ -1682,8 +1783,43 @@ function stampIdentity(
|
|
|
1682
1783
|
scene.traverse((object) => {
|
|
1683
1784
|
const presentationId = object.userData.pascalPresentationId
|
|
1684
1785
|
const label = object.userData.label
|
|
1786
|
+
const motion = object.userData.proceduralMotion as
|
|
1787
|
+
| {
|
|
1788
|
+
nodeId: string
|
|
1789
|
+
partId: string
|
|
1790
|
+
groupId: string
|
|
1791
|
+
kind: 'hinge' | 'slide' | 'spin'
|
|
1792
|
+
clip?: string
|
|
1793
|
+
activeWindow?: [number, number]
|
|
1794
|
+
}
|
|
1795
|
+
| undefined
|
|
1796
|
+
const surfaceNodeIds = object.userData.surfaceNodeIds
|
|
1797
|
+
const ifcRole = object.userData.pascalIfcRole
|
|
1798
|
+
const ifcParts = object.userData.pascalIfcParts
|
|
1799
|
+
const slotId = object.userData.slotId
|
|
1685
1800
|
object.userData =
|
|
1686
1801
|
typeof presentationId === 'string' ? { pascalPresentationId: presentationId, label } : {}
|
|
1802
|
+
if (
|
|
1803
|
+
Array.isArray(surfaceNodeIds) &&
|
|
1804
|
+
surfaceNodeIds.every((id) => typeof id === 'string' && nodes[id])
|
|
1805
|
+
)
|
|
1806
|
+
object.userData.surfaceNodeIds = surfaceNodeIds
|
|
1807
|
+
if (typeof ifcRole === 'string') object.userData.pascalIfcRole = ifcRole
|
|
1808
|
+
if (Array.isArray(ifcParts) && ifcParts.every(isIfcRolePart))
|
|
1809
|
+
object.userData.pascalIfcParts = ifcParts
|
|
1810
|
+
if (typeof slotId === 'string') object.userData.slotId = slotId
|
|
1811
|
+
if (motion) {
|
|
1812
|
+
object.userData.proceduralMotion = {
|
|
1813
|
+
nodeId: motion.nodeId,
|
|
1814
|
+
partId: motion.partId,
|
|
1815
|
+
groupId: motion.groupId,
|
|
1816
|
+
kind: motion.kind,
|
|
1817
|
+
...(motion.clip && clipNamesByNode.get(motion.nodeId)?.includes(motion.clip)
|
|
1818
|
+
? { clip: motion.clip }
|
|
1819
|
+
: {}),
|
|
1820
|
+
...(motion.activeWindow ? { activeWindow: motion.activeWindow } : {}),
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1687
1823
|
})
|
|
1688
1824
|
|
|
1689
1825
|
for (const [id, original] of registryEntries) {
|
|
@@ -1708,21 +1844,15 @@ function stampIdentity(
|
|
|
1708
1844
|
extras.label = getLevelDisplayName(node as LevelNode)
|
|
1709
1845
|
target.visible = true
|
|
1710
1846
|
}
|
|
1711
|
-
//
|
|
1712
|
-
//
|
|
1713
|
-
//
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
}
|
|
1721
|
-
// Items with a baked ambient clip (a fan's spin) carry the clip name but no
|
|
1722
|
-
// `openable` flag — nothing opens; the clip just loops.
|
|
1723
|
-
if (node.type === 'item') {
|
|
1724
|
-
const clipNames = clipNamesByNode.get(id)
|
|
1725
|
-
if (clipNames?.length) extras.clips = clipNames
|
|
1847
|
+
// Every node that baked clips lists them, whatever its kind, so the viewer
|
|
1848
|
+
// can play them. Only nodes that actually baked an open clip are openable. A
|
|
1849
|
+
// cased opening (no leaf), fixed window, or static cabinet produces no clip,
|
|
1850
|
+
// so it stays unflagged — the file never claims a part opens when nothing
|
|
1851
|
+
// moves; an ambient loop (a fan's spin) is listed without the flag.
|
|
1852
|
+
const clipNames = clipNamesByNode.get(id)
|
|
1853
|
+
if (clipNames?.length) {
|
|
1854
|
+
if (clipNames.some((name) => name.endsWith(': open'))) extras.openable = true
|
|
1855
|
+
extras.clips = clipNames
|
|
1726
1856
|
}
|
|
1727
1857
|
if (node.type === 'zone') {
|
|
1728
1858
|
// Zone fills are stripped from the bake; /viewer rebuilds the room from
|
|
@@ -1730,6 +1860,7 @@ function stampIdentity(
|
|
|
1730
1860
|
// `onlyVisible` keeps it even when the editor had zones hidden at export.
|
|
1731
1861
|
const zone = node as ZoneNode
|
|
1732
1862
|
extras.polygon = zone.polygon
|
|
1863
|
+
if (zone.holes?.length) extras.holes = zone.holes
|
|
1733
1864
|
extras.color = zone.color
|
|
1734
1865
|
target.visible = true
|
|
1735
1866
|
}
|
|
@@ -1741,6 +1872,6 @@ function stampIdentity(
|
|
|
1741
1872
|
extras.rotation = (node as { rotation?: number }).rotation ?? 0
|
|
1742
1873
|
target.visible = true
|
|
1743
1874
|
}
|
|
1744
|
-
target.userData = extras
|
|
1875
|
+
target.userData = { ...target.userData, ...extras }
|
|
1745
1876
|
}
|
|
1746
1877
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { Group, Vector3 } from 'three'
|
|
3
|
+
import { gridLocalNormal, gridLocalPoint } from './grid-frame'
|
|
4
|
+
|
|
5
|
+
test('grid floor and cursor stay in model coordinates after God-view pan, scale and yaw', () => {
|
|
6
|
+
const root = new Group()
|
|
7
|
+
root.position.set(2, 4, -3)
|
|
8
|
+
root.rotation.y = Math.PI / 3
|
|
9
|
+
root.scale.setScalar(0.2)
|
|
10
|
+
root.updateMatrixWorld(true)
|
|
11
|
+
const local = new Vector3(3, 2.5, 7)
|
|
12
|
+
const world = root.localToWorld(local.clone())
|
|
13
|
+
expect(gridLocalPoint(root, world, new Vector3()).distanceTo(local)).toBeLessThan(1e-8)
|
|
14
|
+
const normal = new Vector3(1, 0, 0).transformDirection(root.matrixWorld)
|
|
15
|
+
expect(
|
|
16
|
+
gridLocalNormal(root, normal, new Vector3()).distanceTo(new Vector3(1, 0, 0)),
|
|
17
|
+
).toBeLessThan(1e-8)
|
|
18
|
+
expect(world.distanceTo(local)).toBeGreaterThan(1)
|
|
19
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Object3D, Vector3 } from 'three'
|
|
2
|
+
|
|
3
|
+
// Placement surfaces are world-space; the grid lives under the scalable XR
|
|
4
|
+
// scene root. Keep its lattice and reveal in that root's model coordinates.
|
|
5
|
+
export function gridLocalPoint(parent: Object3D | null, world: Vector3, target: Vector3) {
|
|
6
|
+
target.copy(world)
|
|
7
|
+
if (parent) {
|
|
8
|
+
parent.updateWorldMatrix(true, false)
|
|
9
|
+
parent.worldToLocal(target)
|
|
10
|
+
}
|
|
11
|
+
return target
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function gridLocalNormal(parent: Object3D | null, normal: Vector3, target: Vector3) {
|
|
15
|
+
target.copy(normal)
|
|
16
|
+
if (parent) target.transformDirection(parent.matrixWorld.clone().invert())
|
|
17
|
+
return target
|
|
18
|
+
}
|
package/src/lib/history.test.ts
CHANGED
|
@@ -217,6 +217,9 @@ describe('standalone history source invalidation', () => {
|
|
|
217
217
|
const { Group, Mesh, MeshBasicMaterial, BoxGeometry } = await importShared('three')
|
|
218
218
|
const viewer = await importShared('@pascal-app/viewer')
|
|
219
219
|
const { captureChangedNodes, runBatchFrame, resetNodeBatchState } = await import(${JSON.stringify(resolve(import.meta.dir, '../../../nodes/src/shared/node-batch/system.tsx'))})
|
|
220
|
+
// The node batch reads each kind's \`capabilities.batchable\` from the registry.
|
|
221
|
+
const { doorBatchable } = await import(${JSON.stringify(resolve(import.meta.dir, '../../../nodes/src/shared/node-batch/batchable.ts'))})
|
|
222
|
+
core.nodeRegistry._register({ kind: 'door', schemaVersion: 1, capabilities: { batchable: doorBatchable } })
|
|
220
223
|
const root = new Group()
|
|
221
224
|
core.sceneRegistry.nodes.set(level.id, root)
|
|
222
225
|
core.sceneRegistry.byType.level.add(level.id)
|
|
@@ -260,6 +263,9 @@ describe('standalone history source invalidation', () => {
|
|
|
260
263
|
const { Group, Mesh, MeshBasicMaterial, BoxGeometry } = await importShared('three')
|
|
261
264
|
const viewer = await importShared('@pascal-app/viewer')
|
|
262
265
|
const { captureChangedNodes, runBatchFrame, resetNodeBatchState } = await import(${JSON.stringify(resolve(import.meta.dir, '../../../nodes/src/shared/node-batch/system.tsx'))})
|
|
266
|
+
// The node batch reads each kind's \`capabilities.batchable\` from the registry.
|
|
267
|
+
const { doorBatchable } = await import(${JSON.stringify(resolve(import.meta.dir, '../../../nodes/src/shared/node-batch/batchable.ts'))})
|
|
268
|
+
core.nodeRegistry._register({ kind: 'door', schemaVersion: 1, capabilities: { batchable: doorBatchable } })
|
|
263
269
|
const root = new Group()
|
|
264
270
|
core.sceneRegistry.nodes.set(level.id, root)
|
|
265
271
|
core.sceneRegistry.byType.level.add(level.id)
|
package/src/lib/history.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@pascal-app/core'
|
|
10
10
|
import { markPerfAction } from '@pascal-app/viewer'
|
|
11
11
|
import useInteractionScope from '../store/use-interaction-scope'
|
|
12
|
+
import { cancelGestures } from './gesture-lifecycle'
|
|
12
13
|
import { registeredDraftingConfig } from './interaction/registered-drafting'
|
|
13
14
|
|
|
14
15
|
export type HistoryCommandState = {
|
|
@@ -130,6 +131,9 @@ export function shouldCancelDraftOnHistoryJump(): boolean {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
export function runUndo(): HistoryCommandResult {
|
|
134
|
+
// A history jump under a live gesture would land on its paused baseline;
|
|
135
|
+
// every history command, not only the keyboard's, ends it first.
|
|
136
|
+
cancelGestures('history')
|
|
133
137
|
if (shouldCancelDraftOnHistoryJump()) emitter.emit('tool:cancel')
|
|
134
138
|
if (historyCommandDelegate) {
|
|
135
139
|
const result = historyCommandDelegate.undo()
|
|
@@ -147,6 +151,9 @@ export function runUndo(): HistoryCommandResult {
|
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
export function runRedo(): HistoryCommandResult {
|
|
154
|
+
// A history jump under a live gesture would land on its paused baseline;
|
|
155
|
+
// every history command, not only the keyboard's, ends it first.
|
|
156
|
+
cancelGestures('history')
|
|
150
157
|
if (shouldCancelDraftOnHistoryJump()) emitter.emit('tool:cancel')
|
|
151
158
|
if (historyCommandDelegate) {
|
|
152
159
|
const result = historyCommandDelegate.redo()
|