@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,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
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { expect } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
acquireSceneHistoryPause,
|
|
5
|
+
BuildingNode,
|
|
6
|
+
createZone,
|
|
7
|
+
DoorNode,
|
|
8
|
+
generateId,
|
|
9
|
+
getSceneHistoryPauseDepth,
|
|
10
|
+
initSpaceDetectionSync,
|
|
11
|
+
LevelNode,
|
|
12
|
+
useLiveNodeOverrides,
|
|
13
|
+
useScene,
|
|
14
|
+
type WallNode,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
import { commitRoomElevation } from '../lib/room-handle-drag'
|
|
17
|
+
import { applyRoomPlan } from '../lib/room-structure-commands'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The harness scene, reconciled live: a 6 × 4 m room (`zoneId`, its east wall
|
|
21
|
+
* at x = 6 the one gestures move) and, west of it through a door, a room raised
|
|
22
|
+
* 0.45 m (`raisedZoneId`) whose floor steps down through the doorway. Returns
|
|
23
|
+
* the reconciler's stop.
|
|
24
|
+
*/
|
|
25
|
+
export function seedRoundTripScene(buildingId: string, levelId: string) {
|
|
26
|
+
const building = BuildingNode.parse({ id: buildingId, children: [levelId] })
|
|
27
|
+
const level = LevelNode.parse({ id: levelId, parentId: building.id })
|
|
28
|
+
useScene.setState({
|
|
29
|
+
nodes: { [building.id]: building, [level.id]: level },
|
|
30
|
+
rootNodeIds: [building.id],
|
|
31
|
+
dirtyNodes: new Set(),
|
|
32
|
+
materials: {},
|
|
33
|
+
collections: {},
|
|
34
|
+
readOnly: false,
|
|
35
|
+
})
|
|
36
|
+
const stop = initSpaceDetectionSync(useScene, {
|
|
37
|
+
getState: () => ({ spaces: {}, setSpaces: () => {} }),
|
|
38
|
+
})
|
|
39
|
+
const room = (polygon: [number, number][]) => {
|
|
40
|
+
const plan = createZone(useScene.getState().nodes, {
|
|
41
|
+
levelId,
|
|
42
|
+
polygon,
|
|
43
|
+
enclose: true,
|
|
44
|
+
mintId: generateId,
|
|
45
|
+
})
|
|
46
|
+
applyRoomPlan(plan)
|
|
47
|
+
return plan.zoneId
|
|
48
|
+
}
|
|
49
|
+
const zoneId = room([
|
|
50
|
+
[0, 0],
|
|
51
|
+
[6, 0],
|
|
52
|
+
[6, 4],
|
|
53
|
+
[0, 4],
|
|
54
|
+
])
|
|
55
|
+
const raisedZoneId = room([
|
|
56
|
+
[-4, 0],
|
|
57
|
+
[0, 0],
|
|
58
|
+
[0, 4],
|
|
59
|
+
[-4, 4],
|
|
60
|
+
])
|
|
61
|
+
const shared = Object.values(useScene.getState().nodes).find(
|
|
62
|
+
(n): n is WallNode => n.type === 'wall' && n.start[0] === 0 && n.end[0] === 0,
|
|
63
|
+
)!
|
|
64
|
+
const door = DoorNode.parse({ parentId: shared.id, wallId: shared.id, position: [2, 0, 0] })
|
|
65
|
+
useScene.getState().applyNodeChanges({ create: [{ node: door, parentId: shared.id }] })
|
|
66
|
+
commitRoomElevation(raisedZoneId, 0.45)
|
|
67
|
+
return { stop, zoneId, raisedZoneId }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A live gesture as the harness drives it: `start` begins it and moves it far
|
|
72
|
+
* enough that its preview reshapes the building (derived plates, ceilings,
|
|
73
|
+
* zones included); then either `cancel` or `commit` ends it.
|
|
74
|
+
*/
|
|
75
|
+
export type LiveGesture = {
|
|
76
|
+
start: () => unknown
|
|
77
|
+
cancel: () => unknown
|
|
78
|
+
commit: () => unknown
|
|
79
|
+
/** Whether the preview ran (an override, a ghost…), checked after `start`. */
|
|
80
|
+
previewed?: () => boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Every node, as plain data (JSON also folds -0 into 0, as the transport does). */
|
|
84
|
+
export const sceneData = (): Record<string, unknown> =>
|
|
85
|
+
JSON.parse(JSON.stringify(useScene.getState().nodes))
|
|
86
|
+
|
|
87
|
+
function expectReleased() {
|
|
88
|
+
expect([...useLiveNodeOverrides.getState().overrides.keys()]).toEqual([])
|
|
89
|
+
expect(getSceneHistoryPauseDepth()).toBe(0)
|
|
90
|
+
expect(useScene.temporal.getState().isTracking).toBe(true)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* start → move → cancel leaves every node deep-equal to before (derived ones
|
|
95
|
+
* included), no history entry, history tracking again, no override left, and
|
|
96
|
+
* every node the preview drew from an override marked to rebuild — else its
|
|
97
|
+
* mesh stays at the preview.
|
|
98
|
+
*/
|
|
99
|
+
export async function expectCancelRestores(gesture: LiveGesture) {
|
|
100
|
+
const before = sceneData()
|
|
101
|
+
const past = useScene.temporal.getState().pastStates.length
|
|
102
|
+
await gesture.start()
|
|
103
|
+
if (gesture.previewed) expect(gesture.previewed()).toBe(true)
|
|
104
|
+
const drawnFromPreview = [...useLiveNodeOverrides.getState().overrides.keys()] as AnyNodeId[]
|
|
105
|
+
useScene.getState().dirtyNodes.clear()
|
|
106
|
+
await gesture.cancel()
|
|
107
|
+
expect(sceneData()).toEqual(before)
|
|
108
|
+
expect(useScene.temporal.getState().pastStates.length).toBe(past)
|
|
109
|
+
expectReleased()
|
|
110
|
+
const { nodes, dirtyNodes, markDirty } = useScene.getState()
|
|
111
|
+
const stale = drawnFromPreview.filter((id) => {
|
|
112
|
+
if (!nodes[id] || dirtyNodes.has(id)) return false
|
|
113
|
+
// A kind that opts out of dirty tracking redraws from the override store itself.
|
|
114
|
+
markDirty(id)
|
|
115
|
+
return dirtyNodes.has(id)
|
|
116
|
+
})
|
|
117
|
+
expect(stale).toEqual([])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* start → move → commit writes one undo step that reshapes derived nodes of
|
|
122
|
+
* `derivedTypes` too, releases everything, and one undo brings every node back.
|
|
123
|
+
*/
|
|
124
|
+
export async function expectCommitIsOneUndoStep(
|
|
125
|
+
gesture: LiveGesture,
|
|
126
|
+
derivedTypes: readonly string[] = ['slab', 'zone'],
|
|
127
|
+
) {
|
|
128
|
+
const before = sceneData()
|
|
129
|
+
const past = useScene.temporal.getState().pastStates.length
|
|
130
|
+
await gesture.start()
|
|
131
|
+
await gesture.commit()
|
|
132
|
+
const after = sceneData()
|
|
133
|
+
expect(useScene.temporal.getState().pastStates.length).toBe(past + 1)
|
|
134
|
+
expectReleased()
|
|
135
|
+
const changedTypes = new Set(
|
|
136
|
+
Object.keys({ ...before, ...after })
|
|
137
|
+
.filter((id) => JSON.stringify(before[id]) !== JSON.stringify(after[id]))
|
|
138
|
+
.map((id) => ((after[id] ?? before[id]) as { type: string }).type),
|
|
139
|
+
)
|
|
140
|
+
for (const type of derivedTypes) expect([...changedTypes]).toContain(type)
|
|
141
|
+
useScene.temporal.getState().undo()
|
|
142
|
+
expect(sceneData()).toEqual(before)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* During the drag only the surfaces (plates, ceilings, zones) the release
|
|
147
|
+
* reshapes are drawn from a preview, and each is drawn exactly as the release
|
|
148
|
+
* leaves it — the floors it never touches keep their stored shape.
|
|
149
|
+
*/
|
|
150
|
+
export async function expectPreviewMatchesCommit(
|
|
151
|
+
gesture: LiveGesture,
|
|
152
|
+
surfaceTypes: readonly string[] = ['slab', 'ceiling', 'zone'],
|
|
153
|
+
) {
|
|
154
|
+
const before = sceneData() as Record<string, Record<string, unknown>>
|
|
155
|
+
await gesture.start()
|
|
156
|
+
const previews = new Map(
|
|
157
|
+
[...useLiveNodeOverrides.getState().overrides].filter(([id]) =>
|
|
158
|
+
surfaceTypes.includes(before[id]?.type as string),
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
await gesture.commit()
|
|
162
|
+
const after = sceneData() as Record<string, Record<string, unknown>>
|
|
163
|
+
const json = (value: unknown) => JSON.stringify(value ?? null)
|
|
164
|
+
const untouched = [...previews.keys()].filter(
|
|
165
|
+
(id) => after[id] && json(before[id]) === json(after[id]),
|
|
166
|
+
)
|
|
167
|
+
expect(untouched).toEqual([])
|
|
168
|
+
const drawn: Record<string, Record<string, string>> = {}
|
|
169
|
+
const committed: Record<string, Record<string, string>> = {}
|
|
170
|
+
for (const [id, values] of previews) {
|
|
171
|
+
if (!after[id]) continue
|
|
172
|
+
drawn[id] = {}
|
|
173
|
+
committed[id] = {}
|
|
174
|
+
for (const key of Object.keys(values)) {
|
|
175
|
+
drawn[id][key] = json(values[key])
|
|
176
|
+
committed[id][key] = json(after[id][key])
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
expect(drawn).toEqual(committed)
|
|
180
|
+
const reshapedUnseen = Object.keys(after).filter(
|
|
181
|
+
(id) =>
|
|
182
|
+
before[id] &&
|
|
183
|
+
surfaceTypes.includes(after[id]!.type as string) &&
|
|
184
|
+
(json(before[id].polygon) !== json(after[id]!.polygon) ||
|
|
185
|
+
json(before[id].holes) !== json(after[id]!.holes)) &&
|
|
186
|
+
!previews.get(id)?.polygon,
|
|
187
|
+
)
|
|
188
|
+
expect(reshapedUnseen).toEqual([])
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A collaborator's edit landing mid-gesture (here: a field the gesture never
|
|
193
|
+
* writes) survives both the cancel and the commit. `target` names the node,
|
|
194
|
+
* read once the gesture has started.
|
|
195
|
+
*/
|
|
196
|
+
export async function expectRemoteEditSurvives(
|
|
197
|
+
gesture: () => LiveGesture,
|
|
198
|
+
target: () => AnyNodeId,
|
|
199
|
+
patch: Record<string, unknown> = { name: 'Edited by a collaborator' },
|
|
200
|
+
) {
|
|
201
|
+
const remote = (id: AnyNodeId) => {
|
|
202
|
+
const nodes = useScene.getState().nodes
|
|
203
|
+
const release = acquireSceneHistoryPause(useScene)
|
|
204
|
+
try {
|
|
205
|
+
useScene.setState({ nodes: { ...nodes, [id]: { ...nodes[id]!, ...patch } } })
|
|
206
|
+
} finally {
|
|
207
|
+
release()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const end of ['cancel', 'commit'] as const) {
|
|
211
|
+
const run = gesture()
|
|
212
|
+
const before = sceneData() as Record<string, Record<string, unknown>>
|
|
213
|
+
await run.start()
|
|
214
|
+
const id = target()
|
|
215
|
+
remote(id)
|
|
216
|
+
await run[end]()
|
|
217
|
+
const after = sceneData() as Record<string, Record<string, unknown>>
|
|
218
|
+
expect({ end, ...pick(after[id], patch) }).toEqual({ end, ...patch })
|
|
219
|
+
if (end === 'cancel') expect(after).toEqual({ ...before, [id]: { ...before[id], ...patch } })
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const pick = (node: Record<string, unknown> | undefined, patch: Record<string, unknown>) =>
|
|
224
|
+
Object.fromEntries(Object.keys(patch).map((key) => [key, node?.[key]]))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import { _roots, act, createRoot, extend } from '@react-three/fiber'
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import * as THREE from 'three'
|
|
6
|
+
import useEditor from '../store/use-editor'
|
|
7
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
8
|
+
import useSessionGroups from '../store/use-session-groups'
|
|
9
|
+
|
|
10
|
+
extend({
|
|
11
|
+
Group: THREE.Group,
|
|
12
|
+
Mesh: THREE.Mesh,
|
|
13
|
+
LineSegments: THREE.LineSegments,
|
|
14
|
+
CircleGeometry: THREE.CircleGeometry,
|
|
15
|
+
CylinderGeometry: THREE.CylinderGeometry,
|
|
16
|
+
PlaneGeometry: THREE.PlaneGeometry,
|
|
17
|
+
MeshBasicMaterial: THREE.MeshBasicMaterial,
|
|
18
|
+
LineBasicMaterial: THREE.LineBasicMaterial,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export async function withSelectionHarness(
|
|
22
|
+
run: (harness: {
|
|
23
|
+
render: (node: ReactNode) => Promise<void>
|
|
24
|
+
canvas: HTMLCanvasElement
|
|
25
|
+
}) => Promise<void>,
|
|
26
|
+
) {
|
|
27
|
+
const scene = useScene.getState()
|
|
28
|
+
const viewer = useViewer.getState()
|
|
29
|
+
const editor = useEditor.getState()
|
|
30
|
+
const scope = useInteractionScope.getState()
|
|
31
|
+
const groups = useSessionGroups.getState()
|
|
32
|
+
const previousWindow = globalThis.window
|
|
33
|
+
const previousRaf = globalThis.requestAnimationFrame
|
|
34
|
+
const actGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
35
|
+
const previousAct = actGlobal.IS_REACT_ACT_ENVIRONMENT
|
|
36
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = true
|
|
37
|
+
globalThis.window = new EventTarget() as Window & typeof globalThis
|
|
38
|
+
globalThis.requestAnimationFrame = () => 0
|
|
39
|
+
const canvas = Object.assign(new EventTarget(), {
|
|
40
|
+
style: { cursor: '' },
|
|
41
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 1000, height: 1000 }),
|
|
42
|
+
}) as unknown as HTMLCanvasElement
|
|
43
|
+
const root = createRoot(canvas)
|
|
44
|
+
try {
|
|
45
|
+
const camera = Object.assign(new THREE.OrthographicCamera(-1, 9, 9, -1, 0.1, 100), {
|
|
46
|
+
manual: true,
|
|
47
|
+
})
|
|
48
|
+
camera.position.set(0, 10, 0)
|
|
49
|
+
camera.up.set(0, 0, -1)
|
|
50
|
+
camera.lookAt(0, 0, 0)
|
|
51
|
+
camera.updateMatrixWorld(true)
|
|
52
|
+
await root.configure({
|
|
53
|
+
gl: {
|
|
54
|
+
domElement: canvas,
|
|
55
|
+
render() {},
|
|
56
|
+
setSize() {},
|
|
57
|
+
setPixelRatio() {},
|
|
58
|
+
} as unknown as THREE.WebGLRenderer,
|
|
59
|
+
camera,
|
|
60
|
+
frameloop: 'never',
|
|
61
|
+
dpr: 1,
|
|
62
|
+
size: { width: 1000, height: 1000, top: 0, left: 0 },
|
|
63
|
+
})
|
|
64
|
+
await run({
|
|
65
|
+
render: async (node) => {
|
|
66
|
+
await act(async () => root.render(node))
|
|
67
|
+
},
|
|
68
|
+
canvas,
|
|
69
|
+
})
|
|
70
|
+
} finally {
|
|
71
|
+
await act(async () => root.render(null))
|
|
72
|
+
_roots.delete(canvas)
|
|
73
|
+
useScene.setState(scene)
|
|
74
|
+
useViewer.setState(viewer)
|
|
75
|
+
useEditor.setState(editor)
|
|
76
|
+
useInteractionScope.setState(scope)
|
|
77
|
+
useSessionGroups.setState(groups)
|
|
78
|
+
globalThis.window = previousWindow
|
|
79
|
+
globalThis.requestAnimationFrame = previousRaf
|
|
80
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct
|
|
81
|
+
}
|
|
82
|
+
}
|