@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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, emitter, type NodeEvent, sceneRegistry, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { useThree } from '@react-three/fiber'
|
|
6
|
+
import { useEffect } from 'react'
|
|
7
|
+
import { selectionModifiersFromEvent } from '../../lib/selection-routing'
|
|
8
|
+
import {
|
|
9
|
+
applyViewerClick,
|
|
10
|
+
applyViewerHover,
|
|
11
|
+
viewerPickFromNodeEvent,
|
|
12
|
+
} from '../../lib/viewer-selection'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Selection for a parametric viewer surface (the published viewer's fallback,
|
|
16
|
+
* its embed, the editor's Preview): free-form and room-first, through the same
|
|
17
|
+
* rules as the baked viewer. Mount it with `<Viewer selectionManager="custom">`.
|
|
18
|
+
*/
|
|
19
|
+
export function ViewerSelectionManager() {
|
|
20
|
+
const gl = useThree((state) => state.gl)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let clickHandled = false
|
|
24
|
+
let hoveredFrom: string | null = null
|
|
25
|
+
const pendingClicks = new Set<number>()
|
|
26
|
+
|
|
27
|
+
const onMove = (event: NodeEvent) => {
|
|
28
|
+
if (useViewer.getState().walkthroughMode) return
|
|
29
|
+
const pick = viewerPickFromNodeEvent(event, useScene.getState().nodes)
|
|
30
|
+
if (!pick) return
|
|
31
|
+
event.stopPropagation()
|
|
32
|
+
hoveredFrom = event.node.id
|
|
33
|
+
applyViewerHover(pick, useScene.getState().nodes)
|
|
34
|
+
}
|
|
35
|
+
const onLeave = (event: NodeEvent) => {
|
|
36
|
+
if (event.node.id !== hoveredFrom) return
|
|
37
|
+
hoveredFrom = null
|
|
38
|
+
useViewer.getState().setHoveredId(null)
|
|
39
|
+
}
|
|
40
|
+
const onClick = (event: NodeEvent) => {
|
|
41
|
+
if (useViewer.getState().walkthroughMode) return
|
|
42
|
+
const pick = viewerPickFromNodeEvent(event, useScene.getState().nodes)
|
|
43
|
+
if (!pick) return
|
|
44
|
+
event.stopPropagation()
|
|
45
|
+
clickHandled = true
|
|
46
|
+
applyViewerClick(
|
|
47
|
+
pick,
|
|
48
|
+
useScene.getState().nodes,
|
|
49
|
+
selectionModifiersFromEvent(event.nativeEvent),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
// A click on nothing selectable clears the room and the element.
|
|
53
|
+
const onCanvasClick = (event: MouseEvent) => {
|
|
54
|
+
const viewer = useViewer.getState()
|
|
55
|
+
if (event.button !== 0 || viewer.walkthroughMode) return
|
|
56
|
+
if (viewer.cameraDragging || viewer.inputDragging) return
|
|
57
|
+
const frame = requestAnimationFrame(() => {
|
|
58
|
+
pendingClicks.delete(frame)
|
|
59
|
+
if (useViewer.getState().walkthroughMode) return
|
|
60
|
+
if (clickHandled) {
|
|
61
|
+
clickHandled = false
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
applyViewerClick(null, useScene.getState().nodes)
|
|
65
|
+
})
|
|
66
|
+
pendingClicks.add(frame)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
emitter.on('node:move', onMove)
|
|
70
|
+
emitter.on('node:enter', onMove)
|
|
71
|
+
emitter.on('node:leave', onLeave)
|
|
72
|
+
emitter.on('node:click', onClick)
|
|
73
|
+
const canvas = gl.domElement
|
|
74
|
+
canvas.addEventListener('click', onCanvasClick)
|
|
75
|
+
return () => {
|
|
76
|
+
emitter.off('node:move', onMove)
|
|
77
|
+
emitter.off('node:enter', onMove)
|
|
78
|
+
emitter.off('node:leave', onLeave)
|
|
79
|
+
emitter.off('node:click', onClick)
|
|
80
|
+
canvas.removeEventListener('click', onCanvasClick)
|
|
81
|
+
for (const frame of pendingClicks) cancelAnimationFrame(frame)
|
|
82
|
+
useViewer.getState().setHoveredId(null)
|
|
83
|
+
}
|
|
84
|
+
}, [gl])
|
|
85
|
+
|
|
86
|
+
return <ViewerOutlineSync />
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The selected elements and the hovered one, outlined (rooms highlight instead). */
|
|
90
|
+
function ViewerOutlineSync() {
|
|
91
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
92
|
+
const externalSelectedIds = useViewer((s) => s.externalSelectedIds)
|
|
93
|
+
const hoveredId = useViewer((s) => s.hoveredId)
|
|
94
|
+
const outliner = useViewer((s) => s.outliner)
|
|
95
|
+
const geometryRevision = useViewer((s) => s.geometryRevision)
|
|
96
|
+
const walkthrough = useViewer((s) => s.walkthroughMode)
|
|
97
|
+
const nodes = useScene((s) => s.nodes)
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
void geometryRevision
|
|
101
|
+
outliner.selectedObjects.length = 0
|
|
102
|
+
outliner.hoveredObjects.length = 0
|
|
103
|
+
if (walkthrough) return
|
|
104
|
+
for (const id of new Set([...selectedIds, ...externalSelectedIds])) {
|
|
105
|
+
const object = sceneRegistry.nodes.get(id)
|
|
106
|
+
if (object) outliner.selectedObjects.push(object)
|
|
107
|
+
}
|
|
108
|
+
const hovered = hoveredId ? nodes[hoveredId as AnyNodeId] : undefined
|
|
109
|
+
if (hovered && hovered.type !== 'zone') {
|
|
110
|
+
const object = sceneRegistry.nodes.get(hovered.id)
|
|
111
|
+
if (object) outliner.hoveredObjects.push(object)
|
|
112
|
+
}
|
|
113
|
+
}, [selectedIds, externalSelectedIds, hoveredId, outliner, nodes, geometryRevision, walkthrough])
|
|
114
|
+
|
|
115
|
+
useEffect(
|
|
116
|
+
() => () => {
|
|
117
|
+
outliner.selectedObjects.length = 0
|
|
118
|
+
outliner.hoveredObjects.length = 0
|
|
119
|
+
},
|
|
120
|
+
[outliner],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
@@ -60,7 +60,6 @@ function selectViewerLevel(nodes: Record<string, AnyNode>, levelId: string) {
|
|
|
60
60
|
selectedIds: [],
|
|
61
61
|
zoneId: null,
|
|
62
62
|
})
|
|
63
|
-
viewer.setLevelMode('solo')
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
export function ViewerStage({
|
|
@@ -137,7 +136,8 @@ export function ViewerStage({
|
|
|
137
136
|
if (notify && nextLevelId !== useViewer.getState().selection.levelId) {
|
|
138
137
|
markPerfAction('level-switch', nextLevelId)
|
|
139
138
|
}
|
|
140
|
-
|
|
139
|
+
if (nextLevelId !== useViewer.getState().selection.levelId)
|
|
140
|
+
selectViewerLevel(scene?.nodes ?? useScene.getState().nodes, nextLevelId)
|
|
141
141
|
if (notify) onLevelChange?.(nextLevelId)
|
|
142
142
|
},
|
|
143
143
|
[onLevelChange, scene],
|
|
@@ -11,7 +11,6 @@ import useInteractionScope from '../store/use-interaction-scope'
|
|
|
11
11
|
export const ViewerZoneSystem = () => {
|
|
12
12
|
useFrame(() => {
|
|
13
13
|
const { levelId, zoneId } = useViewer.getState().selection
|
|
14
|
-
const structureLayer = useEditor.getState().structureLayer
|
|
15
14
|
const nodes = useScene.getState().nodes
|
|
16
15
|
// Snapshot capture is a clean, camera-only surface — zone geometry and
|
|
17
16
|
// tags stay out of the framed shot (mirrors the editor ZoneSystem's gate).
|
|
@@ -29,17 +28,13 @@ export const ViewerZoneSystem = () => {
|
|
|
29
28
|
|
|
30
29
|
const isOnSelectedLevel = zone.parentId === levelId
|
|
31
30
|
|
|
32
|
-
// Keep group visible (so <Html> labels stay active), hide/show meshes only
|
|
33
|
-
//
|
|
34
|
-
// The editor ZoneSystem handles the selected zone's opacity animation.
|
|
31
|
+
// Keep group visible (so <Html> labels stay active), hide/show meshes only:
|
|
32
|
+
// only the selected zone shows its volume.
|
|
35
33
|
const isSelected = id === zoneId
|
|
36
34
|
// A zone the author hid (sidebar eye) takes the group with it — this
|
|
37
35
|
// per-frame write would otherwise undo the renderer's `visible` prop.
|
|
38
36
|
const nodeVisible = zone.visible !== false
|
|
39
|
-
const shouldShowGeometry =
|
|
40
|
-
nodeVisible &&
|
|
41
|
-
!isCaptureMode &&
|
|
42
|
-
((structureLayer === 'zones' && !!levelId && isOnSelectedLevel) || isSelected)
|
|
37
|
+
const shouldShowGeometry = nodeVisible && !isCaptureMode && isSelected
|
|
43
38
|
if (obj.visible !== nodeVisible) obj.visible = nodeVisible
|
|
44
39
|
obj.traverse((child) => {
|
|
45
40
|
if ((child as Mesh).isMesh) {
|
|
@@ -11,6 +11,7 @@ import { create } from '@react-three/test-renderer'
|
|
|
11
11
|
import { type SaveStatus, useAutoSave } from './use-auto-save'
|
|
12
12
|
|
|
13
13
|
const originalWindow = globalThis.window
|
|
14
|
+
const originalRequestAnimationFrame = globalThis.requestAnimationFrame
|
|
14
15
|
const previousScene = useScene.getState()
|
|
15
16
|
let renderer: Awaited<ReturnType<typeof create>> | undefined
|
|
16
17
|
|
|
@@ -18,6 +19,7 @@ afterEach(async () => {
|
|
|
18
19
|
await renderer?.unmount()
|
|
19
20
|
renderer = undefined
|
|
20
21
|
globalThis.window = originalWindow
|
|
22
|
+
globalThis.requestAnimationFrame = originalRequestAnimationFrame
|
|
21
23
|
useScene.setState(previousScene, true)
|
|
22
24
|
clearSceneHistory()
|
|
23
25
|
})
|
|
@@ -42,11 +44,8 @@ function graph(count: number) {
|
|
|
42
44
|
return { nodes: Object.fromEntries(nodes.map((node) => [node.id, node])), rootNodeIds: [site.id] }
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
guardAgainstSceneWipe?: boolean,
|
|
48
|
-
save?: () => Promise<void>,
|
|
49
|
-
) {
|
|
47
|
+
/** Mounts the hook in its initial state: a scene load not yet completed. */
|
|
48
|
+
async function mountBeforeLoad(guardAgainstSceneWipe?: boolean, save?: () => Promise<void>) {
|
|
50
49
|
globalThis.window = new EventTarget() as unknown as Window & typeof globalThis
|
|
51
50
|
useScene.setState({ ...graph(0), readOnly: false })
|
|
52
51
|
clearSceneHistory()
|
|
@@ -65,10 +64,6 @@ async function mount(
|
|
|
65
64
|
return null
|
|
66
65
|
}
|
|
67
66
|
renderer = await create(<Host guardAgainstSceneWipe={guardAgainstSceneWipe} />)
|
|
68
|
-
useScene.setState(graph(loadedCount))
|
|
69
|
-
clearSceneHistory()
|
|
70
|
-
controls!.isLoadingSceneRef.current = false
|
|
71
|
-
statuses.length = 0
|
|
72
67
|
return {
|
|
73
68
|
writes,
|
|
74
69
|
statuses,
|
|
@@ -78,6 +73,125 @@ async function mount(
|
|
|
78
73
|
}
|
|
79
74
|
}
|
|
80
75
|
|
|
76
|
+
async function mount(
|
|
77
|
+
loadedCount: number,
|
|
78
|
+
guardAgainstSceneWipe?: boolean,
|
|
79
|
+
save?: () => Promise<void>,
|
|
80
|
+
) {
|
|
81
|
+
const mounted = await mountBeforeLoad(guardAgainstSceneWipe, save)
|
|
82
|
+
useScene.setState(graph(loadedCount))
|
|
83
|
+
clearSceneHistory()
|
|
84
|
+
mounted.controls.completeSceneLoad()
|
|
85
|
+
mounted.statuses.length = 0
|
|
86
|
+
return mounted
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The Editor's load sequence: begin, unload, hydrate the store, complete. */
|
|
90
|
+
function loadScene(controls: ReturnType<typeof useAutoSave>, count: number) {
|
|
91
|
+
controls.beginSceneLoad()
|
|
92
|
+
useScene.getState().unloadScene()
|
|
93
|
+
hydrate(count)
|
|
94
|
+
controls.completeSceneLoad()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hydrate(count: number) {
|
|
98
|
+
const { nodes, rootNodeIds } = graph(count)
|
|
99
|
+
useScene.getState().setScene(nodes as never, rootNodeIds as never)
|
|
100
|
+
clearSceneHistory()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function agentAddsColumn() {
|
|
104
|
+
useScene
|
|
105
|
+
.getState()
|
|
106
|
+
.createNode(
|
|
107
|
+
ColumnNode.parse({ id: 'column_agent', parentId: 'level_autosave' }),
|
|
108
|
+
'level_autosave' as never,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A hidden tab: requestAnimationFrame callbacks never run. */
|
|
113
|
+
function hideDocument() {
|
|
114
|
+
const frames: FrameRequestCallback[] = []
|
|
115
|
+
globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => {
|
|
116
|
+
frames.push(callback)
|
|
117
|
+
return frames.length
|
|
118
|
+
}) as typeof requestAnimationFrame
|
|
119
|
+
return frames
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test('a scene loaded in a hidden tab autosaves agent edits without waiting for a frame', async () => {
|
|
123
|
+
hideDocument()
|
|
124
|
+
const { writes, statuses, controls } = await mountBeforeLoad()
|
|
125
|
+
statuses.length = 0
|
|
126
|
+
// Pre-hydration document writes (the host-panel installedPlugins sync on
|
|
127
|
+
// mount) still must not arm a save of the empty store.
|
|
128
|
+
useScene.getState().setInstalledPlugins(['pre-hydration-default'], { explicit: false })
|
|
129
|
+
loadScene(controls, 12)
|
|
130
|
+
expect(statuses).toEqual([])
|
|
131
|
+
|
|
132
|
+
agentAddsColumn()
|
|
133
|
+
await Bun.sleep(1100)
|
|
134
|
+
expect(writes).toEqual([13])
|
|
135
|
+
expect(statuses.at(-1)).toBe('saved')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('opening a scene and leaving it unchanged saves nothing', async () => {
|
|
139
|
+
const { writes, statuses, controls } = await mountBeforeLoad()
|
|
140
|
+
loadScene(controls, 12)
|
|
141
|
+
statuses.length = 0
|
|
142
|
+
// Mount-time writes after the load: systems re-marking nodes, and a plugin
|
|
143
|
+
// that registers late syncing the default plugin set.
|
|
144
|
+
useScene.getState().markDirty('level_autosave' as never)
|
|
145
|
+
useScene.getState().setInstalledPlugins(['late-default'], { explicit: false })
|
|
146
|
+
await Bun.sleep(1100)
|
|
147
|
+
window.dispatchEvent(new Event('pagehide'))
|
|
148
|
+
expect(writes).toEqual([])
|
|
149
|
+
expect(statuses).toEqual([])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('installing a plugin by hand after the load is saved', async () => {
|
|
153
|
+
const { writes, controls } = await mountBeforeLoad()
|
|
154
|
+
loadScene(controls, 12)
|
|
155
|
+
useScene.getState().setInstalledPlugins(['chosen'], { explicit: true })
|
|
156
|
+
await Bun.sleep(1100)
|
|
157
|
+
expect(writes).toEqual([12])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('the exit flush writes an agent edit made right after a hidden load', async () => {
|
|
161
|
+
hideDocument()
|
|
162
|
+
const { writes, statuses, controls } = await mountBeforeLoad()
|
|
163
|
+
loadScene(controls, 12)
|
|
164
|
+
agentAddsColumn()
|
|
165
|
+
window.dispatchEvent(new Event('pagehide'))
|
|
166
|
+
expect(writes).toEqual([13])
|
|
167
|
+
expect(statuses).not.toContain('error')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('the exit flush still skips while a load is in flight', async () => {
|
|
171
|
+
const { writes, controls } = await mount(12)
|
|
172
|
+
agentAddsColumn()
|
|
173
|
+
controls.beginSceneLoad()
|
|
174
|
+
useScene.getState().unloadScene()
|
|
175
|
+
window.dispatchEvent(new Event('pagehide'))
|
|
176
|
+
expect(writes).toEqual([])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('a save that came due mid-load is written once the load completes', async () => {
|
|
180
|
+
const { writes, statuses, controls } = await mount(12)
|
|
181
|
+
agentAddsColumn()
|
|
182
|
+
controls.beginSceneLoad()
|
|
183
|
+
useScene.getState().unloadScene()
|
|
184
|
+
await Bun.sleep(1100)
|
|
185
|
+
expect(writes).toEqual([])
|
|
186
|
+
expect(statuses.at(-1)).toBe('paused')
|
|
187
|
+
|
|
188
|
+
hydrate(12)
|
|
189
|
+
controls.completeSceneLoad()
|
|
190
|
+
await Bun.sleep(0)
|
|
191
|
+
expect(writes).toEqual([12])
|
|
192
|
+
expect(statuses.at(-1)).toBe('saved')
|
|
193
|
+
})
|
|
194
|
+
|
|
81
195
|
test.each([
|
|
82
196
|
'undo',
|
|
83
197
|
'redo',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useScene } from '@pascal-app/core'
|
|
4
|
-
import {
|
|
5
|
-
import { type SceneGraph, saveSceneToLocalStorage } from '../lib/scene'
|
|
4
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
5
|
+
import { getSavedSceneDocument, type SceneGraph, saveSceneToLocalStorage } from '../lib/scene'
|
|
6
6
|
|
|
7
7
|
const AUTOSAVE_DEBOUNCE_MS = 1000
|
|
8
8
|
const STRUCTURAL_NODE_COUNT = 4
|
|
@@ -105,7 +105,8 @@ export function useAutoSave({
|
|
|
105
105
|
onSaveStatusChange,
|
|
106
106
|
isVersionPreviewMode = false,
|
|
107
107
|
}: UseAutoSaveOptions): {
|
|
108
|
-
|
|
108
|
+
beginSceneLoad: () => void
|
|
109
|
+
completeSceneLoad: () => void
|
|
109
110
|
saveNow: () => void
|
|
110
111
|
} {
|
|
111
112
|
const saveTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
|
@@ -169,7 +170,8 @@ export function useAutoSave({
|
|
|
169
170
|
return
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
const { nodes, rootNodeIds, collections, materials, installedPlugins } =
|
|
173
|
+
const { nodes, rootNodeIds, collections, materials, installedPlugins } =
|
|
174
|
+
getSavedSceneDocument()
|
|
173
175
|
const sceneGraph = {
|
|
174
176
|
nodes,
|
|
175
177
|
rootNodeIds,
|
|
@@ -237,18 +239,23 @@ export function useAutoSave({
|
|
|
237
239
|
return
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
// A plugin set the user never chose is the host's defaults, re-derived on
|
|
243
|
+
// every load: a plugin registering after the load (hosted plugins arrive
|
|
244
|
+
// late) syncs it without anyone editing the scene.
|
|
245
|
+
const pluginsEdited =
|
|
246
|
+
state.installedPlugins !== lastInstalledPluginsRef && state.hasExplicitPluginInstallState
|
|
247
|
+
lastInstalledPluginsRef = state.installedPlugins
|
|
240
248
|
const currentNodesSnapshot = JSON.stringify(state.nodes)
|
|
241
249
|
const changed =
|
|
242
250
|
currentNodesSnapshot !== lastNodesSnapshot ||
|
|
243
251
|
state.collections !== lastCollectionsRef ||
|
|
244
252
|
state.materials !== lastMaterialsRef ||
|
|
245
|
-
|
|
253
|
+
pluginsEdited
|
|
246
254
|
if (!changed) return
|
|
247
255
|
|
|
248
256
|
lastNodesSnapshot = currentNodesSnapshot
|
|
249
257
|
lastCollectionsRef = state.collections
|
|
250
258
|
lastMaterialsRef = state.materials
|
|
251
|
-
lastInstalledPluginsRef = state.installedPlugins
|
|
252
259
|
hasDirtyChangesRef.current = true
|
|
253
260
|
onDirtyRef.current?.()
|
|
254
261
|
setSaveStatus('pending')
|
|
@@ -272,7 +279,8 @@ export function useAutoSave({
|
|
|
272
279
|
// would otherwise drop the change entirely. `pagehide` fires in cases
|
|
273
280
|
// (mobile Safari, bfcache) where `beforeunload` does not.
|
|
274
281
|
function flushOnExit() {
|
|
275
|
-
const { nodes, rootNodeIds, collections, materials, installedPlugins } =
|
|
282
|
+
const { nodes, rootNodeIds, collections, materials, installedPlugins } =
|
|
283
|
+
getSavedSceneDocument()
|
|
276
284
|
const currentNodeCount = Object.keys(nodes).length
|
|
277
285
|
const previousNodeCount = storedNodeCount.count
|
|
278
286
|
const decision = decideExitFlush({
|
|
@@ -376,5 +384,23 @@ export function useAutoSave({
|
|
|
376
384
|
executeSaveRef.current?.()
|
|
377
385
|
}, [])
|
|
378
386
|
|
|
379
|
-
|
|
387
|
+
const beginSceneLoad = useCallback(() => {
|
|
388
|
+
isLoadingSceneRef.current = true
|
|
389
|
+
}, [])
|
|
390
|
+
|
|
391
|
+
// Call as soon as the store holds the loaded graph — hydration is data, not
|
|
392
|
+
// a frame. Gating this on requestAnimationFrame kept a tab loaded while
|
|
393
|
+
// hidden (no frames ever run) "loading" indefinitely: every later edit was
|
|
394
|
+
// adopted as the loaded baseline, so nothing autosaved and the exit flush
|
|
395
|
+
// had nothing dirty to write.
|
|
396
|
+
const completeSceneLoad = useCallback(() => {
|
|
397
|
+
isLoadingSceneRef.current = false
|
|
398
|
+
// A save that came due mid-load was deferred; the store now holds real
|
|
399
|
+
// data, so write it rather than leave the status parked on 'paused'.
|
|
400
|
+
if (pendingSaveRef.current && !isSavingRef.current && !saveTimeoutRef.current) {
|
|
401
|
+
executeSaveRef.current?.()
|
|
402
|
+
}
|
|
403
|
+
}, [])
|
|
404
|
+
|
|
405
|
+
return { beginSceneLoad, completeSceneLoad, saveNow }
|
|
380
406
|
}
|
|
@@ -10,20 +10,29 @@ import {
|
|
|
10
10
|
nodeRegistry,
|
|
11
11
|
PipeFittingNode,
|
|
12
12
|
PipeSegmentNode,
|
|
13
|
+
registerNode,
|
|
14
|
+
useInteractive,
|
|
13
15
|
useScene,
|
|
14
16
|
} from '@pascal-app/core'
|
|
17
|
+
import { useSidebarStore } from '../components/ui/primitives/sidebar'
|
|
15
18
|
import { runRedo, runUndo } from '../lib/history'
|
|
16
19
|
import { meshEditScope } from '../lib/interaction/scope'
|
|
20
|
+
import { setSidebarTabIds } from '../lib/sidebar-panel'
|
|
17
21
|
import useEditor from '../store/use-editor'
|
|
18
22
|
import useInteractionScope from '../store/use-interaction-scope'
|
|
19
23
|
import {
|
|
24
|
+
armPaintFromShortcut,
|
|
25
|
+
armWallToolFromShortcut,
|
|
20
26
|
blocksSnappingShortcut,
|
|
21
27
|
canCycleSnappingModeShortcut,
|
|
22
28
|
canRunGlobalRotationShortcut,
|
|
23
29
|
isToolOwnedCanopyForm,
|
|
24
30
|
isToolOwnedRotation,
|
|
25
31
|
markToolCancelConsumed,
|
|
32
|
+
openItemCatalogFromShortcut,
|
|
26
33
|
runHistoryShortcut,
|
|
34
|
+
runNodeInteraction,
|
|
35
|
+
switchPhaseFromShortcut,
|
|
27
36
|
} from './use-keyboard'
|
|
28
37
|
|
|
29
38
|
type RafFn = (callback: (time: number) => void) => number
|
|
@@ -106,11 +115,10 @@ describe('rotation shortcut ownership', () => {
|
|
|
106
115
|
expect(isToolOwnedRotation()).toBe(true)
|
|
107
116
|
})
|
|
108
117
|
|
|
109
|
-
test('
|
|
118
|
+
test('does not reserve R for the wall tool: its shape is picked in the Build panel', () => {
|
|
110
119
|
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
111
120
|
|
|
112
|
-
expect(isToolOwnedRotation(
|
|
113
|
-
expect(isToolOwnedRotation('t')).toBe(false)
|
|
121
|
+
expect(isToolOwnedRotation()).toBe(false)
|
|
114
122
|
})
|
|
115
123
|
|
|
116
124
|
test('leaves R and T to a moving lean-to extension', () => {
|
|
@@ -249,3 +257,170 @@ describe('history while drawing distribution runs', () => {
|
|
|
249
257
|
})
|
|
250
258
|
}
|
|
251
259
|
})
|
|
260
|
+
|
|
261
|
+
describe('E on a single selected node', () => {
|
|
262
|
+
function registerKind(overrides: Record<string, unknown>) {
|
|
263
|
+
const kind = `test:e-${crypto.randomUUID()}`
|
|
264
|
+
registerNode({
|
|
265
|
+
kind,
|
|
266
|
+
schemaVersion: 1,
|
|
267
|
+
category: 'furnish',
|
|
268
|
+
defaults: () => ({}),
|
|
269
|
+
capabilities: {},
|
|
270
|
+
...overrides,
|
|
271
|
+
} as never)
|
|
272
|
+
return { object: 'node', id: `${kind}_1`, type: kind } as unknown as AnyNode
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const switchMechanism = {
|
|
276
|
+
has: () => true,
|
|
277
|
+
isOn: (node: AnyNode, state: ReturnType<typeof useInteractive.getState>) =>
|
|
278
|
+
Boolean(state.mechanisms[node.id]),
|
|
279
|
+
set: (node: AnyNode, on: boolean) => useInteractive.getState().setMechanism(node.id, on),
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
test('runs the mechanism of a kind that declares only the capability', () => {
|
|
283
|
+
const node = registerKind({ capabilities: { mechanism: switchMechanism } })
|
|
284
|
+
try {
|
|
285
|
+
expect(runNodeInteraction(node)).toBe(true)
|
|
286
|
+
expect(useInteractive.getState().mechanisms[node.id]).toBe(true)
|
|
287
|
+
expect(runNodeInteraction(node)).toBe(true)
|
|
288
|
+
expect(useInteractive.getState().mechanisms[node.id]).toBe(false)
|
|
289
|
+
} finally {
|
|
290
|
+
useInteractive.getState().removeMechanism(node.id)
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test("prefers the kind's own E action over its mechanism", () => {
|
|
295
|
+
let ran = 0
|
|
296
|
+
const node = registerKind({
|
|
297
|
+
capabilities: { mechanism: switchMechanism },
|
|
298
|
+
keyboardActions: { e: { appliesTo: () => true, run: () => (ran += 1) } },
|
|
299
|
+
})
|
|
300
|
+
expect(runNodeInteraction(node)).toBe(true)
|
|
301
|
+
expect(ran).toBe(1)
|
|
302
|
+
expect(useInteractive.getState().mechanisms[node.id]).toBeUndefined()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('leaves E to the door and window fallbacks when nothing applies', () => {
|
|
306
|
+
const node = registerKind({
|
|
307
|
+
capabilities: { mechanism: { ...switchMechanism, has: () => false } },
|
|
308
|
+
})
|
|
309
|
+
expect(runNodeInteraction(node)).toBe(false)
|
|
310
|
+
expect(runNodeInteraction(registerKind({}))).toBe(false)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('B and P open their panel', () => {
|
|
315
|
+
afterEach(() => {
|
|
316
|
+
setSidebarTabIds([])
|
|
317
|
+
useSidebarStore.getState().setIsCollapsed(false)
|
|
318
|
+
useEditor.getState().setContinuation('wall', 'room')
|
|
319
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('1 goes to site and 2 into the building; 3 is no longer a shortcut', () => {
|
|
323
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
324
|
+
expect(switchPhaseFromShortcut('1')).toBe(true)
|
|
325
|
+
expect(useEditor.getState().phase).toBe('site')
|
|
326
|
+
expect(useEditor.getState().mode).toBe('select')
|
|
327
|
+
expect(switchPhaseFromShortcut('2')).toBe(true)
|
|
328
|
+
expect(useEditor.getState().phase).toBe('building')
|
|
329
|
+
expect(switchPhaseFromShortcut('3')).toBe(false)
|
|
330
|
+
expect(useEditor.getState().phase).toBe('building')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
test('F, B and P arm their tool and keep the building', () => {
|
|
334
|
+
setSidebarTabIds(['site', 'build', 'paint', 'items'])
|
|
335
|
+
useEditor.getState().setPhase('building')
|
|
336
|
+
for (const [arm, mode, tool] of [
|
|
337
|
+
[openItemCatalogFromShortcut, 'build', 'item'],
|
|
338
|
+
[armWallToolFromShortcut, 'build', 'wall'],
|
|
339
|
+
[armPaintFromShortcut, 'material-paint', null],
|
|
340
|
+
] as const) {
|
|
341
|
+
arm()
|
|
342
|
+
expect(useEditor.getState().phase).toBe('building')
|
|
343
|
+
expect(useEditor.getState().mode).toBe(mode)
|
|
344
|
+
expect(useEditor.getState().tool).toBe(tool)
|
|
345
|
+
}
|
|
346
|
+
openItemCatalogFromShortcut()
|
|
347
|
+
expect(useEditor.getState().activeSidebarPanel).toBe('items')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test('F, B and P also keep Site when arming their tool', () => {
|
|
351
|
+
setSidebarTabIds(['site', 'build', 'paint', 'items'])
|
|
352
|
+
for (const arm of [
|
|
353
|
+
openItemCatalogFromShortcut,
|
|
354
|
+
armWallToolFromShortcut,
|
|
355
|
+
armPaintFromShortcut,
|
|
356
|
+
]) {
|
|
357
|
+
useEditor.getState().setPhase('site')
|
|
358
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
359
|
+
arm()
|
|
360
|
+
expect(useEditor.getState().phase).toBe('site')
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
test('F opens a collapsed catalog sidebar', () => {
|
|
365
|
+
setSidebarTabIds(['site', 'build', 'items'])
|
|
366
|
+
useSidebarStore.getState().setIsCollapsed(true)
|
|
367
|
+
openItemCatalogFromShortcut()
|
|
368
|
+
expect(useEditor.getState().activeSidebarPanel).toBe('items')
|
|
369
|
+
expect(useSidebarStore.getState().isCollapsed).toBe(false)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test('B arms the wall tool on Rectangle and shows the Build panel', () => {
|
|
373
|
+
setSidebarTabIds(['site', 'build', 'paint', 'items'])
|
|
374
|
+
useEditor.getState().setActiveSidebarPanel('items')
|
|
375
|
+
useSidebarStore.getState().setIsCollapsed(true)
|
|
376
|
+
useEditor.getState().setContinuation('wall', 'single')
|
|
377
|
+
|
|
378
|
+
armWallToolFromShortcut()
|
|
379
|
+
|
|
380
|
+
const editor = useEditor.getState()
|
|
381
|
+
expect(editor.mode).toBe('build')
|
|
382
|
+
expect(editor.tool).toBe('wall')
|
|
383
|
+
expect(editor.getContinuation('wall')).toBe('rectangle')
|
|
384
|
+
expect(editor.activeSidebarPanel).toBe('build')
|
|
385
|
+
expect(useSidebarStore.getState().isCollapsed).toBe(false)
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// A host may drop the armed tool when the panel changes (the community does,
|
|
389
|
+
// with any placement in flight), so B and F open their panel first.
|
|
390
|
+
test('B and F keep their tool from another panel where the host disarms on a panel change', () => {
|
|
391
|
+
setSidebarTabIds(['build', 'paint', 'items', 'ai'])
|
|
392
|
+
const unsubscribe = useEditor.subscribe((state, prev) => {
|
|
393
|
+
if (state.activeSidebarPanel !== prev.activeSidebarPanel) {
|
|
394
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
try {
|
|
398
|
+
for (const [arm, from, panel, tool] of [
|
|
399
|
+
[armWallToolFromShortcut, 'ai', 'build', 'wall'],
|
|
400
|
+
[openItemCatalogFromShortcut, 'paint', 'items', 'item'],
|
|
401
|
+
] as const) {
|
|
402
|
+
useEditor.getState().setActiveSidebarPanel(from)
|
|
403
|
+
arm()
|
|
404
|
+
const editor = useEditor.getState()
|
|
405
|
+
expect(editor.activeSidebarPanel).toBe(panel)
|
|
406
|
+
expect(editor.mode).toBe('build')
|
|
407
|
+
expect(editor.tool).toBe(tool)
|
|
408
|
+
}
|
|
409
|
+
} finally {
|
|
410
|
+
unsubscribe()
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
test('P arms paint and shows the Paint panel, or Build where the host has none', () => {
|
|
415
|
+
setSidebarTabIds(['build', 'paint', 'items'])
|
|
416
|
+
useEditor.getState().setActiveSidebarPanel('items')
|
|
417
|
+
armPaintFromShortcut()
|
|
418
|
+
expect(useEditor.getState().mode).toBe('material-paint')
|
|
419
|
+
expect(useEditor.getState().activeSidebarPanel).toBe('paint')
|
|
420
|
+
|
|
421
|
+
setSidebarTabIds(['site', 'build', 'items'])
|
|
422
|
+
useEditor.getState().setActiveSidebarPanel('items')
|
|
423
|
+
armPaintFromShortcut()
|
|
424
|
+
expect(useEditor.getState().activeSidebarPanel).toBe('build')
|
|
425
|
+
})
|
|
426
|
+
})
|