@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,379 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
BuildingNode,
|
|
5
|
+
clearSceneHistory,
|
|
6
|
+
createZone,
|
|
7
|
+
generateId,
|
|
8
|
+
initSpaceDetectionSync,
|
|
9
|
+
LevelNode,
|
|
10
|
+
SlabNode,
|
|
11
|
+
type SlabNode as SlabNodeType,
|
|
12
|
+
useScene,
|
|
13
|
+
ZoneNode,
|
|
14
|
+
type ZoneNode as ZoneNodeType,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
import { roomBuiltOn, roomFloorChoices, roomFloorKey, separateFloorSummary } from './room-built-on'
|
|
17
|
+
import { roomConstructionState } from './room-construction'
|
|
18
|
+
import {
|
|
19
|
+
commitRoomFloorElevation,
|
|
20
|
+
roomRelativeFloorHeight,
|
|
21
|
+
separateFloorOffset,
|
|
22
|
+
setRoomFloor,
|
|
23
|
+
setRoomRelativeFloorHeight,
|
|
24
|
+
stepRoomFloorElevation,
|
|
25
|
+
} from './room-construction-commands'
|
|
26
|
+
import {
|
|
27
|
+
commitRoomElevation,
|
|
28
|
+
ownFloorElevationBounds,
|
|
29
|
+
roomFloorElevation,
|
|
30
|
+
} from './room-handle-drag'
|
|
31
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
32
|
+
import { showRoomNotice, useRoomTransform } from './room-transform-session'
|
|
33
|
+
|
|
34
|
+
const LEVEL = 'level_built_on'
|
|
35
|
+
type Ring = [number, number][]
|
|
36
|
+
|
|
37
|
+
const rect = (x0: number, z0: number, x1: number, z1: number): Ring => [
|
|
38
|
+
[x0, z0],
|
|
39
|
+
[x1, z0],
|
|
40
|
+
[x1, z1],
|
|
41
|
+
[x0, z1],
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
/** A room; `key` puts it on a separate floor (set past the schema, which may lag the key type). */
|
|
45
|
+
const zone = (id: string, name: string, polygon: Ring, key?: string) => {
|
|
46
|
+
const room = ZoneNode.parse({ id, name, parentId: LEVEL, polygon, spaceRole: 'room' })
|
|
47
|
+
return (key ? { ...room, floor: { ...room.floor, footprint: key } } : room) as ZoneNode
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const plate = (id: string, polygon: Ring, zoneIds: string[], elevation: number, name?: string) =>
|
|
51
|
+
SlabNode.parse({
|
|
52
|
+
id,
|
|
53
|
+
name,
|
|
54
|
+
parentId: LEVEL,
|
|
55
|
+
polygon,
|
|
56
|
+
plateRole: 'base',
|
|
57
|
+
boundary: 'auto',
|
|
58
|
+
zoneIds,
|
|
59
|
+
elevation,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const scene = (...nodes: AnyNode[]): Record<string, AnyNode> =>
|
|
63
|
+
Object.fromEntries(
|
|
64
|
+
[LevelNode.parse({ id: LEVEL }), ...nodes].map((node) => [node.id, node as AnyNode]),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const living = zone('zone_living', 'Living', rect(0, 0, 4, 4))
|
|
68
|
+
const lanai = zone('zone_lanai', 'Lanai', rect(4, 0, 6, 4))
|
|
69
|
+
const keyedLanai = zone('zone_lanai', 'Lanai', rect(4, 0, 6, 4), 'floor_lanai')
|
|
70
|
+
const length = (meters: number) => `${meters.toFixed(2)} m`
|
|
71
|
+
|
|
72
|
+
describe('Built on: when the picker shows and what it offers', () => {
|
|
73
|
+
test('a room sharing its footprint offers that floor, named as the footprint is', () => {
|
|
74
|
+
const house = plate('slab_house', rect(0, 0, 6, 4), [living.id, lanai.id], 0.15)
|
|
75
|
+
const model = roomBuiltOn(scene(living, lanai, house), lanai.id)
|
|
76
|
+
expect(model?.current).toEqual({ key: null, plateId: house.id, name: 'Shared floor' })
|
|
77
|
+
expect(model?.choices).toHaveLength(1)
|
|
78
|
+
// A floor the user named shows that name.
|
|
79
|
+
const named = plate('slab_house', rect(0, 0, 6, 4), [living.id, lanai.id], 0.15, 'Cabin')
|
|
80
|
+
expect(roomBuiltOn(scene(living, lanai, named), lanai.id)?.current.name).toBe('Cabin')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('a room alone on the shared floor with no other floor touching has no choice to make', () => {
|
|
84
|
+
const shed = plate('slab_shed', rect(10, 0, 12, 4), [lanai.id], 0.05)
|
|
85
|
+
expect(roomBuiltOn(scene(lanai, shed), lanai.id)).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('a room on a separate floor lists the floors it touches, current first, and reads against the shared floor', () => {
|
|
89
|
+
const house = plate('slab_house', rect(0, 0, 4, 4), [living.id], 0.15)
|
|
90
|
+
const own = plate('slab_lanai', rect(4, 0, 6, 4), [keyedLanai.id], 0)
|
|
91
|
+
const far = plate('slab_far', rect(20, 0, 24, 4), ['zone_far'], 0.05)
|
|
92
|
+
const farRoom = zone('zone_far', 'Studio', rect(20, 0, 24, 4))
|
|
93
|
+
const nodes = scene(living, keyedLanai, house, own, far, farRoom)
|
|
94
|
+
const model = roomBuiltOn(nodes, keyedLanai.id)!
|
|
95
|
+
expect(model.choices).toEqual([
|
|
96
|
+
{ key: 'floor_lanai', plateId: own.id, name: 'Lanai floor' },
|
|
97
|
+
{ key: null, plateId: house.id, name: 'Living floor' },
|
|
98
|
+
])
|
|
99
|
+
expect(model.shared?.plateId).toBe(house.id)
|
|
100
|
+
expect(separateFloorOffset(nodes, keyedLanai.id)).toBeCloseTo(-0.15)
|
|
101
|
+
// The house room touches the lanai's floor: it may join it.
|
|
102
|
+
expect(roomFloorChoices(nodes, living.id).map((choice) => choice.key)).toEqual([
|
|
103
|
+
null,
|
|
104
|
+
'floor_lanai',
|
|
105
|
+
])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('a room on a separate floor no shared floor touches can still go back to the shared floor', () => {
|
|
109
|
+
const own = plate('slab_lanai', rect(4, 0, 6, 4), [keyedLanai.id], 0)
|
|
110
|
+
const nodes = scene(keyedLanai, own)
|
|
111
|
+
const model = roomBuiltOn(nodes, keyedLanai.id)!
|
|
112
|
+
expect(model.choices).toEqual([
|
|
113
|
+
{ key: 'floor_lanai', plateId: own.id, name: 'Lanai floor' },
|
|
114
|
+
{ key: null, plateId: null, name: 'Shared floor' },
|
|
115
|
+
])
|
|
116
|
+
expect(model.shared).toBeNull()
|
|
117
|
+
expect(separateFloorOffset(nodes, keyedLanai.id)).toBeNull()
|
|
118
|
+
expect(
|
|
119
|
+
separateFloorSummary({ ...model, offset: separateFloorOffset(nodes, keyedLanai.id) }, length),
|
|
120
|
+
).toBe('Lanai floor')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('a room alone on the shared floor still sees a separate floor it touches', () => {
|
|
124
|
+
const house = plate('slab_house', rect(0, 0, 4, 4), [living.id], 0.15)
|
|
125
|
+
const own = plate('slab_lanai', rect(4, 0, 6, 4), [keyedLanai.id], 0)
|
|
126
|
+
expect(roomBuiltOn(scene(living, keyedLanai, house, own), living.id)?.choices).toHaveLength(2)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('a room standing on a drawn slab has its floor on the slab: no picker', () => {
|
|
130
|
+
const drawn = SlabNode.parse({ id: 'slab_drawn', parentId: LEVEL, polygon: rect(4, 0, 6, 4) })
|
|
131
|
+
const onSlab = ZoneNode.parse({
|
|
132
|
+
id: 'zone_lanai',
|
|
133
|
+
name: 'Lanai',
|
|
134
|
+
parentId: LEVEL,
|
|
135
|
+
polygon: rect(4, 0, 6, 4),
|
|
136
|
+
spaceRole: 'room',
|
|
137
|
+
floor: { sourceSlabId: drawn.id },
|
|
138
|
+
})
|
|
139
|
+
const house = plate('slab_house', rect(0, 0, 4, 4), [living.id], 0.15)
|
|
140
|
+
expect(roomBuiltOn(scene(living, onSlab, house, drawn), onSlab.id)).toBeNull()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('a mezzanine has no footprint: no picker', () => {
|
|
144
|
+
const mezzanine = ZoneNode.parse({
|
|
145
|
+
id: 'zone_mezz',
|
|
146
|
+
name: 'Loft',
|
|
147
|
+
parentId: LEVEL,
|
|
148
|
+
polygon: rect(0, 0, 2, 2),
|
|
149
|
+
spaceRole: 'room',
|
|
150
|
+
hostZoneId: living.id,
|
|
151
|
+
floor: { support: 'open', elevation: 2.4 },
|
|
152
|
+
})
|
|
153
|
+
const house = plate('slab_house', rect(0, 0, 6, 4), [living.id, lanai.id, mezzanine.id], 0.15)
|
|
154
|
+
expect(roomBuiltOn(scene(living, lanai, mezzanine, house), mezzanine.id)).toBeNull()
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('Built on: the Floor row summary', () => {
|
|
159
|
+
const lanaiFloor = { name: 'Lanai floor' }
|
|
160
|
+
test('says how a separate floor sits against the shared floor', () => {
|
|
161
|
+
const shared = { name: 'Shared floor' }
|
|
162
|
+
expect(separateFloorSummary({ current: lanaiFloor, shared, offset: -0.15 }, length)).toBe(
|
|
163
|
+
'Lanai floor · 0.15 m below Shared floor',
|
|
164
|
+
)
|
|
165
|
+
expect(
|
|
166
|
+
separateFloorSummary({ current: lanaiFloor, shared: { name: 'Cabin' }, offset: 0.3 }, length),
|
|
167
|
+
).toBe('Lanai floor · 0.30 m above Cabin')
|
|
168
|
+
expect(separateFloorSummary({ current: lanaiFloor, shared, offset: 0.002 }, length)).toBe(
|
|
169
|
+
'Lanai floor · level with Shared floor',
|
|
170
|
+
)
|
|
171
|
+
expect(separateFloorSummary({ current: lanaiFloor, shared: null, offset: null }, length)).toBe(
|
|
172
|
+
'Lanai floor',
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test("compares the room's walking surface with the shared-floor room beside it, not plate with plate", () => {
|
|
177
|
+
const wall = 'wall_between'
|
|
178
|
+
// The living room is raised 0.15 on the 0.15 house plate: it walks at 0.30.
|
|
179
|
+
const raised = {
|
|
180
|
+
...living,
|
|
181
|
+
boundaryWallIds: [wall],
|
|
182
|
+
floor: { ...living.floor, elevation: 0.3 },
|
|
183
|
+
} as ZoneNodeType
|
|
184
|
+
const walled = { ...keyedLanai, boundaryWallIds: [wall] } as ZoneNodeType
|
|
185
|
+
const house = plate('slab_house', rect(0, 0, 4, 4), [living.id], 0.15)
|
|
186
|
+
const own = plate('slab_lanai', rect(4, 0, 6, 4), [keyedLanai.id], 0)
|
|
187
|
+
const nodes = scene(raised, walled, house, own)
|
|
188
|
+
expect(separateFloorOffset(nodes, keyedLanai.id)).toBeCloseTo(-0.3)
|
|
189
|
+
const model = roomBuiltOn(nodes, keyedLanai.id)!
|
|
190
|
+
expect(
|
|
191
|
+
separateFloorSummary({ ...model, offset: separateFloorOffset(nodes, keyedLanai.id) }, length),
|
|
192
|
+
).toBe('Lanai floor · 0.30 m below Living floor')
|
|
193
|
+
// Nothing shared across a wall or open side: the shared plate's top.
|
|
194
|
+
expect(separateFloorOffset(scene(raised, keyedLanai, house, own), keyedLanai.id)).toBeCloseTo(
|
|
195
|
+
-0.15,
|
|
196
|
+
)
|
|
197
|
+
// A room on the shared floor has no step to state.
|
|
198
|
+
expect(separateFloorOffset(nodes, living.id)).toBeNull()
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('Built on: moving between floors, through core', () => {
|
|
203
|
+
const INT = 'level_built_on_live'
|
|
204
|
+
const live = () => useScene.getState().nodes as Record<string, AnyNode>
|
|
205
|
+
const steps = () => useScene.temporal.getState().pastStates.length
|
|
206
|
+
let stop = () => {}
|
|
207
|
+
let houseRoom = ''
|
|
208
|
+
let lanaiRoom = ''
|
|
209
|
+
|
|
210
|
+
const draw = (polygon: Ring, name: string) => {
|
|
211
|
+
const plan = createZone(live(), {
|
|
212
|
+
levelId: INT,
|
|
213
|
+
polygon,
|
|
214
|
+
enclose: true,
|
|
215
|
+
mintId: generateId,
|
|
216
|
+
name,
|
|
217
|
+
})
|
|
218
|
+
applyRoomPlan(plan)
|
|
219
|
+
return plan.zoneId
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
globalThis.requestAnimationFrame ??= (callback) => {
|
|
224
|
+
callback(0)
|
|
225
|
+
return 0
|
|
226
|
+
}
|
|
227
|
+
globalThis.cancelAnimationFrame ??= () => {}
|
|
228
|
+
const building = BuildingNode.parse({ id: 'building_built_on', children: [INT] })
|
|
229
|
+
const level = LevelNode.parse({ id: INT, parentId: building.id })
|
|
230
|
+
useScene.setState({
|
|
231
|
+
nodes: { [building.id]: building, [level.id]: level },
|
|
232
|
+
rootNodeIds: [building.id],
|
|
233
|
+
dirtyNodes: new Set(),
|
|
234
|
+
materials: {},
|
|
235
|
+
collections: {},
|
|
236
|
+
readOnly: false,
|
|
237
|
+
})
|
|
238
|
+
useScene.temporal.getState().resume()
|
|
239
|
+
stop = initSpaceDetectionSync(useScene, {
|
|
240
|
+
getState: () => ({ spaces: {}, setSpaces: () => {} }),
|
|
241
|
+
})
|
|
242
|
+
houseRoom = draw(rect(0, 0, 5, 4), 'Living')
|
|
243
|
+
draw(rect(5, 0, 8, 4), 'Bedroom')
|
|
244
|
+
lanaiRoom = draw(rect(0, 4, 5, 7), 'Lanai')
|
|
245
|
+
expect(setRoomRelativeFloorHeight(lanaiRoom, -0.15).status).toBe('applied')
|
|
246
|
+
clearSceneHistory()
|
|
247
|
+
})
|
|
248
|
+
afterEach(() => {
|
|
249
|
+
stop()
|
|
250
|
+
stop = () => {}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('New floor detaches in one undo step, keeps the height, and reads against the shared floor', () => {
|
|
254
|
+
expect(roomRelativeFloorHeight(live(), lanaiRoom)).toBeCloseTo(-0.15)
|
|
255
|
+
expect(roomBuiltOn(live(), lanaiRoom)?.current.key).toBeNull()
|
|
256
|
+
expect(setRoomFloor(lanaiRoom, 'new').status).toBe('applied')
|
|
257
|
+
expect(steps()).toBe(1)
|
|
258
|
+
const model = roomBuiltOn(live(), lanaiRoom)!
|
|
259
|
+
expect(model.current.key).not.toBeNull()
|
|
260
|
+
expect(model.current.name).toBe('Lanai floor')
|
|
261
|
+
expect(model.shared?.name).toBe('Shared floor')
|
|
262
|
+
expect(separateFloorOffset(live(), lanaiRoom)).toBeCloseTo(-0.15)
|
|
263
|
+
// Its height reads the same: measured from the shared floor beside it.
|
|
264
|
+
expect(roomRelativeFloorHeight(live(), lanaiRoom)).toBeCloseTo(-0.15)
|
|
265
|
+
// The shared wall stands on the house floor: the lanai is still fully floored.
|
|
266
|
+
expect(roomConstructionState(live(), lanaiRoom)?.floor.state).toBe('present')
|
|
267
|
+
// The house room touches the new floor: it can join it.
|
|
268
|
+
expect(roomBuiltOn(live(), houseRoom)?.choices.map((choice) => choice.name)).toEqual([
|
|
269
|
+
'Shared floor',
|
|
270
|
+
'Lanai floor',
|
|
271
|
+
])
|
|
272
|
+
useScene.temporal.getState().undo()
|
|
273
|
+
expect(roomBuiltOn(live(), lanaiRoom)?.current.key).toBeNull()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('a touching room joins the separate floor and keeps its walking height', () => {
|
|
277
|
+
setRoomFloor(lanaiRoom, 'new')
|
|
278
|
+
const key = roomBuiltOn(live(), lanaiRoom)!.current.key
|
|
279
|
+
const top = roomFloorElevation(live(), houseRoom)
|
|
280
|
+
clearSceneHistory()
|
|
281
|
+
expect(setRoomFloor(houseRoom, key).status).toBe('applied')
|
|
282
|
+
expect(steps()).toBe(1)
|
|
283
|
+
const joined = roomBuiltOn(live(), houseRoom)!
|
|
284
|
+
expect(joined.current).toEqual(roomBuiltOn(live(), lanaiRoom)!.current)
|
|
285
|
+
expect(joined.current).toMatchObject({ key, name: 'Lanai floor' })
|
|
286
|
+
expect(roomFloorElevation(live(), houseRoom)).toBeCloseTo(top)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test("a separate floor's Height raises it on a foundation, and picking the shared floor rejoins", () => {
|
|
290
|
+
setRoomFloor(lanaiRoom, 'new')
|
|
291
|
+
clearSceneHistory()
|
|
292
|
+
expect(setRoomRelativeFloorHeight(lanaiRoom, 0.1).status).toBe('applied')
|
|
293
|
+
expect(steps()).toBe(1)
|
|
294
|
+
const model = roomBuiltOn(live(), lanaiRoom)!
|
|
295
|
+
const own = live()[model.current.plateId] as SlabNodeType
|
|
296
|
+
const shared = live()[model.shared!.plateId] as SlabNodeType
|
|
297
|
+
expect(own.elevation - shared.elevation).toBeCloseTo(0.1)
|
|
298
|
+
expect(own.foundation?.type).toBe('solid')
|
|
299
|
+
expect(setRoomFloor(lanaiRoom, null).status).toBe('applied')
|
|
300
|
+
expect(roomBuiltOn(live(), lanaiRoom)?.current.key).toBeNull()
|
|
301
|
+
expect(roomRelativeFloorHeight(live(), lanaiRoom)).toBeCloseTo(0.1)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('the plan stepper and the 3D handle move a separate floor through its foundation', () => {
|
|
305
|
+
setRoomFloor(lanaiRoom, 'new')
|
|
306
|
+
clearSceneHistory()
|
|
307
|
+
// The detached lanai kept its legacy sunk top; its first edit lands its slab on the ground.
|
|
308
|
+
const next = stepRoomFloorElevation(live(), lanaiRoom, 0.05)
|
|
309
|
+
expect(next).not.toBeNull()
|
|
310
|
+
commitRoomElevation(lanaiRoom, next!)
|
|
311
|
+
expect(steps()).toBe(1)
|
|
312
|
+
expect(roomRelativeFloorHeight(live(), lanaiRoom)).toBeCloseTo(0)
|
|
313
|
+
const bounds = ownFloorElevationBounds(live(), lanaiRoom)!
|
|
314
|
+
expect(bounds.min).toBeCloseTo(roomFloorElevation(live(), lanaiRoom))
|
|
315
|
+
expect(bounds.max).toBeGreaterThan(roomFloorElevation(live(), lanaiRoom))
|
|
316
|
+
commitRoomElevation(lanaiRoom, stepRoomFloorElevation(live(), lanaiRoom, 0.05)!)
|
|
317
|
+
expect(roomRelativeFloorHeight(live(), lanaiRoom)).toBeCloseTo(0.05)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('a separate floor steps from the ground onto a 0.05 foundation and back', () => {
|
|
321
|
+
setRoomFloor(lanaiRoom, 'new')
|
|
322
|
+
expect(commitRoomElevation(lanaiRoom, 0)).toBe(true)
|
|
323
|
+
const plateId = roomBuiltOn(live(), lanaiRoom)!.current.plateId
|
|
324
|
+
useScene.getState().updateNode(plateId as SlabNodeType['id'], {
|
|
325
|
+
foundation: { type: 'none', material: 'library:stone' },
|
|
326
|
+
})
|
|
327
|
+
const before = live()
|
|
328
|
+
const own = before[plateId] as SlabNodeType
|
|
329
|
+
// On the ground: the slab sits on grade, so the top is its thickness.
|
|
330
|
+
expect(own.elevation).toBeCloseTo(own.thickness)
|
|
331
|
+
expect(ownFloorElevationBounds(before, lanaiRoom)!.min).toBeCloseTo(own.thickness)
|
|
332
|
+
for (let trip = 0; trip < 2; trip++) {
|
|
333
|
+
const up = stepRoomFloorElevation(live(), lanaiRoom, 0.05)!
|
|
334
|
+
expect(up).toBeCloseTo(own.thickness + 0.05)
|
|
335
|
+
expect(commitRoomElevation(lanaiRoom, up)).toBe(true)
|
|
336
|
+
expect(live()[plateId]).toMatchObject({
|
|
337
|
+
elevation: up,
|
|
338
|
+
thickness: own.thickness,
|
|
339
|
+
foundation: { type: 'solid', material: 'library:stone' },
|
|
340
|
+
})
|
|
341
|
+
expect(stepRoomFloorElevation(live(), lanaiRoom, -0.05)).toBeCloseTo(own.thickness)
|
|
342
|
+
// Below the ground is out of reach: the slab stays on grade.
|
|
343
|
+
expect(commitRoomElevation(lanaiRoom, own.thickness - 0.05)).toBe(true)
|
|
344
|
+
expect(live()).toEqual(before)
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test('a separate floor no shared-floor room touches still goes back to the shared floor', () => {
|
|
349
|
+
setRoomFloor(lanaiRoom, 'new')
|
|
350
|
+
const key = roomBuiltOn(live(), lanaiRoom)!.current.key
|
|
351
|
+
// The house room joins the lanai's floor: nothing on the shared floor touches the lanai now.
|
|
352
|
+
expect(setRoomFloor(houseRoom, key).status).toBe('applied')
|
|
353
|
+
const model = roomBuiltOn(live(), lanaiRoom)!
|
|
354
|
+
expect(model.shared).toBeNull()
|
|
355
|
+
expect(model.choices.at(-1)).toEqual({ key: null, plateId: null, name: 'Shared floor' })
|
|
356
|
+
clearSceneHistory()
|
|
357
|
+
expect(setRoomFloor(lanaiRoom, null).status).toBe('applied')
|
|
358
|
+
expect(steps()).toBe(1)
|
|
359
|
+
expect(roomFloorKey(live(), lanaiRoom)).toBeNull()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('a refused plan-stepper or 3D-handle height says why under the room pill', () => {
|
|
363
|
+
showRoomNotice(null)
|
|
364
|
+
const result = commitRoomFloorElevation(houseRoom, 10, length)
|
|
365
|
+
expect(result.status).toBe('conflict')
|
|
366
|
+
const notice = useRoomTransform.getState().notice
|
|
367
|
+
expect(notice?.zoneId).toBe(houseRoom)
|
|
368
|
+
expect(notice?.message).toMatch(/^Too high: the floor can rise at most \d+\.\d\d m/)
|
|
369
|
+
expect(steps()).toBe(0)
|
|
370
|
+
showRoomNotice(null)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('a refused move comes back as a message, never a throw', () => {
|
|
374
|
+
useScene.getState().updateNodes([{ id: lanaiRoom as AnyNode['id'], data: { hasFloor: false } }])
|
|
375
|
+
const result = setRoomFloor(lanaiRoom, 'new')
|
|
376
|
+
expect(result.status).toBe('conflict')
|
|
377
|
+
expect(result.status === 'conflict' && result.message).toContain('Level the floors first')
|
|
378
|
+
})
|
|
379
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
roomFloorChoices as coreRoomFloorChoices,
|
|
4
|
+
roomDrawnFloor,
|
|
5
|
+
type SlabNode,
|
|
6
|
+
type ZoneNode,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
import { levelFootprints } from './floor-footprints'
|
|
9
|
+
|
|
10
|
+
// "Built on": which floor a room stands on. By default a room is part of its
|
|
11
|
+
// connected footprint's shared floor (`floor.footprint` absent); a floor key
|
|
12
|
+
// puts it on a separate floor — a lanai a step below the house, with its own
|
|
13
|
+
// height, thickness and foundation — shared by every touching room with the
|
|
14
|
+
// same key. These helpers read that choice in the words the room panel uses.
|
|
15
|
+
|
|
16
|
+
type Nodes = Readonly<Record<string, AnyNode>>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One floor a room can stand on: its key (null = the shared floor), plate and
|
|
20
|
+
* name. A null plate is the shared floor of a room no shared floor touches:
|
|
21
|
+
* picking it gives the room a shared floor of its own.
|
|
22
|
+
*/
|
|
23
|
+
export type FloorChoice = { key: string | null; plateId: string | null; name: string }
|
|
24
|
+
/** A floor that exists: its plate is in the scene. */
|
|
25
|
+
export type PlateChoice = FloorChoice & { plateId: string }
|
|
26
|
+
|
|
27
|
+
const SHARED_FLOOR: FloorChoice = { key: null, plateId: null, name: 'Shared floor' }
|
|
28
|
+
|
|
29
|
+
const isRoom = (node: AnyNode | undefined): node is ZoneNode =>
|
|
30
|
+
node?.type === 'zone' && node.spaceRole === 'room'
|
|
31
|
+
|
|
32
|
+
const isGroundRoom = (node: AnyNode | undefined): node is ZoneNode =>
|
|
33
|
+
isRoom(node) && node.floor?.support !== 'open'
|
|
34
|
+
|
|
35
|
+
/** The room's floor key: null on the shared floor. */
|
|
36
|
+
export function roomFloorKey(nodes: Nodes, zoneId: string): string | null {
|
|
37
|
+
const zone = nodes[zoneId]
|
|
38
|
+
return (isRoom(zone) && zone.floor?.footprint) || null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The base plate listing the room, if any. */
|
|
42
|
+
function listingPlate(nodes: Nodes, zone: ZoneNode): SlabNode | null {
|
|
43
|
+
if (!zone.parentId) return null
|
|
44
|
+
return (
|
|
45
|
+
levelFootprints(nodes as Record<string, AnyNode>, zone.parentId).find((plate) =>
|
|
46
|
+
plate.zoneIds?.includes(zone.id),
|
|
47
|
+
) ?? null
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The floors a room can stand on (core's `roomFloorChoices`): the one it is
|
|
53
|
+
* on first, then every floor of its level it touches, each by
|
|
54
|
+
* `floorFootprintName`. Drawn slabs and mezzanine decks are no floor to move
|
|
55
|
+
* a room onto.
|
|
56
|
+
*/
|
|
57
|
+
export function roomFloorChoices(nodes: Nodes, zoneId: string): PlateChoice[] {
|
|
58
|
+
return coreRoomFloorChoices(nodes, zoneId)
|
|
59
|
+
.filter((choice) => !choice.drawn && !choice.mezzanine)
|
|
60
|
+
.map(({ key, plateId, name }) => ({ key, plateId, name }))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The plate a room on a separate floor stands on alone: its height is that
|
|
65
|
+
* plate's floor height (nothing else stands on it). Null for a room on the
|
|
66
|
+
* shared floor or sharing its separate floor with other rooms.
|
|
67
|
+
*/
|
|
68
|
+
export function roomOwnPlate(nodes: Nodes, zoneId: string): SlabNode | null {
|
|
69
|
+
const zone = nodes[zoneId]
|
|
70
|
+
if (!isRoom(zone) || !zone.floor?.footprint || zone.floor.support === 'open') return null
|
|
71
|
+
const plate = listingPlate(nodes, zone)
|
|
72
|
+
if (!plate) return null
|
|
73
|
+
const alone = !(plate.zoneIds ?? []).some((id) => id !== zoneId && isGroundRoom(nodes[id]))
|
|
74
|
+
return alone ? plate : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The shared floor beside a room on a separate floor, where it goes back to.
|
|
79
|
+
* For a room on the shared floor, its own plate.
|
|
80
|
+
*/
|
|
81
|
+
export function roomSharedFootprint(nodes: Nodes, zoneId: string): SlabNode | null {
|
|
82
|
+
const zone = nodes[zoneId]
|
|
83
|
+
if (!isRoom(zone) || !zone.parentId) return null
|
|
84
|
+
if (!zone.floor?.footprint) return listingPlate(nodes, zone)
|
|
85
|
+
const shared = roomFloorChoices(nodes, zoneId).find((choice) => choice.key === null)
|
|
86
|
+
const plate = shared ? nodes[shared.plateId] : undefined
|
|
87
|
+
return plate?.type === 'slab' ? plate : null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type RoomBuiltOnModel = {
|
|
91
|
+
/** The floor the room stands on (checked in the picker). */
|
|
92
|
+
current: PlateChoice
|
|
93
|
+
/** Every floor it can stand on, the current one first. */
|
|
94
|
+
choices: FloorChoice[]
|
|
95
|
+
/** The shared floor beside a room on a separate floor, when one touches it. */
|
|
96
|
+
shared: PlateChoice | null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What the "Built on" picker shows, or null when it has nothing to offer: a
|
|
101
|
+
* mezzanine, a room whose floor is a drawn slab, and a room on the shared
|
|
102
|
+
* floor alone on its footprint with no other floor touching it. A room on a
|
|
103
|
+
* separate floor can always go back to the shared floor, touching one or not.
|
|
104
|
+
*/
|
|
105
|
+
export function roomBuiltOn(nodes: Nodes, zoneId: string): RoomBuiltOnModel | null {
|
|
106
|
+
const zone = nodes[zoneId]
|
|
107
|
+
if (!isGroundRoom(zone)) return null
|
|
108
|
+
if (roomDrawnFloor(nodes, zoneId)) return null
|
|
109
|
+
const choices = roomFloorChoices(nodes, zoneId)
|
|
110
|
+
const plate = listingPlate(nodes, zone)
|
|
111
|
+
const current = plate && choices.find((choice) => choice.plateId === plate.id)
|
|
112
|
+
if (!plate || !current) return null
|
|
113
|
+
const key = current.key
|
|
114
|
+
const alone = !(plate.zoneIds ?? []).some((id) => id !== zoneId && isGroundRoom(nodes[id]))
|
|
115
|
+
if (key === null && alone && choices.length === 1) return null
|
|
116
|
+
const sharedPlate = key === null ? null : roomSharedFootprint(nodes, zoneId)
|
|
117
|
+
const shared = sharedPlate
|
|
118
|
+
? (choices.find((choice) => choice.plateId === sharedPlate.id) ?? null)
|
|
119
|
+
: null
|
|
120
|
+
const offersShared = choices.some((choice) => choice.key === null)
|
|
121
|
+
return { current, choices: offersShared ? choices : [...choices, SHARED_FLOOR], shared }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The Floor row's line for a room on a separate floor: "Lanai floor · 0.15 m
|
|
126
|
+
* below Shared floor", "… above …", or "… level with …", `offset` being the
|
|
127
|
+
* step from the shared floor's walking surface to the room's
|
|
128
|
+
* (`separateFloorOffset`).
|
|
129
|
+
*/
|
|
130
|
+
export function separateFloorSummary(
|
|
131
|
+
model: { current: { name: string }; shared: { name: string } | null; offset: number | null },
|
|
132
|
+
length: (meters: number) => string,
|
|
133
|
+
): string {
|
|
134
|
+
const { current, shared, offset } = model
|
|
135
|
+
if (!shared || offset === null) return current.name
|
|
136
|
+
if (Math.abs(offset) < 0.005) return `${current.name} · level with ${shared.name}`
|
|
137
|
+
return `${current.name} · ${length(Math.abs(offset))} ${offset > 0 ? 'above' : 'below'} ${shared.name}`
|
|
138
|
+
}
|