@pascal-app/editor 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +221 -128
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +132 -0
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +82 -23
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +7 -19
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +191 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +87 -0
- package/src/lib/scene.ts +39 -45
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import { create } from 'zustand'
|
|
4
|
+
import { hasLiveGestures } from '../lib/gesture-lifecycle'
|
|
5
|
+
import { removeWallRegion } from '../lib/paint-regions'
|
|
6
|
+
import { sfxEmitter } from '../lib/sfx-bus'
|
|
7
|
+
import { isTypingTarget } from '../lib/typing-target'
|
|
8
|
+
import useDeleteConfirmation from './use-delete-confirmation'
|
|
9
|
+
import useInteractionScope from './use-interaction-scope'
|
|
10
|
+
|
|
11
|
+
// The paint region the user last picked on the selected wall (a handle, an
|
|
12
|
+
// outline or a panel row). While it is set, Delete removes that region instead
|
|
13
|
+
// of the wall. Cleared whenever the wall selection changes.
|
|
14
|
+
//
|
|
15
|
+
// `stale` remembers an active region that vanished under the user (undo, another
|
|
16
|
+
// edit) while its wall stayed selected: the next Delete is swallowed rather than
|
|
17
|
+
// deleting the wall the user was not aiming at.
|
|
18
|
+
|
|
19
|
+
export type ActiveWallRegion = { wallId: string; regionId: string }
|
|
20
|
+
|
|
21
|
+
type WallRegionSelectionState = {
|
|
22
|
+
active: ActiveWallRegion | null
|
|
23
|
+
stale: ActiveWallRegion | null
|
|
24
|
+
setActive: (active: ActiveWallRegion | null) => void
|
|
25
|
+
setStale: (stale: ActiveWallRegion | null) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useWallRegionSelection = create<WallRegionSelectionState>((set) => ({
|
|
29
|
+
active: null,
|
|
30
|
+
stale: null,
|
|
31
|
+
setActive: (active) =>
|
|
32
|
+
set((state) =>
|
|
33
|
+
state.active?.wallId === active?.wallId && state.active?.regionId === active?.regionId
|
|
34
|
+
? state
|
|
35
|
+
: active
|
|
36
|
+
? { active, stale: null }
|
|
37
|
+
: { active },
|
|
38
|
+
),
|
|
39
|
+
setStale: (stale) => set((state) => (state.stale === stale ? state : { stale })),
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
function isSoleSelection(wallId: string, selectedIds: readonly string[]) {
|
|
43
|
+
return selectedIds.length === 1 && selectedIds[0] === wallId
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function regionExists(wallId: string, regionId: string) {
|
|
47
|
+
const wall = useScene.getState().nodes[wallId as AnyNodeId]
|
|
48
|
+
if (wall?.type !== 'wall') return false
|
|
49
|
+
return wall.faceRegions?.some((region) => region.id === regionId) ?? false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The active region, only while its wall is the sole selection and the region still exists. */
|
|
53
|
+
export function resolveActiveWallRegion(): ActiveWallRegion | null {
|
|
54
|
+
const active = useWallRegionSelection.getState().active
|
|
55
|
+
if (!active) return null
|
|
56
|
+
if (!isSoleSelection(active.wallId, useViewer.getState().selection.selectedIds)) return null
|
|
57
|
+
return regionExists(active.wallId, active.regionId) ? active : null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveStaleWallRegion(): ActiveWallRegion | null {
|
|
61
|
+
const stale = useWallRegionSelection.getState().stale
|
|
62
|
+
if (!stale) return null
|
|
63
|
+
return isSoleSelection(stale.wallId, useViewer.getState().selection.selectedIds) ? stale : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Clears the active (and stale) region unless the selection is still exactly its wall. */
|
|
67
|
+
export function syncWallRegionSelection(selectedIds: readonly string[]) {
|
|
68
|
+
const { active, stale, setActive, setStale } = useWallRegionSelection.getState()
|
|
69
|
+
if (active && !isSoleSelection(active.wallId, selectedIds)) setActive(null)
|
|
70
|
+
if (stale && !isSoleSelection(stale.wallId, selectedIds)) setStale(null)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Drops the active region once it no longer exists in the scene. */
|
|
74
|
+
export function syncWallRegionWithScene() {
|
|
75
|
+
const { active, setActive, setStale } = useWallRegionSelection.getState()
|
|
76
|
+
if (!active || regionExists(active.wallId, active.regionId)) return
|
|
77
|
+
setActive(null)
|
|
78
|
+
const wallStillThere = useScene.getState().nodes[active.wallId as AnyNodeId]?.type === 'wall'
|
|
79
|
+
const selectedIds = useViewer.getState().selection.selectedIds
|
|
80
|
+
if (wallStillThere && isSoleSelection(active.wallId, selectedIds)) setStale(active)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Keeps the active region in step with scene edits (undo, remote edits). Returns the unsubscribe. */
|
|
84
|
+
export function installWallRegionSceneSync() {
|
|
85
|
+
return useScene.subscribe((state, previous) => {
|
|
86
|
+
if (state.nodes !== previous.nodes) syncWallRegionWithScene()
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Delete belongs to the region only when focus is on the scene or the wall's
|
|
91
|
+
// region list; a focused toolbar button or panel control keeps its own key.
|
|
92
|
+
function isSceneOrRegionListTarget(target: EventTarget | null) {
|
|
93
|
+
if (!target) return true
|
|
94
|
+
if (
|
|
95
|
+
typeof document !== 'undefined' &&
|
|
96
|
+
(target === document || target === document.body || target === document.documentElement)
|
|
97
|
+
) {
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
const element = target as { tagName?: string; closest?: (selector: string) => unknown }
|
|
101
|
+
if (element.tagName?.toUpperCase() === 'CANVAS') return true
|
|
102
|
+
return !!element.closest?.('[data-wall-region-list]')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function consume(event: KeyEventLike) {
|
|
106
|
+
event.preventDefault()
|
|
107
|
+
event.stopPropagation()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type KeyEventLike = Pick<KeyboardEvent, 'key' | 'target' | 'preventDefault' | 'stopPropagation'>
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Delete / Backspace with an active region removes that region (one undo step)
|
|
114
|
+
* and consumes the key so the global delete never sees it. Mid-gesture, or
|
|
115
|
+
* right after the active region vanished, the key is swallowed so it cannot
|
|
116
|
+
* fall through to deleting the wall. Returns whether the key was consumed.
|
|
117
|
+
*/
|
|
118
|
+
export function handleWallRegionDeleteKey(event: KeyEventLike): boolean {
|
|
119
|
+
if (event.key !== 'Delete' && event.key !== 'Backspace') return false
|
|
120
|
+
if (isTypingTarget(event.target)) return false
|
|
121
|
+
if (!isSceneOrRegionListTarget(event.target)) return false
|
|
122
|
+
if (useDeleteConfirmation.getState().request) return false
|
|
123
|
+
const active = resolveActiveWallRegion()
|
|
124
|
+
const stale = active ? null : resolveStaleWallRegion()
|
|
125
|
+
if (!(active || stale)) return false
|
|
126
|
+
|
|
127
|
+
consume(event)
|
|
128
|
+
const busy = useInteractionScope.getState().scope.kind !== 'idle' || hasLiveGestures()
|
|
129
|
+
if (busy) return true
|
|
130
|
+
if (stale) {
|
|
131
|
+
useWallRegionSelection.getState().setStale(null)
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
if (!active) return true
|
|
135
|
+
// Cleared first so the scene sync does not mistake this removal for a vanished region.
|
|
136
|
+
useWallRegionSelection.getState().setActive(null)
|
|
137
|
+
if (removeWallRegion(active.wallId, active.regionId)) sfxEmitter.emit('sfx:structure-delete')
|
|
138
|
+
return true
|
|
139
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { availableViews, editorHostViewRegistry, registerEditorHostView } from '../lib/editor-views'
|
|
3
|
+
import useEditor, { normalizePersistedEditorUiState } from './use-editor'
|
|
4
|
+
import {
|
|
5
|
+
activeViewLayout,
|
|
6
|
+
DEFAULT_VIEW_LAYOUT,
|
|
7
|
+
isViewVisible,
|
|
8
|
+
sceneLayout,
|
|
9
|
+
VIEW_2D,
|
|
10
|
+
VIEW_3D,
|
|
11
|
+
visibleScene,
|
|
12
|
+
} from './view-layout'
|
|
13
|
+
|
|
14
|
+
function layout() {
|
|
15
|
+
return activeViewLayout(useEditor.getState())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
useEditor.getState().setCaptureMode(false)
|
|
20
|
+
useEditor.getState().setFirstPersonMode(false)
|
|
21
|
+
useEditor.getState().setWorkspaceMode('edit')
|
|
22
|
+
useEditor.setState({ viewLayouts: {} })
|
|
23
|
+
editorHostViewRegistry.reset()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('the stage layout', () => {
|
|
27
|
+
test('a view sits in one pane: picking the other pane’s view swaps them', () => {
|
|
28
|
+
useEditor.getState().setViewLayout(sceneLayout('split'))
|
|
29
|
+
useEditor.getState().setPaneView(0, VIEW_3D)
|
|
30
|
+
expect(layout().panes).toEqual([VIEW_3D, VIEW_2D])
|
|
31
|
+
expect(layout().split).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('a view opened beside the scene splits next to the focused pane', () => {
|
|
35
|
+
useEditor.getState().showView('gallery', { beside: true })
|
|
36
|
+
expect(layout()).toMatchObject({ split: true, panes: [VIEW_3D, 'gallery'], focus: 1 })
|
|
37
|
+
expect(visibleScene(useEditor.getState())).toBe('3d')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('opening a view already on screen only focuses it', () => {
|
|
41
|
+
useEditor.getState().setViewLayout(sceneLayout('split'))
|
|
42
|
+
useEditor.getState().showView(VIEW_3D)
|
|
43
|
+
expect(layout()).toMatchObject({ panes: [VIEW_2D, VIEW_3D], focus: 1, split: true })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('splitting from 3D brings the plan in on the left, as the split always did', () => {
|
|
47
|
+
useEditor.getState().toggleSplit()
|
|
48
|
+
expect(layout()).toMatchObject({ split: true, panes: [VIEW_2D, VIEW_3D], focus: 1 })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('closing the split keeps the focused pane', () => {
|
|
52
|
+
useEditor.getState().setViewLayout(sceneLayout('split'))
|
|
53
|
+
useEditor.getState().focusViewPane(1)
|
|
54
|
+
useEditor.getState().toggleSplit()
|
|
55
|
+
expect(layout()).toMatchObject({ split: false, panes: [VIEW_3D, VIEW_2D] })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('closing a view collapses its split, or brings a lone pane back to 3D', () => {
|
|
59
|
+
useEditor.getState().showView('gallery', { beside: true })
|
|
60
|
+
useEditor.getState().closeView('gallery')
|
|
61
|
+
expect(layout()).toMatchObject({ split: false, panes: [VIEW_3D, VIEW_2D] })
|
|
62
|
+
useEditor.getState().setViewLayout(sceneLayout('2d'))
|
|
63
|
+
useEditor.getState().showView('gallery')
|
|
64
|
+
useEditor.getState().closeView('gallery')
|
|
65
|
+
expect(visibleScene(useEditor.getState())).toBe('3d')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('a plan beside another view owns the scene like a plan alone', () => {
|
|
69
|
+
useEditor.getState().setViewLayout(sceneLayout('2d'))
|
|
70
|
+
useEditor.getState().showView('gallery', { beside: true })
|
|
71
|
+
expect(visibleScene(useEditor.getState())).toBe('2d')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('each workspace keeps its own layout', () => {
|
|
75
|
+
useEditor.getState().setViewLayout(sceneLayout('split'))
|
|
76
|
+
useEditor.getState().setWorkspaceMode('studio')
|
|
77
|
+
expect(layout()).toEqual(DEFAULT_VIEW_LAYOUT)
|
|
78
|
+
useEditor.getState().showView('gallery')
|
|
79
|
+
useEditor.getState().setWorkspaceMode('edit')
|
|
80
|
+
expect(visibleScene(useEditor.getState())).toBe('split')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('capture frames in 3D alone and gives the layout back untouched', () => {
|
|
84
|
+
useEditor.getState().setViewLayout(sceneLayout('2d'))
|
|
85
|
+
useEditor.getState().setCaptureMode(true)
|
|
86
|
+
expect(visibleScene(useEditor.getState())).toBe('3d')
|
|
87
|
+
useEditor.getState().setCaptureMode(false)
|
|
88
|
+
expect(visibleScene(useEditor.getState())).toBe('2d')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('the deprecated viewMode for plugins written before views', () => {
|
|
93
|
+
test('mirrors the scene on screen and sets it', () => {
|
|
94
|
+
useEditor.getState().setViewMode('split')
|
|
95
|
+
expect(visibleScene(useEditor.getState())).toBe('split')
|
|
96
|
+
expect(useEditor.getState().viewMode).toBe('split')
|
|
97
|
+
useEditor.getState().showView('gallery')
|
|
98
|
+
expect(useEditor.getState().viewMode).toBe('3d')
|
|
99
|
+
useEditor.getState().setCaptureMode(true)
|
|
100
|
+
expect(useEditor.getState().viewMode).toBe('3d')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('layouts saved before views', () => {
|
|
105
|
+
test.each([
|
|
106
|
+
['3d', '3d'],
|
|
107
|
+
['2d', '2d'],
|
|
108
|
+
['split', 'split'],
|
|
109
|
+
] as const)('a stored %s view opens as %s, the plan on the left', (viewMode, scene) => {
|
|
110
|
+
const state = normalizePersistedEditorUiState({ viewMode, floorplanPaneRatio: 0.4 } as never)
|
|
111
|
+
const restored = state.viewLayouts.edit
|
|
112
|
+
expect(restored).toBeDefined()
|
|
113
|
+
if (!restored) return
|
|
114
|
+
expect(
|
|
115
|
+
visibleScene({
|
|
116
|
+
viewLayouts: state.viewLayouts,
|
|
117
|
+
workspaceMode: 'edit',
|
|
118
|
+
isCaptureMode: false,
|
|
119
|
+
isFirstPersonMode: false,
|
|
120
|
+
}),
|
|
121
|
+
).toBe(scene)
|
|
122
|
+
if (scene !== '3d') expect(restored).toMatchObject({ panes: [VIEW_2D, VIEW_3D], ratio: 0.4 })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('an open floorplan from before view modes opens split', () => {
|
|
126
|
+
const state = normalizePersistedEditorUiState({ isFloorplanOpen: true } as never)
|
|
127
|
+
expect(state.viewLayouts.edit).toMatchObject({ split: true })
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('registered views', () => {
|
|
132
|
+
test('a plugin view is offered only where its plugin is installed and its workspace matches', () => {
|
|
133
|
+
registerEditorHostView({
|
|
134
|
+
id: 'acme:pets',
|
|
135
|
+
label: 'Pets',
|
|
136
|
+
icon: { kind: 'iconify', name: 'lucide:cat' },
|
|
137
|
+
component: async () => ({ default: () => null }),
|
|
138
|
+
pluginId: 'acme',
|
|
139
|
+
})
|
|
140
|
+
registerEditorHostView({
|
|
141
|
+
id: 'acme:habitat',
|
|
142
|
+
label: 'Habitat',
|
|
143
|
+
icon: { kind: 'iconify', name: 'lucide:house' },
|
|
144
|
+
component: async () => ({ default: () => null }),
|
|
145
|
+
pluginId: 'acme',
|
|
146
|
+
workspaces: ['studio'],
|
|
147
|
+
})
|
|
148
|
+
const ids = (workspaceMode: string, installedPlugins: string[]) =>
|
|
149
|
+
availableViews({
|
|
150
|
+
registered: editorHostViewRegistry.getSnapshot(),
|
|
151
|
+
workspaceMode,
|
|
152
|
+
installedPlugins,
|
|
153
|
+
}).map((view) => view.id)
|
|
154
|
+
expect(ids('edit', [])).toEqual([VIEW_3D, VIEW_2D])
|
|
155
|
+
expect(ids('edit', ['acme'])).toEqual([VIEW_3D, VIEW_2D, 'acme:pets'])
|
|
156
|
+
expect(ids('studio', ['acme'])).toEqual([VIEW_3D, VIEW_2D, 'acme:habitat'])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('the scene views cannot be replaced', () => {
|
|
160
|
+
expect(() =>
|
|
161
|
+
registerEditorHostView({
|
|
162
|
+
id: VIEW_3D,
|
|
163
|
+
label: '3D',
|
|
164
|
+
icon: { kind: 'iconify', name: 'lucide:box' },
|
|
165
|
+
component: async () => ({ default: () => null }),
|
|
166
|
+
}),
|
|
167
|
+
).toThrow()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('a layout naming a view that is gone keeps it, so the choice returns with the view', () => {
|
|
171
|
+
useEditor.getState().showView('acme:pets')
|
|
172
|
+
expect(isViewVisible(useEditor.getState(), 'acme:pets')).toBe(true)
|
|
173
|
+
expect(isViewVisible(useEditor.getState(), VIEW_3D)).toBe(false)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stage is one or two panes, each showing one view: the 3D scene, the 2D
|
|
3
|
+
* plan, or a view a host or plugin registered (`registerEditorHostView`). A view
|
|
4
|
+
* sits in at most one pane — the 3D canvas owns a single WebGL context and the
|
|
5
|
+
* plan a single interaction session — so every write keeps the panes distinct.
|
|
6
|
+
*
|
|
7
|
+
* Each workspace keeps its own layout, so Studio can sit on 3D | Gallery while
|
|
8
|
+
* Edit stays on 2D | 3D.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const VIEW_3D = '3d'
|
|
12
|
+
export const VIEW_2D = '2d'
|
|
13
|
+
|
|
14
|
+
export type ViewPaneIndex = 0 | 1
|
|
15
|
+
|
|
16
|
+
export type ViewLayout = {
|
|
17
|
+
split: boolean
|
|
18
|
+
/** `panes[1]` is what split opens beside `panes[0]`, also while unsplit. */
|
|
19
|
+
panes: readonly [string, string]
|
|
20
|
+
/** Width of pane 0 as a fraction of the stage. */
|
|
21
|
+
ratio: number
|
|
22
|
+
focus: ViewPaneIndex
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const MIN_VIEW_PANE_RATIO = 0.15
|
|
26
|
+
export const MAX_VIEW_PANE_RATIO = 0.85
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_VIEW_LAYOUT: ViewLayout = {
|
|
29
|
+
split: false,
|
|
30
|
+
panes: [VIEW_3D, VIEW_2D],
|
|
31
|
+
ratio: 0.5,
|
|
32
|
+
focus: 0,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Capture and first person frame through the 3D camera, whatever the layout says. */
|
|
36
|
+
const SINGLE_3D_LAYOUT: ViewLayout = DEFAULT_VIEW_LAYOUT
|
|
37
|
+
|
|
38
|
+
export function clampViewPaneRatio(value: unknown): number {
|
|
39
|
+
if (!(typeof value === 'number' && Number.isFinite(value))) return DEFAULT_VIEW_LAYOUT.ratio
|
|
40
|
+
return Math.min(MAX_VIEW_PANE_RATIO, Math.max(MIN_VIEW_PANE_RATIO, value))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeViewLayout(value: unknown): ViewLayout {
|
|
44
|
+
const candidate = value as Partial<ViewLayout> | null | undefined
|
|
45
|
+
const panes = candidate?.panes
|
|
46
|
+
const first = typeof panes?.[0] === 'string' && panes[0] ? panes[0] : VIEW_3D
|
|
47
|
+
let second = typeof panes?.[1] === 'string' && panes[1] ? panes[1] : VIEW_2D
|
|
48
|
+
if (second === first) second = first === VIEW_3D ? VIEW_2D : VIEW_3D
|
|
49
|
+
const split = candidate?.split === true
|
|
50
|
+
return {
|
|
51
|
+
split,
|
|
52
|
+
panes: [first, second],
|
|
53
|
+
ratio: clampViewPaneRatio(candidate?.ratio),
|
|
54
|
+
focus: split && candidate?.focus === 1 ? 1 : 0,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function visibleViews(layout: ViewLayout): readonly string[] {
|
|
59
|
+
return layout.split ? layout.panes : [layout.panes[0]]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function paneOfView(layout: ViewLayout, viewId: string): ViewPaneIndex | null {
|
|
63
|
+
if (layout.panes[0] === viewId) return 0
|
|
64
|
+
if (layout.split && layout.panes[1] === viewId) return 1
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Put `viewId` in `pane`; if the other pane holds it, the two trade places. */
|
|
69
|
+
export function withPaneView(layout: ViewLayout, pane: ViewPaneIndex, viewId: string): ViewLayout {
|
|
70
|
+
const target: ViewPaneIndex = layout.split ? pane : 0
|
|
71
|
+
const other: ViewPaneIndex = target === 0 ? 1 : 0
|
|
72
|
+
const panes: [string, string] = [layout.panes[0], layout.panes[1]]
|
|
73
|
+
if (panes[target] === viewId) return { ...layout, focus: target }
|
|
74
|
+
if (panes[other] === viewId) panes[other] = panes[target]
|
|
75
|
+
panes[target] = viewId
|
|
76
|
+
return { ...layout, panes, focus: target }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Show `viewId`. A view already on screen just takes focus; otherwise it
|
|
81
|
+
* replaces the focused pane, or with `beside` opens next to it.
|
|
82
|
+
*/
|
|
83
|
+
export function withView(
|
|
84
|
+
layout: ViewLayout,
|
|
85
|
+
viewId: string,
|
|
86
|
+
options: { beside?: boolean } = {},
|
|
87
|
+
): ViewLayout {
|
|
88
|
+
const pane = paneOfView(layout, viewId)
|
|
89
|
+
if (pane !== null) return { ...layout, focus: pane }
|
|
90
|
+
if (!options.beside) return withPaneView(layout, layout.focus, viewId)
|
|
91
|
+
if (!layout.split) return withPaneView({ ...layout, split: true }, 1, viewId)
|
|
92
|
+
return withPaneView(layout, layout.focus === 0 ? 1 : 0, viewId)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Opening the split from 3D brings the plan in on the left, where the split has
|
|
97
|
+
* always put it, with 3D keeping focus. Closing a split keeps the focused pane.
|
|
98
|
+
*/
|
|
99
|
+
export function withSplitToggled(layout: ViewLayout): ViewLayout {
|
|
100
|
+
if (!layout.split) {
|
|
101
|
+
if (layout.panes[0] === VIEW_3D && layout.panes[1] === VIEW_2D) {
|
|
102
|
+
return { ...layout, split: true, panes: [VIEW_2D, VIEW_3D], focus: 1 }
|
|
103
|
+
}
|
|
104
|
+
return { ...layout, split: true }
|
|
105
|
+
}
|
|
106
|
+
const kept = layout.focus === 1 ? swapped(layout) : layout
|
|
107
|
+
return { ...kept, split: false, focus: 0 }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Close a view: a split collapses to the other pane; a single pane goes back to
|
|
112
|
+
* the scene.
|
|
113
|
+
*/
|
|
114
|
+
export function withoutView(layout: ViewLayout, viewId: string): ViewLayout {
|
|
115
|
+
const pane = paneOfView(layout, viewId)
|
|
116
|
+
if (pane === null) return layout
|
|
117
|
+
const kept = layout.split ? layout.panes[pane === 0 ? 1 : 0] : VIEW_3D
|
|
118
|
+
return { ...layout, split: false, panes: [kept, kept === VIEW_3D ? VIEW_2D : VIEW_3D], focus: 0 }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function swapped(layout: ViewLayout): ViewLayout {
|
|
122
|
+
return {
|
|
123
|
+
...layout,
|
|
124
|
+
panes: [layout.panes[1], layout.panes[0]],
|
|
125
|
+
ratio: 1 - layout.ratio,
|
|
126
|
+
focus: layout.focus === 0 ? 1 : 0,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type ViewLayoutState = {
|
|
131
|
+
viewLayouts: Readonly<Record<string, ViewLayout>>
|
|
132
|
+
workspaceMode: string
|
|
133
|
+
isCaptureMode: boolean
|
|
134
|
+
isFirstPersonMode: boolean
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function storedViewLayout(state: Pick<ViewLayoutState, 'viewLayouts' | 'workspaceMode'>) {
|
|
138
|
+
return state.viewLayouts[state.workspaceMode] ?? DEFAULT_VIEW_LAYOUT
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The layout on screen: the workspace's own, unless capture or first person needs 3D alone. */
|
|
142
|
+
export function activeViewLayout(state: ViewLayoutState): ViewLayout {
|
|
143
|
+
if (state.isCaptureMode || state.isFirstPersonMode) return SINGLE_3D_LAYOUT
|
|
144
|
+
return storedViewLayout(state)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isViewVisible(state: ViewLayoutState, viewId: string): boolean {
|
|
148
|
+
return paneOfView(activeViewLayout(state), viewId) !== null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type VisibleScene = '3d' | '2d' | 'split'
|
|
152
|
+
|
|
153
|
+
/** A layout showing just the scene: 3D, the plan, or the plan beside 3D (the split as it always was). */
|
|
154
|
+
export function sceneLayout(scene: VisibleScene, ratio = DEFAULT_VIEW_LAYOUT.ratio): ViewLayout {
|
|
155
|
+
if (scene === '3d') return DEFAULT_VIEW_LAYOUT
|
|
156
|
+
return {
|
|
157
|
+
split: scene === 'split',
|
|
158
|
+
panes: [VIEW_2D, VIEW_3D],
|
|
159
|
+
ratio: clampViewPaneRatio(ratio),
|
|
160
|
+
focus: 0,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Which projections of the scene are on screen — 3D alone, the plan alone, or
|
|
166
|
+
* both side by side — or `null` while the panes show other views only. The
|
|
167
|
+
* code that hands input between the canvas and the plan reads this: a plan
|
|
168
|
+
* beside a gallery owns input exactly as a plan alone does.
|
|
169
|
+
*/
|
|
170
|
+
export function visibleScene(state: ViewLayoutState): VisibleScene | null {
|
|
171
|
+
const layout = activeViewLayout(state)
|
|
172
|
+
const has3d = paneOfView(layout, VIEW_3D) !== null
|
|
173
|
+
const has2d = paneOfView(layout, VIEW_2D) !== null
|
|
174
|
+
if (has3d && has2d) return 'split'
|
|
175
|
+
if (has3d) return '3d'
|
|
176
|
+
if (has2d) return '2d'
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Layouts saved before views existed stored one `viewMode` (`'3d' | '2d' |
|
|
182
|
+
* 'split'`, the plan always on the left) and the plan's width.
|
|
183
|
+
*/
|
|
184
|
+
export function viewLayoutFromLegacy(legacy: {
|
|
185
|
+
viewMode?: unknown
|
|
186
|
+
isFloorplanOpen?: unknown
|
|
187
|
+
floorplanPaneRatio?: unknown
|
|
188
|
+
}): ViewLayout {
|
|
189
|
+
const viewMode =
|
|
190
|
+
legacy.viewMode === '2d' || legacy.viewMode === '3d' || legacy.viewMode === 'split'
|
|
191
|
+
? legacy.viewMode
|
|
192
|
+
: legacy.isFloorplanOpen === true
|
|
193
|
+
? 'split'
|
|
194
|
+
: '3d'
|
|
195
|
+
return sceneLayout(viewMode, clampViewPaneRatio(legacy.floorplanPaneRatio))
|
|
196
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function installImmediateAnimationFrames(): () => void {
|
|
2
|
+
const names = ['requestAnimationFrame', 'cancelAnimationFrame'] as const
|
|
3
|
+
const previous = names.map((name) => Object.getOwnPropertyDescriptor(globalThis, name))
|
|
4
|
+
globalThis.requestAnimationFrame = (callback) => {
|
|
5
|
+
callback(0)
|
|
6
|
+
return 0
|
|
7
|
+
}
|
|
8
|
+
globalThis.cancelAnimationFrame = () => {}
|
|
9
|
+
return () => {
|
|
10
|
+
for (const [index, name] of names.entries()) {
|
|
11
|
+
const descriptor = previous[index]
|
|
12
|
+
if (descriptor) Object.defineProperty(globalThis, name, descriptor)
|
|
13
|
+
else Reflect.deleteProperty(globalThis, name)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|