@pascal-app/editor 1.0.3 → 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 +11 -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 +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -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 +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -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 +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/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 +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- 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-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- 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 +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- 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/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- 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 +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- 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/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 +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +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-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 +4 -4
- 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/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/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- 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.tsx +5 -6
- 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 +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- 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 +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/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 +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +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 +59 -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 +485 -0
- package/src/lib/glb-export.ts +237 -119
- 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 +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- 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 +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- 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/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 +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- 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 +24 -36
- package/src/lib/scene.ts +33 -57
- 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 +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -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/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-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
|
@@ -19,7 +19,9 @@ import {
|
|
|
19
19
|
type WindowNode,
|
|
20
20
|
type ZoneNode,
|
|
21
21
|
} from '@pascal-app/core'
|
|
22
|
+
import { evaluateRecipe } from '@pascal-app/core/procedural-items'
|
|
22
23
|
import {
|
|
24
|
+
decorateProceduralEmission,
|
|
23
25
|
getPascalTextureRef,
|
|
24
26
|
isViewerPresentationTextureBorrowed,
|
|
25
27
|
poseDoorMovingParts,
|
|
@@ -38,6 +40,8 @@ import {
|
|
|
38
40
|
} from 'three/examples/jsm/exporters/GLTFExporter.js'
|
|
39
41
|
import { createExportTextureUtils, type ExportTextureUtils } from './export-texture-utils'
|
|
40
42
|
import { cloneExportUserData } from './export-user-data'
|
|
43
|
+
import { isIfcRolePart } from './ifc-parts'
|
|
44
|
+
import { nodeDisplayLabel } from './node-display-label'
|
|
41
45
|
import {
|
|
42
46
|
type CompressedTextureDecompressor,
|
|
43
47
|
decompressCanonicalNormalMaps,
|
|
@@ -141,6 +145,115 @@ export function nextFrames(): Promise<void> {
|
|
|
141
145
|
})
|
|
142
146
|
}
|
|
143
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
|
+
|
|
144
257
|
type GltfExtrasDef = {
|
|
145
258
|
extras?: Record<string, unknown>
|
|
146
259
|
}
|
|
@@ -369,7 +482,10 @@ function startSceneExportPreparation(
|
|
|
369
482
|
|
|
370
483
|
for (const [id, original] of registryEntries) {
|
|
371
484
|
const node = nodes[id]
|
|
372
|
-
if (!node)
|
|
485
|
+
if (!node) {
|
|
486
|
+
excludedObjects.add(original)
|
|
487
|
+
continue
|
|
488
|
+
}
|
|
373
489
|
const installedPlugins = sceneState.hasExplicitPluginInstallState
|
|
374
490
|
? sceneState.installedPlugins
|
|
375
491
|
: undefined
|
|
@@ -482,10 +598,18 @@ function finishSceneExportPreparation(preparation: SceneExportPreparation): GlbE
|
|
|
482
598
|
if (clone) identityNodes.add(clone)
|
|
483
599
|
}
|
|
484
600
|
|
|
485
|
-
pruneNonRenderableMeshes(scene, identityNodes)
|
|
601
|
+
const warnings = pruneNonRenderableMeshes(scene, identityNodes)
|
|
486
602
|
sanitizeMaterialGroups(scene, identityNodes)
|
|
487
603
|
convertMaterials(scene, options.textures ?? 'embed', options.purpose ?? 'viewer')
|
|
488
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
|
+
|
|
489
613
|
const retainedCloneByOriginal = retainedClones(scene, cloneByOriginal)
|
|
490
614
|
const keepClips = options.animations
|
|
491
615
|
? options.animations === 'keep'
|
|
@@ -493,13 +617,20 @@ function finishSceneExportPreparation(preparation: SceneExportPreparation): GlbE
|
|
|
493
617
|
const animation = keepClips
|
|
494
618
|
? bakeAnimationClips(retainedCloneByOriginal, nodes, registryEntries)
|
|
495
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
|
+
}
|
|
496
627
|
stampIdentity(scene, retainedCloneByOriginal, nodes, animation.clipNamesByNode, registryEntries)
|
|
497
628
|
|
|
498
629
|
let disposed = false
|
|
499
630
|
return {
|
|
500
631
|
scene,
|
|
501
632
|
animations: animation.clips,
|
|
502
|
-
warnings
|
|
633
|
+
warnings,
|
|
503
634
|
dispose: () => {
|
|
504
635
|
if (disposed) return
|
|
505
636
|
disposed = true
|
|
@@ -715,31 +846,7 @@ function pruneHiddenSceneNodes(
|
|
|
715
846
|
nodes: Record<string, AnyNode>,
|
|
716
847
|
registryEntries: readonly RegistryEntry[],
|
|
717
848
|
) {
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
const isVisible = (id: string, path: Set<string>): boolean => {
|
|
721
|
-
const cached = visibility.get(id)
|
|
722
|
-
if (cached !== undefined) return cached
|
|
723
|
-
|
|
724
|
-
const node = nodes[id]
|
|
725
|
-
if (!node) return true
|
|
726
|
-
if (node.visible === false) {
|
|
727
|
-
visibility.set(id, false)
|
|
728
|
-
return false
|
|
729
|
-
}
|
|
730
|
-
const parentId = node.parentId
|
|
731
|
-
const parent = parentId ? nodes[parentId] : undefined
|
|
732
|
-
if (!parentId || path.has(id) || (parent && !hidesDescendants(parent))) {
|
|
733
|
-
visibility.set(id, true)
|
|
734
|
-
return true
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
path.add(id)
|
|
738
|
-
const visible = isVisible(parentId, path)
|
|
739
|
-
path.delete(id)
|
|
740
|
-
visibility.set(id, visible)
|
|
741
|
-
return visible
|
|
742
|
-
}
|
|
849
|
+
const isVisible = createExportVisibility(nodes)
|
|
743
850
|
|
|
744
851
|
const nodeClones = new Set<THREE.Object3D>()
|
|
745
852
|
for (const [, original] of registryEntries) {
|
|
@@ -748,7 +855,7 @@ function pruneHiddenSceneNodes(
|
|
|
748
855
|
}
|
|
749
856
|
|
|
750
857
|
for (const [id, original] of registryEntries) {
|
|
751
|
-
if (isVisible(id
|
|
858
|
+
if (isVisible(id)) continue
|
|
752
859
|
const clone = cloneByOriginal.get(original)
|
|
753
860
|
if (!clone) continue
|
|
754
861
|
const node = nodes[id]
|
|
@@ -902,7 +1009,16 @@ const PLACEHOLDER_MATERIAL = new THREE.MeshBasicMaterial({ visible: false })
|
|
|
902
1009
|
*/
|
|
903
1010
|
function pruneNonRenderableMeshes(root: THREE.Object3D, identityNodes: Set<THREE.Object3D>) {
|
|
904
1011
|
const toRemove: THREE.Object3D[] = []
|
|
1012
|
+
const warnings: string[] = []
|
|
905
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
|
+
}
|
|
906
1022
|
if (object.userData.pascalExport === 'strip') {
|
|
907
1023
|
toRemove.push(object)
|
|
908
1024
|
return
|
|
@@ -961,6 +1077,7 @@ function pruneNonRenderableMeshes(root: THREE.Object3D, identityNodes: Set<THREE
|
|
|
961
1077
|
for (const object of toRemove) {
|
|
962
1078
|
object.removeFromParent()
|
|
963
1079
|
}
|
|
1080
|
+
return warnings
|
|
964
1081
|
}
|
|
965
1082
|
|
|
966
1083
|
/**
|
|
@@ -1342,15 +1459,19 @@ function bakeAnimationClips(
|
|
|
1342
1459
|
const target = cloneByOriginal.get(original)
|
|
1343
1460
|
if (!node || !target) continue
|
|
1344
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
|
|
1345
1464
|
const clip =
|
|
1346
1465
|
bakeRegistryAnimationClips(node, target) ??
|
|
1347
|
-
(
|
|
1348
|
-
?
|
|
1349
|
-
: node.type === '
|
|
1350
|
-
?
|
|
1351
|
-
: node.type === '
|
|
1352
|
-
?
|
|
1353
|
-
:
|
|
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)
|
|
1354
1475
|
|
|
1355
1476
|
if (clip) {
|
|
1356
1477
|
const nodeClips = Array.isArray(clip) ? clip : [clip]
|
|
@@ -1373,44 +1494,47 @@ function bakeRegistryAnimationClips(
|
|
|
1373
1494
|
}
|
|
1374
1495
|
|
|
1375
1496
|
/**
|
|
1376
|
-
* Re-emit
|
|
1377
|
-
* subtree.
|
|
1378
|
-
* since every
|
|
1379
|
-
* cloned node's uuid so
|
|
1380
|
-
* 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`).
|
|
1381
1502
|
*/
|
|
1382
|
-
function bakeItemClip(id: string, itemObject: THREE.Object3D): THREE.AnimationClip | null {
|
|
1383
|
-
const
|
|
1384
|
-
if (!
|
|
1503
|
+
function bakeItemClip(id: string, itemObject: THREE.Object3D): THREE.AnimationClip[] | null {
|
|
1504
|
+
const entries = itemClipRegistry.get(id)
|
|
1505
|
+
if (!entries?.length) return null
|
|
1385
1506
|
|
|
1386
|
-
const tracks: THREE.KeyframeTrack[] = []
|
|
1387
1507
|
// The catalog node names (e.g. "lamp_018") repeat across every instance of the
|
|
1388
1508
|
// item, and the glTF export→import roundtrip rebinds clip tracks by node name —
|
|
1389
|
-
// so a shared name would make all fans share one clip. Uniquify
|
|
1390
|
-
// 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.
|
|
1391
1511
|
const renamed = new Map<string, THREE.Object3D>()
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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)
|
|
1404
1531
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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)
|
|
1408
1536
|
}
|
|
1409
|
-
|
|
1410
|
-
if (tracks.length === 0) return null
|
|
1411
|
-
const clip = new THREE.AnimationClip(`${id}: loop`, entry.clip.duration, tracks)
|
|
1412
|
-
clip.userData = { loop: entry.loop }
|
|
1413
|
-
return clip
|
|
1537
|
+
return clips.length > 0 ? clips : null
|
|
1414
1538
|
}
|
|
1415
1539
|
|
|
1416
1540
|
/**
|
|
@@ -1649,42 +1773,6 @@ function bakeWindowClip(
|
|
|
1649
1773
|
* (e.g. `pascalSwingLeaf`, cached-material flags) leaks into glTF extras — the
|
|
1650
1774
|
* file describes itself with exactly the fields a consumer needs.
|
|
1651
1775
|
*/
|
|
1652
|
-
/**
|
|
1653
|
-
* Human-readable label for a baked node, mirroring the viewer's `getNodeName`:
|
|
1654
|
-
* an explicit name wins, items fall back to their catalog asset name, other
|
|
1655
|
-
* kinds to a capitalized type. Levels override this with their display name.
|
|
1656
|
-
*/
|
|
1657
|
-
function nodeDisplayLabel(node: AnyNode): string {
|
|
1658
|
-
if (node.name) return node.name
|
|
1659
|
-
switch (node.type) {
|
|
1660
|
-
case 'item':
|
|
1661
|
-
return (node as { asset?: { name?: string } }).asset?.name || 'Item'
|
|
1662
|
-
case 'wall':
|
|
1663
|
-
return 'Wall'
|
|
1664
|
-
case 'door':
|
|
1665
|
-
return 'Door'
|
|
1666
|
-
case 'window':
|
|
1667
|
-
return 'Window'
|
|
1668
|
-
case 'cabinet':
|
|
1669
|
-
case 'cabinet-module':
|
|
1670
|
-
return 'Cabinet'
|
|
1671
|
-
case 'slab':
|
|
1672
|
-
return 'Slab'
|
|
1673
|
-
case 'ceiling':
|
|
1674
|
-
return 'Ceiling'
|
|
1675
|
-
case 'roof':
|
|
1676
|
-
return 'Roof'
|
|
1677
|
-
case 'fence':
|
|
1678
|
-
return 'Fence'
|
|
1679
|
-
case 'column':
|
|
1680
|
-
return 'Column'
|
|
1681
|
-
case 'stair':
|
|
1682
|
-
return 'Stairs'
|
|
1683
|
-
default:
|
|
1684
|
-
return node.type
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
1776
|
function stampIdentity(
|
|
1689
1777
|
scene: THREE.Object3D,
|
|
1690
1778
|
cloneByOriginal: Map<THREE.Object3D, THREE.Object3D>,
|
|
@@ -1695,8 +1783,43 @@ function stampIdentity(
|
|
|
1695
1783
|
scene.traverse((object) => {
|
|
1696
1784
|
const presentationId = object.userData.pascalPresentationId
|
|
1697
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
|
|
1698
1800
|
object.userData =
|
|
1699
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
|
+
}
|
|
1700
1823
|
})
|
|
1701
1824
|
|
|
1702
1825
|
for (const [id, original] of registryEntries) {
|
|
@@ -1721,21 +1844,15 @@ function stampIdentity(
|
|
|
1721
1844
|
extras.label = getLevelDisplayName(node as LevelNode)
|
|
1722
1845
|
target.visible = true
|
|
1723
1846
|
}
|
|
1724
|
-
//
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
}
|
|
1734
|
-
// Items with a baked ambient clip (a fan's spin) carry the clip name but no
|
|
1735
|
-
// `openable` flag — nothing opens; the clip just loops.
|
|
1736
|
-
if (node.type === 'item') {
|
|
1737
|
-
const clipNames = clipNamesByNode.get(id)
|
|
1738
|
-
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
|
|
1739
1856
|
}
|
|
1740
1857
|
if (node.type === 'zone') {
|
|
1741
1858
|
// Zone fills are stripped from the bake; /viewer rebuilds the room from
|
|
@@ -1743,6 +1860,7 @@ function stampIdentity(
|
|
|
1743
1860
|
// `onlyVisible` keeps it even when the editor had zones hidden at export.
|
|
1744
1861
|
const zone = node as ZoneNode
|
|
1745
1862
|
extras.polygon = zone.polygon
|
|
1863
|
+
if (zone.holes?.length) extras.holes = zone.holes
|
|
1746
1864
|
extras.color = zone.color
|
|
1747
1865
|
target.visible = true
|
|
1748
1866
|
}
|
|
@@ -1754,6 +1872,6 @@ function stampIdentity(
|
|
|
1754
1872
|
extras.rotation = (node as { rotation?: number }).rotation ?? 0
|
|
1755
1873
|
target.visible = true
|
|
1756
1874
|
}
|
|
1757
|
-
target.userData = extras
|
|
1875
|
+
target.userData = { ...target.userData, ...extras }
|
|
1758
1876
|
}
|
|
1759
1877
|
}
|
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()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type IconRef, nodeRegistry, type TerrainVerb } from '@pascal-app/core'
|
|
2
|
+
import type { ContinuationMode } from './continuation'
|
|
3
|
+
import { getWallDrawVariantInfo, wallDrawVariantOf } from './wall-draw-variant'
|
|
4
|
+
|
|
5
|
+
/** The contextual helper panel's header: what is in hand, and its key. */
|
|
6
|
+
export type HudTitle = {
|
|
7
|
+
label: string
|
|
8
|
+
icon?: IconRef
|
|
9
|
+
shortcut?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const url = (src: string): IconRef => ({ kind: 'url', src })
|
|
13
|
+
|
|
14
|
+
/** Tools with a global arming key (`use-keyboard.ts`). */
|
|
15
|
+
const TOOL_SHORTCUTS: Record<string, string> = {
|
|
16
|
+
wall: 'B',
|
|
17
|
+
measurement: 'M',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SELECT_HUD_TITLE: HudTitle = {
|
|
21
|
+
label: 'Select',
|
|
22
|
+
icon: url('/icons/select.webp'),
|
|
23
|
+
shortcut: 'V',
|
|
24
|
+
}
|
|
25
|
+
export const PAINT_HUD_TITLE: HudTitle = { label: 'Paint', icon: url('/icons/paint.webp') }
|
|
26
|
+
export const ERASE_HUD_TITLE: HudTitle = {
|
|
27
|
+
label: 'Erase',
|
|
28
|
+
icon: { kind: 'iconify', name: 'lucide:eraser' },
|
|
29
|
+
}
|
|
30
|
+
export const PICK_MATERIAL_HUD_TITLE: HudTitle = {
|
|
31
|
+
label: 'Pick material',
|
|
32
|
+
icon: { kind: 'iconify', name: 'lucide:pipette' },
|
|
33
|
+
}
|
|
34
|
+
export const ROTATE_HUD_TITLE: HudTitle = { label: 'Rotate', icon: url('/icons/rotate.webp') }
|
|
35
|
+
export const MOVE_SELECTION_HUD_TITLE: HudTitle = {
|
|
36
|
+
label: 'Move selection',
|
|
37
|
+
icon: url('/icons/pan.webp'),
|
|
38
|
+
}
|
|
39
|
+
export const MOVE_ROOM_HUD_TITLE: HudTitle = { label: 'Move room', icon: url('/icons/room.webp') }
|
|
40
|
+
export const RESIZE_HUD_TITLE: HudTitle = {
|
|
41
|
+
label: 'Resize',
|
|
42
|
+
icon: url('/icons/resize.webp'),
|
|
43
|
+
}
|
|
44
|
+
export const DIVIDE_ROOM_HUD_TITLE: HudTitle = {
|
|
45
|
+
label: 'Divide room',
|
|
46
|
+
icon: url('/icons/divide-room.webp'),
|
|
47
|
+
}
|
|
48
|
+
export const MEZZANINE_HUD_TITLE: HudTitle = {
|
|
49
|
+
label: 'Mezzanine',
|
|
50
|
+
icon: url('/icons/mezzanine.webp'),
|
|
51
|
+
}
|
|
52
|
+
export const OPENING_HUD_TITLE: HudTitle = {
|
|
53
|
+
label: 'Cut opening',
|
|
54
|
+
icon: { kind: 'iconify', name: 'lucide:square-dashed' },
|
|
55
|
+
}
|
|
56
|
+
export const TERRACE_HUD_TITLE: HudTitle = {
|
|
57
|
+
label: 'Terrace',
|
|
58
|
+
icon: url('/icons/floor.webp'),
|
|
59
|
+
}
|
|
60
|
+
export const SMART_MEASURE_HUD_TITLE: HudTitle = {
|
|
61
|
+
label: 'Smart measure',
|
|
62
|
+
icon: url('/icons/measure.webp'),
|
|
63
|
+
shortcut: 'M',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const PAINT_REGION_LABELS = {
|
|
67
|
+
rectangle: 'Paint a rectangle',
|
|
68
|
+
polygon: 'Paint a shape',
|
|
69
|
+
} as const
|
|
70
|
+
|
|
71
|
+
export function paintRegionHudTitle(mode: keyof typeof PAINT_REGION_LABELS): HudTitle {
|
|
72
|
+
return { label: PAINT_REGION_LABELS[mode], icon: url('/icons/paint.webp') }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function terrainHudTitle(verb: TerrainVerb): HudTitle {
|
|
76
|
+
const label =
|
|
77
|
+
verb === 'raise'
|
|
78
|
+
? 'Raise terrain'
|
|
79
|
+
: verb === 'lower'
|
|
80
|
+
? 'Lower terrain'
|
|
81
|
+
: verb === 'flatten'
|
|
82
|
+
? 'Level terrain'
|
|
83
|
+
: 'Smooth terrain'
|
|
84
|
+
return { label, icon: url(`/icons/terrain-${verb}.webp`) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A kind's registry name and icon, optionally behind a verb ("Move door"). */
|
|
88
|
+
export function nodeKindHudTitle(kind: string, verb?: string): HudTitle | null {
|
|
89
|
+
const presentation = nodeRegistry.get(kind)?.presentation
|
|
90
|
+
if (!presentation) return null
|
|
91
|
+
const name = presentation.label
|
|
92
|
+
return {
|
|
93
|
+
label: verb ? `${verb} ${name.toLowerCase()}` : name,
|
|
94
|
+
icon: presentation.icon,
|
|
95
|
+
shortcut: verb ? undefined : TOOL_SHORTCUTS[kind],
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The armed build tool. The wall tool is named after the Rooms variant it draws
|
|
101
|
+
* (Rectangle room / Polygon room / Walls) — the tile the user picked.
|
|
102
|
+
*/
|
|
103
|
+
export function toolHudTitle(tool: string, wallMode: ContinuationMode): HudTitle | null {
|
|
104
|
+
if (tool === 'wall') {
|
|
105
|
+
const variant = getWallDrawVariantInfo(wallDrawVariantOf(wallMode))
|
|
106
|
+
return { label: variant.title, icon: url(variant.iconSrc), shortcut: TOOL_SHORTCUTS.wall }
|
|
107
|
+
}
|
|
108
|
+
return nodeKindHudTitle(tool)
|
|
109
|
+
}
|