@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,395 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
acquireSceneHistoryPause,
|
|
5
|
+
applyStructureReconciliation,
|
|
6
|
+
area,
|
|
7
|
+
DEFAULT_FOUNDATION_HEIGHT,
|
|
8
|
+
type FloorFoundationPatch,
|
|
9
|
+
generateId,
|
|
10
|
+
getRoomBaseElevation,
|
|
11
|
+
groundFloorConstruction,
|
|
12
|
+
intersection,
|
|
13
|
+
roomDrawnFloor,
|
|
14
|
+
type SlabNode,
|
|
15
|
+
setFloorFoundation,
|
|
16
|
+
setRoomFloorConstruction,
|
|
17
|
+
upperFloorHeightControl,
|
|
18
|
+
useScene,
|
|
19
|
+
} from '@pascal-app/core'
|
|
20
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
21
|
+
import useEditor from '../store/use-editor'
|
|
22
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
23
|
+
import { createSessionWrites } from './session-writes'
|
|
24
|
+
|
|
25
|
+
// "Floor & foundation": each connected footprint of a level (the house, a
|
|
26
|
+
// garden shed…) stands on one base plate. On the ground it is built bottom up:
|
|
27
|
+
// the foundation (none, or a height) on the ground, the slab (its thickness)
|
|
28
|
+
// on the foundation; the floor top follows from the two. These helpers name a
|
|
29
|
+
// footprint and read or write those settings in the words the panel uses.
|
|
30
|
+
|
|
31
|
+
export type FootprintPreset = 'ground' | 'raised'
|
|
32
|
+
|
|
33
|
+
/** A footprint raised on a foundation starts with one this high. */
|
|
34
|
+
export { DEFAULT_FOUNDATION_HEIGHT }
|
|
35
|
+
|
|
36
|
+
/** Foundations this low count as on the ground. */
|
|
37
|
+
const ON_GROUND = 0.0005
|
|
38
|
+
|
|
39
|
+
const isBase = (node: AnyNode | undefined): node is SlabNode =>
|
|
40
|
+
node?.type === 'slab' && node.plateRole === 'base'
|
|
41
|
+
|
|
42
|
+
const footprintArea = (plate: SlabNode) => area([{ outer: plate.polygon, holes: plate.holes }])
|
|
43
|
+
|
|
44
|
+
/** The level's footprints, largest first (ties by id, so the order is stable). */
|
|
45
|
+
export function levelFootprints(nodes: Record<string, AnyNode>, levelId: string): SlabNode[] {
|
|
46
|
+
return Object.values(nodes)
|
|
47
|
+
.filter((node): node is SlabNode => isBase(node) && node.parentId === levelId)
|
|
48
|
+
.sort((a, b) => footprintArea(b) - footprintArea(a) || a.id.localeCompare(b.id))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The drawn slab a room's floor is: the one it visibly stands on (a legacy
|
|
53
|
+
* floor can show a hand-drawn slab over the one it was taken from), else the
|
|
54
|
+
* one it was taken from, while it still stands on the room's level.
|
|
55
|
+
*/
|
|
56
|
+
function authoredFloorOwner(nodes: Record<string, AnyNode>, zoneId: string): SlabNode | null {
|
|
57
|
+
const zone = nodes[zoneId]
|
|
58
|
+
if (zone?.type !== 'zone' || !zone.parentId) return null
|
|
59
|
+
const drawn = roomDrawnFloor(nodes, zoneId)
|
|
60
|
+
if (drawn) return nodes[drawn.slabId] as SlabNode
|
|
61
|
+
const source = zone.floor?.sourceSlabId ? nodes[zone.floor.sourceSlabId] : undefined
|
|
62
|
+
return source?.type === 'slab' && source.parentId === zone.parentId ? source : null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What a room's floor is built as — the same owner core's
|
|
67
|
+
* `setRoomFloorConstruction` edits: the drawn slab the floor came from, else
|
|
68
|
+
* the footprint plate listing the room. Null when the room has none.
|
|
69
|
+
*/
|
|
70
|
+
export function roomFloorOwner(nodes: Record<string, AnyNode>, zoneId: string): SlabNode | null {
|
|
71
|
+
const zone = nodes[zoneId]
|
|
72
|
+
if (zone?.type !== 'zone' || !zone.parentId) return null
|
|
73
|
+
return (
|
|
74
|
+
authoredFloorOwner(nodes, zoneId) ??
|
|
75
|
+
levelFootprints(nodes, zone.parentId).find((plate) => plate.zoneIds?.includes(zoneId)) ??
|
|
76
|
+
null
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The footprint a room stands on: the one listing it, else the one it
|
|
82
|
+
* overlaps most — unless its floor is a drawn slab, which is its own and not
|
|
83
|
+
* a footprint's (a plate that only brushes it is not where its floor is).
|
|
84
|
+
*/
|
|
85
|
+
export function roomFootprint(nodes: Record<string, AnyNode>, zoneId: string): SlabNode | null {
|
|
86
|
+
const zone = nodes[zoneId]
|
|
87
|
+
if (zone?.type !== 'zone' || !zone.parentId) return null
|
|
88
|
+
const plates = levelFootprints(nodes, zone.parentId)
|
|
89
|
+
const listed = plates.find((plate) => plate.zoneIds?.includes(zoneId))
|
|
90
|
+
if (listed) return listed
|
|
91
|
+
if (authoredFloorOwner(nodes, zoneId)) return null
|
|
92
|
+
let best: SlabNode | null = null
|
|
93
|
+
let bestOverlap = 0
|
|
94
|
+
for (const plate of plates) {
|
|
95
|
+
const overlap = area(
|
|
96
|
+
intersection(
|
|
97
|
+
{ outer: plate.polygon, holes: plate.holes },
|
|
98
|
+
{ outer: zone.polygon, holes: zone.holes },
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
if (overlap > bestOverlap) {
|
|
102
|
+
best = plate
|
|
103
|
+
bestOverlap = overlap
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return best
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The floor top an upper footprint rests at by default: mirrors core's
|
|
111
|
+
* `automaticFloorHeight` — its rooms' base heights read with the base plates
|
|
112
|
+
* set aside.
|
|
113
|
+
*/
|
|
114
|
+
export function groundFloorHeight(nodes: Record<string, AnyNode>, plate: SlabNode): number {
|
|
115
|
+
const withoutBase = Object.fromEntries(Object.entries(nodes).filter(([, node]) => !isBase(node)))
|
|
116
|
+
const heights = (plate.zoneIds ?? []).map((id) => getRoomBaseElevation(withoutBase, id))
|
|
117
|
+
return heights.length ? Math.max(...heights) : 0.05
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* How far an upper floor's top is raised above its default position (resting
|
|
122
|
+
* on the walls below), meters; negative when set lower than its default.
|
|
123
|
+
*/
|
|
124
|
+
export function floorLift(nodes: Record<string, AnyNode>, plate: SlabNode): number {
|
|
125
|
+
return plate.elevation - groundFloorHeight(nodes, plate)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A ground footprint's foundation height, meters: 0 on the ground. Derived
|
|
130
|
+
* from the floor top, so a legacy slab reaching below the ground reads 0.
|
|
131
|
+
*/
|
|
132
|
+
export function foundationHeight(nodes: Record<string, AnyNode>, plate: SlabNode): number {
|
|
133
|
+
return groundFloorConstruction(nodes, plate).foundationHeight
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A ground footprint's floor top above the ground: foundation + slab. Read-only. */
|
|
137
|
+
export function floorTopAboveGround(nodes: Record<string, AnyNode>, plate: SlabNode): number {
|
|
138
|
+
const { top, grade } = groundFloorConstruction(nodes, plate)
|
|
139
|
+
return Math.round((top - grade) * 1e6) / 1e6
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The one height a footprint's handle and panel field edit. On the ground it
|
|
144
|
+
* is the foundation height (the slab stays on it; the top follows); an upper
|
|
145
|
+
* floor rests on the walls below, so its height is its floor top, and raising
|
|
146
|
+
* it thickens the plate — the underside stays, the storeys above ride up.
|
|
147
|
+
*/
|
|
148
|
+
export function footprintHeightValue(nodes: Record<string, AnyNode>, plate: SlabNode): number {
|
|
149
|
+
return upperFloorHeightControl(nodes, plate)?.currentTop ?? foundationHeight(nodes, plate)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The lowest height the handle and field go to: on the ground (no
|
|
154
|
+
* foundation); upstairs, resting on the walls below. An upper floor already
|
|
155
|
+
* lower starts from where it is, so the first drag never jumps.
|
|
156
|
+
*/
|
|
157
|
+
export function footprintHeightMinimum(nodes: Record<string, AnyNode>, plate: SlabNode): number {
|
|
158
|
+
const upper = upperFloorHeightControl(nodes, plate)
|
|
159
|
+
return upper ? Math.min(upper.currentTop, upper.minimumTop) : 0
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The patch a footprint height writes: upstairs the plate's thickness, on the ground its foundation. */
|
|
163
|
+
export function footprintHeightPatch(
|
|
164
|
+
nodes: Record<string, AnyNode>,
|
|
165
|
+
plate: SlabNode,
|
|
166
|
+
value: number,
|
|
167
|
+
): FloorFoundationPatch {
|
|
168
|
+
const upper = upperFloorHeightControl(nodes, plate, value)
|
|
169
|
+
return upper
|
|
170
|
+
? { thickness: upper.write.thickness }
|
|
171
|
+
: { foundationHeight: Math.max(0, Math.round(value * 1e6) / 1e6) }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const THICK_FLOOR_HINT = 'Very thick floor — a mezzanine may fit better.'
|
|
175
|
+
|
|
176
|
+
/** A plate thick enough that a mezzanine would likely serve better (core's advice; never a refusal). */
|
|
177
|
+
export function thickFloorAdvice(nodes: Record<string, AnyNode>, plate: SlabNode, top?: number) {
|
|
178
|
+
return upperFloorHeightControl(nodes, plate, top)?.advice === 'thick-floor'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** On the ground or raised on a foundation: raised exactly when the foundation has a height. */
|
|
182
|
+
export function footprintPreset(nodes: Record<string, AnyNode>, plate: SlabNode): FootprintPreset {
|
|
183
|
+
return foundationHeight(nodes, plate) > ON_GROUND ? 'raised' : 'ground'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The patch a preset choice writes: only the foundation height. Core's
|
|
188
|
+
* `setFloorFoundation` makes the foundation solid while it has a height. Raising
|
|
189
|
+
* keeps a height already set.
|
|
190
|
+
*/
|
|
191
|
+
export function presetPatch(
|
|
192
|
+
nodes: Record<string, AnyNode>,
|
|
193
|
+
plate: SlabNode,
|
|
194
|
+
preset: FootprintPreset,
|
|
195
|
+
): FloorFoundationPatch {
|
|
196
|
+
if (preset === 'ground') return { foundationHeight: 0 }
|
|
197
|
+
const current = foundationHeight(nodes, plate)
|
|
198
|
+
return { foundationHeight: current > ON_GROUND ? current : DEFAULT_FOUNDATION_HEIGHT }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Writes Floor & foundation settings through core's `setFloorFoundation`: the
|
|
203
|
+
* footprint's floor, walls, openings and room floors move together, in one
|
|
204
|
+
* undo step. Returns why it was refused, or null.
|
|
205
|
+
*/
|
|
206
|
+
export function applyFloorFoundation(
|
|
207
|
+
slabId: string | readonly string[],
|
|
208
|
+
patch: FloorFoundationPatch,
|
|
209
|
+
): string | null {
|
|
210
|
+
const nodes = useScene.getState().nodes
|
|
211
|
+
const plan =
|
|
212
|
+
typeof slabId === 'string'
|
|
213
|
+
? setFloorFoundation(nodes, { slabId, patch })
|
|
214
|
+
: setFloorFoundation(nodes, { slabIds: [...slabId], patch })
|
|
215
|
+
if (plan.conflicts?.length) return plan.conflicts.map((conflict) => conflict.message).join(' ')
|
|
216
|
+
applyRoomPlan(plan)
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Edits a room's floor construction through core's `setRoomFloorConstruction`
|
|
222
|
+
* — the one command the room panel and MCP share. It picks the room's floor
|
|
223
|
+
* owner (a drawn slab, or the footprint plate listing the room). A numeric
|
|
224
|
+
* floorHeight moves a drawn slab's top; only a ground footprint has a
|
|
225
|
+
* foundation. Returns why a change was refused, or null.
|
|
226
|
+
*/
|
|
227
|
+
export function applyRoomFloorConstruction(
|
|
228
|
+
zoneId: string,
|
|
229
|
+
patch: FloorFoundationPatch,
|
|
230
|
+
slabId?: string,
|
|
231
|
+
): string | null {
|
|
232
|
+
const plan = setRoomFloorConstruction(useScene.getState().nodes, { zoneId, slabId, patch })
|
|
233
|
+
if (plan.conflicts?.length) return plan.conflicts.map((conflict) => conflict.message).join(' ')
|
|
234
|
+
applyRoomPlan(plan)
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const SHARED_STOREY = 'floor-foundation-shared-storey'
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* When a change to one footprint is refused only because an upper floor (or a
|
|
242
|
+
* roof, a ceiling) also stands on other footprints, the footprints that would
|
|
243
|
+
* have to move with it — this one first, so the patch stays its own — if
|
|
244
|
+
* moving them together is accepted. Otherwise null.
|
|
245
|
+
*/
|
|
246
|
+
export function footprintsToMoveTogether(
|
|
247
|
+
nodes: Record<string, AnyNode>,
|
|
248
|
+
slabId: string,
|
|
249
|
+
patch: FloorFoundationPatch,
|
|
250
|
+
): string[] | null {
|
|
251
|
+
const conflicts = setFloorFoundation(nodes, { slabId, patch }).conflicts ?? []
|
|
252
|
+
if (conflicts.length === 0 || conflicts.some((conflict) => conflict.code !== SHARED_STOREY))
|
|
253
|
+
return null
|
|
254
|
+
const ids = new Set([slabId])
|
|
255
|
+
for (const conflict of conflicts)
|
|
256
|
+
for (const id of conflict.nodeIds) if (isBase(nodes[id])) ids.add(id)
|
|
257
|
+
if (ids.size < 2) return null
|
|
258
|
+
const together = setFloorFoundation(nodes, { slabIds: [...ids], patch })
|
|
259
|
+
return together.conflicts?.length ? null : [...ids]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Sets a footprint's height (`footprintHeightValue`'s terms). */
|
|
263
|
+
export function setFootprintHeight(slabId: string, value: number): string | null {
|
|
264
|
+
const plate = useScene.getState().nodes[slabId as AnyNodeId]
|
|
265
|
+
if (!isBase(plate)) return 'The floor is gone.'
|
|
266
|
+
return applyFloorFoundation(slabId, footprintHeightPatch(useScene.getState().nodes, plate, value))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
type NodeMap = Record<string, AnyNode>
|
|
270
|
+
|
|
271
|
+
/** Every level of the building a level belongs to (just that level when it has none). */
|
|
272
|
+
function buildingLevelIds(nodes: NodeMap, levelId: string | null | undefined): string[] {
|
|
273
|
+
const level = levelId ? nodes[levelId] : undefined
|
|
274
|
+
if (level?.type !== 'level') return []
|
|
275
|
+
const building = level.parentId ? nodes[level.parentId] : undefined
|
|
276
|
+
if (building?.type !== 'building') return [level.id]
|
|
277
|
+
return building.children.filter((id) => nodes[id]?.type === 'level')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A live height preview (a handle drag, a scrub): each value moves the
|
|
282
|
+
* building outside history; `commit` takes the preview's writes back and
|
|
283
|
+
* writes the final height once — one undo step and one shared change whose
|
|
284
|
+
* "before" is what everyone else has. (Writing the start height back through
|
|
285
|
+
* the command instead is not exact: a foundation finish picked up on the way
|
|
286
|
+
* would stay, and collaborators would refuse the change.)
|
|
287
|
+
*/
|
|
288
|
+
export function beginFootprintHeightPreview(slabId: string) {
|
|
289
|
+
return beginFootprintPreview(slabId, setFootprintHeight, footprintHeightValue)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Sets a ground footprint's slab thickness: the top and everything on it follow. */
|
|
293
|
+
export function setFootprintThickness(slabId: string, value: number): string | null {
|
|
294
|
+
return applyFloorFoundation(slabId, { thickness: value })
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** A live slab-thickness preview (a scrub): the same session as the height's, one undo step. */
|
|
298
|
+
export function beginFootprintThicknessPreview(slabId: string) {
|
|
299
|
+
return beginFootprintPreview(slabId, setFootprintThickness, (_, plate) => plate.thickness)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function beginFootprintPreview(
|
|
303
|
+
slabId: string,
|
|
304
|
+
write: (slabId: string, value: number) => string | null,
|
|
305
|
+
read: (nodes: Record<string, AnyNode>, plate: SlabNode) => number,
|
|
306
|
+
) {
|
|
307
|
+
let writes = createSessionWrites()
|
|
308
|
+
let release: (() => void) | null = null
|
|
309
|
+
const end = () => {
|
|
310
|
+
if (!release) return false
|
|
311
|
+
release()
|
|
312
|
+
release = null
|
|
313
|
+
writes.revert()
|
|
314
|
+
writes = createSessionWrites()
|
|
315
|
+
return true
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
preview(value: number): string | null {
|
|
319
|
+
release ??= acquireSceneHistoryPause(useScene)
|
|
320
|
+
return writes.record(() => {
|
|
321
|
+
const before = useScene.getState().nodes
|
|
322
|
+
const refused = write(slabId, value)
|
|
323
|
+
// Paused writes skip the structure reconciliation a commit runs (the
|
|
324
|
+
// plates' derived heights, the rooms on them); run it here so the
|
|
325
|
+
// building follows the preview.
|
|
326
|
+
// An upper floor's storeys above ride up with it: reconcile the building.
|
|
327
|
+
const levelIds = buildingLevelIds(before, before[slabId as AnyNodeId]?.parentId)
|
|
328
|
+
if (!refused && levelIds.length)
|
|
329
|
+
applyStructureReconciliation(useScene, {
|
|
330
|
+
levelIds,
|
|
331
|
+
previousNodes: before,
|
|
332
|
+
mintId: generateId,
|
|
333
|
+
})
|
|
334
|
+
return refused
|
|
335
|
+
})
|
|
336
|
+
},
|
|
337
|
+
commit(value: number): string | null {
|
|
338
|
+
end()
|
|
339
|
+
const plate = useScene.getState().nodes[slabId as AnyNodeId]
|
|
340
|
+
if (isBase(plate) && Math.abs(read(useScene.getState().nodes, plate) - value) < 1e-6)
|
|
341
|
+
return null
|
|
342
|
+
return write(slabId, value)
|
|
343
|
+
},
|
|
344
|
+
cancel() {
|
|
345
|
+
end()
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Opens a footprint's Floor & foundation panel: its base plate becomes the selection. */
|
|
351
|
+
export function openFloorFoundation(slabId: string) {
|
|
352
|
+
useEditor.getState().clearRoom()
|
|
353
|
+
useViewer.getState().setSelection({ selectedIds: [slabId as SlabNode['id']] })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** A base plate's own outside faces: what a click opens Floor & foundation for. */
|
|
357
|
+
const FOOTPRINT_ROLES = new Set(['edge', 'foundation', 'side', 'riser', 'underside', 'surface'])
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Whether a pointer hit is on a footprint's floor edge band or foundation (not
|
|
361
|
+
* a room's floor, step or edge): a click there opens that footprint's Floor &
|
|
362
|
+
* foundation panel rather than the room above it.
|
|
363
|
+
*/
|
|
364
|
+
export function isFootprintConstructionHit(
|
|
365
|
+
node: AnyNode | undefined,
|
|
366
|
+
object: { userData?: Record<string, unknown> } | null | undefined,
|
|
367
|
+
): boolean {
|
|
368
|
+
if (!isBase(node)) return false
|
|
369
|
+
const role = object?.userData?.paintRole
|
|
370
|
+
return typeof role === 'string' && FOOTPRINT_ROLES.has(role)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Where a click on a room-owned plate lands once it is past the room (the
|
|
375
|
+
* drill click, or Alt): a raised or sunken room's plate is that room's floor,
|
|
376
|
+
* so the drill continues to the footprint floor the room stands on — where a
|
|
377
|
+
* ground-level room's drill lands too. A mezzanine's deck has none (null): it
|
|
378
|
+
* stays on its mezzanine.
|
|
379
|
+
*/
|
|
380
|
+
export function roomOwnedPlateDrillTarget(
|
|
381
|
+
nodes: Record<string, AnyNode>,
|
|
382
|
+
plate: SlabNode,
|
|
383
|
+
): SlabNode | null {
|
|
384
|
+
if (plate.support === 'open') return null
|
|
385
|
+
const zoneId = plate.zoneIds?.[0]
|
|
386
|
+
return zoneId ? roomFootprint(nodes, zoneId) : null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** A raised or sunken room's plate, or a mezzanine's: its room is what gets picked. */
|
|
390
|
+
export function isRoomOwnedPlate(node: AnyNode | undefined): boolean {
|
|
391
|
+
return (
|
|
392
|
+
node?.type === 'slab' &&
|
|
393
|
+
(node.plateRole === 'platform' || node.plateRole === 'sunken' || node.support === 'open')
|
|
394
|
+
)
|
|
395
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
BuildingNode,
|
|
6
|
+
createZone,
|
|
7
|
+
floorFootprintSupportClass,
|
|
8
|
+
generateId,
|
|
9
|
+
getCeilingClampBound,
|
|
10
|
+
getLevelElevations,
|
|
11
|
+
initSpaceDetectionSync,
|
|
12
|
+
LevelNode,
|
|
13
|
+
type SlabNode,
|
|
14
|
+
upperFloorHeightControl,
|
|
15
|
+
useScene,
|
|
16
|
+
} from '@pascal-app/core'
|
|
17
|
+
import { createElement } from 'react'
|
|
18
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
19
|
+
import {
|
|
20
|
+
FloorSection,
|
|
21
|
+
floorFoundationModel,
|
|
22
|
+
floorPlateKicker,
|
|
23
|
+
} from '../components/ui/panels/floor-foundation-panel'
|
|
24
|
+
import { installImmediateAnimationFrames } from '../test-utils/immediate-animation-frames'
|
|
25
|
+
import {
|
|
26
|
+
applyFloorFoundation,
|
|
27
|
+
beginFootprintHeightPreview,
|
|
28
|
+
beginFootprintThicknessPreview,
|
|
29
|
+
footprintHeightMinimum,
|
|
30
|
+
footprintHeightPatch,
|
|
31
|
+
footprintHeightValue,
|
|
32
|
+
levelFootprints,
|
|
33
|
+
presetPatch,
|
|
34
|
+
setFootprintHeight,
|
|
35
|
+
thickFloorAdvice,
|
|
36
|
+
} from './floor-footprints'
|
|
37
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
38
|
+
|
|
39
|
+
const GROUND = 'level_support_ground'
|
|
40
|
+
const UPPER = 'level_support_upper'
|
|
41
|
+
let stop = () => {}
|
|
42
|
+
let restoreFrames = () => {}
|
|
43
|
+
const nodes = () => useScene.getState().nodes as Record<string, AnyNode>
|
|
44
|
+
|
|
45
|
+
const rect = (x0: number, z0: number, x1: number, z1: number): [number, number][] => [
|
|
46
|
+
[x0, z0],
|
|
47
|
+
[x1, z0],
|
|
48
|
+
[x1, z1],
|
|
49
|
+
[x0, z1],
|
|
50
|
+
]
|
|
51
|
+
const room = (levelId: string, polygon: [number, number][], name: string) =>
|
|
52
|
+
applyRoomPlan(createZone(nodes(), { levelId, polygon, enclose: true, mintId: generateId, name }))
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
restoreFrames = installImmediateAnimationFrames()
|
|
56
|
+
const building = BuildingNode.parse({ id: 'building_support', children: [GROUND, UPPER] })
|
|
57
|
+
const ground = LevelNode.parse({
|
|
58
|
+
id: GROUND,
|
|
59
|
+
parentId: building.id,
|
|
60
|
+
level: 0,
|
|
61
|
+
name: 'Ground floor',
|
|
62
|
+
})
|
|
63
|
+
const upper = LevelNode.parse({ id: UPPER, parentId: building.id, level: 1, name: 'Floor 1' })
|
|
64
|
+
useScene.setState({
|
|
65
|
+
nodes: { [building.id]: building, [ground.id]: ground, [upper.id]: upper },
|
|
66
|
+
rootNodeIds: [building.id],
|
|
67
|
+
dirtyNodes: new Set(),
|
|
68
|
+
materials: {},
|
|
69
|
+
collections: {},
|
|
70
|
+
readOnly: false,
|
|
71
|
+
})
|
|
72
|
+
useScene.temporal.getState().resume()
|
|
73
|
+
stop = initSpaceDetectionSync(useScene, { getState: () => ({ spaces: {}, setSpaces: () => {} }) })
|
|
74
|
+
room(GROUND, rect(0, 0, 8, 5), 'Living')
|
|
75
|
+
room(UPPER, rect(0, 0, 8, 5), 'Bedroom')
|
|
76
|
+
// A wing upstairs with nothing under it.
|
|
77
|
+
room(UPPER, rect(20, 0, 24, 4), 'Studio')
|
|
78
|
+
})
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
stop()
|
|
81
|
+
restoreFrames()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const plateOf = (levelId: string, name: string) =>
|
|
85
|
+
levelFootprints(nodes(), levelId).find((plate) =>
|
|
86
|
+
plate.zoneIds?.some((id) => nodes()[id]?.name === name),
|
|
87
|
+
) as SlabNode
|
|
88
|
+
|
|
89
|
+
describe('Floor & foundation support class (point 5)', () => {
|
|
90
|
+
test('a floor over the storey below is supported; the ground and a free wing bear on the ground', () => {
|
|
91
|
+
const house = plateOf(GROUND, 'Living')
|
|
92
|
+
const bedroom = plateOf(UPPER, 'Bedroom')
|
|
93
|
+
const studio = plateOf(UPPER, 'Studio')
|
|
94
|
+
expect(floorFootprintSupportClass(nodes(), house)).toBe('ground-bearing')
|
|
95
|
+
expect(floorFootprintSupportClass(nodes(), bedroom)).toBe('supported')
|
|
96
|
+
expect(floorFootprintSupportClass(nodes(), studio)).toBe('ground-bearing')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('the kicker says where the plate is and what it rests on', () => {
|
|
100
|
+
expect(floorPlateKicker(nodes(), plateOf(GROUND, 'Living'))).toBe('Floor plate · Ground floor')
|
|
101
|
+
expect(floorPlateKicker(nodes(), plateOf(UPPER, 'Bedroom'))).toBe(
|
|
102
|
+
'Floor plate · Floor 1 · rests on Ground floor',
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('the height handle drags down to the ground, or upstairs to resting on the walls below', () => {
|
|
107
|
+
const house = plateOf(GROUND, 'Living')
|
|
108
|
+
const bedroom = plateOf(UPPER, 'Bedroom')
|
|
109
|
+
expect(footprintHeightValue(nodes(), house)).toBe(0)
|
|
110
|
+
expect(footprintHeightMinimum(nodes(), house)).toBe(0)
|
|
111
|
+
const control = upperFloorHeightControl(nodes(), bedroom)!
|
|
112
|
+
expect(footprintHeightValue(nodes(), bedroom)).toBeCloseTo(control.currentTop)
|
|
113
|
+
expect(footprintHeightMinimum(nodes(), bedroom)).toBeCloseTo(control.minimumTop)
|
|
114
|
+
// A legacy plate hanging below its resting position starts where it is: no jump.
|
|
115
|
+
const legacy = { ...bedroom, elevation: bedroom.elevation - 0.1, thickness: 0.3 }
|
|
116
|
+
const legacyNodes = { ...nodes(), [legacy.id]: legacy }
|
|
117
|
+
expect(footprintHeightMinimum(legacyNodes, legacy)).toBeCloseTo(
|
|
118
|
+
footprintHeightValue(legacyNodes, legacy),
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('raising an upper floor thickens it: the underside stays, the storey above rides up', () => {
|
|
123
|
+
const floor2 = LevelNode.parse({
|
|
124
|
+
id: 'level_support_2',
|
|
125
|
+
parentId: 'building_support',
|
|
126
|
+
level: 2,
|
|
127
|
+
})
|
|
128
|
+
const building = nodes().building_support as BuildingNode
|
|
129
|
+
useScene.getState().createNode(floor2, building.id)
|
|
130
|
+
room(floor2.id, rect(0, 0, 8, 5), 'Attic')
|
|
131
|
+
const bedroom = plateOf(UPPER, 'Bedroom')
|
|
132
|
+
const underside = bedroom.elevation - bedroom.thickness
|
|
133
|
+
const groundCeiling = getCeilingClampBound(GROUND as AnyNodeId, nodes(), rect(0, 0, 8, 5))
|
|
134
|
+
const atticBefore = getLevelElevations(nodes()).get(floor2.id)!.baseY
|
|
135
|
+
const top = footprintHeightValue(nodes(), bedroom) + 0.25
|
|
136
|
+
expect(footprintHeightPatch(nodes(), bedroom, top)).toEqual({
|
|
137
|
+
thickness: expect.closeTo(bedroom.thickness + 0.25, 6),
|
|
138
|
+
})
|
|
139
|
+
expect(setFootprintHeight(bedroom.id, top)).toBeNull()
|
|
140
|
+
const raised = nodes()[bedroom.id] as SlabNode
|
|
141
|
+
expect(raised.elevation - raised.thickness).toBeCloseTo(underside)
|
|
142
|
+
expect(raised.elevation).toBeCloseTo(bedroom.elevation + 0.25)
|
|
143
|
+
expect(getCeilingClampBound(GROUND as AnyNodeId, nodes(), rect(0, 0, 8, 5))).toBeCloseTo(
|
|
144
|
+
groundCeiling,
|
|
145
|
+
)
|
|
146
|
+
expect(getLevelElevations(nodes()).get(floor2.id)!.baseY).toBeCloseTo(atticBefore + 0.25)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('a very thick upper floor advises a mezzanine, never refuses', () => {
|
|
150
|
+
const bedroom = plateOf(UPPER, 'Bedroom')
|
|
151
|
+
const underside = bedroom.elevation - bedroom.thickness
|
|
152
|
+
expect(thickFloorAdvice(nodes(), bedroom, underside + 0.35)).toBe(false)
|
|
153
|
+
expect(thickFloorAdvice(nodes(), bedroom, underside + 0.45)).toBe(true)
|
|
154
|
+
expect(setFootprintHeight(bedroom.id, underside + 0.45)).toBeNull()
|
|
155
|
+
expect(floorFoundationModel(nodes(), nodes()[bedroom.id] as SlabNode).thickFloor).toBe(true)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('Upper-floor height drag release (round-final item 6)', () => {
|
|
160
|
+
// The handle and the panel scrub both preview live and land once on release:
|
|
161
|
+
// every release, small or thick, must write the value as one undo step.
|
|
162
|
+
for (const delta of [0.01, 0.05, 0.2, 0.5, -0.02]) {
|
|
163
|
+
test(`a drag of ${delta} m lands on release`, () => {
|
|
164
|
+
const bedroom = plateOf(UPPER, 'Bedroom')
|
|
165
|
+
const start = footprintHeightValue(nodes(), bedroom)
|
|
166
|
+
const past = useScene.temporal.getState().pastStates.length
|
|
167
|
+
const preview = beginFootprintHeightPreview(bedroom.id)
|
|
168
|
+
expect(preview.preview(start + delta / 2)).toBeNull()
|
|
169
|
+
expect(preview.preview(start + delta)).toBeNull()
|
|
170
|
+
expect(preview.commit(start + delta)).toBeNull()
|
|
171
|
+
const after = nodes()[bedroom.id] as SlabNode
|
|
172
|
+
expect(after.thickness).toBeCloseTo(bedroom.thickness + delta, 6)
|
|
173
|
+
expect(footprintHeightValue(nodes(), after)).toBeCloseTo(start + delta, 6)
|
|
174
|
+
expect(useScene.temporal.getState().pastStates.length).toBe(past + 1)
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('Floor & foundation panel (point 5)', () => {
|
|
180
|
+
test('a ground floor gets the foundation group; an upper floor never offers the ground', () => {
|
|
181
|
+
const ground = floorFoundationModel(nodes(), plateOf(GROUND, 'Living'))
|
|
182
|
+
expect(ground).toMatchObject({ upper: false, preset: 'ground', underFloor: true, minHeight: 0 })
|
|
183
|
+
const upper = floorFoundationModel(nodes(), plateOf(UPPER, 'Bedroom'))
|
|
184
|
+
expect(upper).toMatchObject({ upper: true, underFloor: false, thickFloor: false })
|
|
185
|
+
expect(upper.kicker).toBe('Floor plate · Floor 1 · rests on Ground floor')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('on the ground the two inputs are slab thickness and foundation height; the top follows', () => {
|
|
189
|
+
const living = plateOf(GROUND, 'Living')
|
|
190
|
+
const start = floorFoundationModel(nodes(), living)
|
|
191
|
+
expect(start).toMatchObject({ preset: 'ground', height: 0, minThickness: 0.01 })
|
|
192
|
+
expect(start.floorTop).toBeCloseTo(living.thickness)
|
|
193
|
+
expect(applyFloorFoundation(living.id, { thickness: 0.1 })).toBeNull()
|
|
194
|
+
let plate = nodes()[living.id] as SlabNode
|
|
195
|
+
expect(floorFoundationModel(nodes(), plate)).toMatchObject({
|
|
196
|
+
preset: 'ground',
|
|
197
|
+
height: 0,
|
|
198
|
+
floorTop: expect.closeTo(0.1),
|
|
199
|
+
})
|
|
200
|
+
expect(applyFloorFoundation(living.id, presetPatch(nodes(), plate, 'raised'))).toBeNull()
|
|
201
|
+
plate = nodes()[living.id] as SlabNode
|
|
202
|
+
expect(floorFoundationModel(nodes(), plate)).toMatchObject({
|
|
203
|
+
preset: 'raised',
|
|
204
|
+
height: expect.closeTo(0.3),
|
|
205
|
+
floorTop: expect.closeTo(0.4),
|
|
206
|
+
})
|
|
207
|
+
expect(plate.foundation?.type).toBe('solid')
|
|
208
|
+
expect(applyFloorFoundation(living.id, presetPatch(nodes(), plate, 'ground'))).toBeNull()
|
|
209
|
+
plate = nodes()[living.id] as SlabNode
|
|
210
|
+
expect(floorFoundationModel(nodes(), plate)).toMatchObject({
|
|
211
|
+
preset: 'ground',
|
|
212
|
+
height: 0,
|
|
213
|
+
floorTop: expect.closeTo(0.1),
|
|
214
|
+
})
|
|
215
|
+
expect(plate.foundation?.type).toBe('none')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('a slab thickness scrub moves the building live and lands as one undo step', () => {
|
|
219
|
+
const living = plateOf(GROUND, 'Living')
|
|
220
|
+
useScene.temporal.getState().clear()
|
|
221
|
+
const past = 0
|
|
222
|
+
const scrub = beginFootprintThicknessPreview(living.id)
|
|
223
|
+
expect(scrub.preview(0.2)).toBeNull()
|
|
224
|
+
const live = nodes()[living.id] as SlabNode
|
|
225
|
+
expect(live.thickness).toBeCloseTo(0.2)
|
|
226
|
+
expect(live.elevation).toBeCloseTo(0.2)
|
|
227
|
+
expect(useScene.temporal.getState().pastStates.length).toBe(past)
|
|
228
|
+
expect(scrub.preview(0.12)).toBeNull()
|
|
229
|
+
expect(scrub.commit(0.12)).toBeNull()
|
|
230
|
+
const after = nodes()[living.id] as SlabNode
|
|
231
|
+
expect(after.thickness).toBeCloseTo(0.12)
|
|
232
|
+
expect(after.elevation).toBeCloseTo(0.12)
|
|
233
|
+
expect(useScene.temporal.getState().pastStates.length).toBe(past + 1)
|
|
234
|
+
useScene.temporal.getState().undo()
|
|
235
|
+
expect((nodes()[living.id] as SlabNode).thickness).toBeCloseTo(living.thickness)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test('upstairs the one control is the thickness; the floor top above the ground is its caption', () => {
|
|
239
|
+
const bedroom = plateOf(UPPER, 'Bedroom')
|
|
240
|
+
const before = floorFoundationModel(nodes(), bedroom)
|
|
241
|
+
const base = getLevelElevations(nodes()).get(UPPER as AnyNodeId)!.baseY
|
|
242
|
+
expect(before.floorTop).toBeCloseTo(base + before.height)
|
|
243
|
+
expect(setFootprintHeight(bedroom.id, before.height + 0.1)).toBeNull()
|
|
244
|
+
const after = floorFoundationModel(nodes(), nodes()[bedroom.id] as SlabNode)
|
|
245
|
+
expect((nodes()[bedroom.id] as SlabNode).thickness).toBeCloseTo(bedroom.thickness + 0.1)
|
|
246
|
+
expect(after.floorTop).toBeCloseTo(before.floorTop + 0.1)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('the section draws top to bottom in build order, labelled not to scale', () => {
|
|
250
|
+
const section = (props: Partial<Parameters<typeof FloorSection>[0]>) =>
|
|
251
|
+
renderToStaticMarkup(
|
|
252
|
+
createElement(FloorSection, {
|
|
253
|
+
supported: false,
|
|
254
|
+
raised: false,
|
|
255
|
+
thickness: 0.2,
|
|
256
|
+
thicknessLabel: '0.20 m',
|
|
257
|
+
...props,
|
|
258
|
+
}),
|
|
259
|
+
)
|
|
260
|
+
const raised = section({
|
|
261
|
+
raised: true,
|
|
262
|
+
foundationHeight: 0.5,
|
|
263
|
+
foundationLabel: '0.50 m',
|
|
264
|
+
topLabel: '0.70 m',
|
|
265
|
+
})
|
|
266
|
+
const order = ['Floor top', 'Slab · 0.20 m', 'Foundation · 0.50 m', 'Ground'].map((label) =>
|
|
267
|
+
raised.indexOf(`>${label}<`),
|
|
268
|
+
)
|
|
269
|
+
expect(order.every((at) => at >= 0)).toBe(true)
|
|
270
|
+
expect([...order].sort((a, b) => a - b)).toEqual(order)
|
|
271
|
+
// The floor top is measured from the ground: foundation + slab.
|
|
272
|
+
expect(raised).toContain('>0.70 m<')
|
|
273
|
+
const onGround = section({ topLabel: '0.20 m' })
|
|
274
|
+
expect(onGround).not.toContain('data-section-foundation')
|
|
275
|
+
expect(onGround).toContain('>0.20 m<')
|
|
276
|
+
expect(raised).toContain('Not to scale')
|
|
277
|
+
const upstairs = section({ supported: true, lift: 0.25, liftLabel: '+0.25 m' })
|
|
278
|
+
expect(upstairs).toContain('>Walls below<')
|
|
279
|
+
expect(upstairs).not.toContain('>Foundation<')
|
|
280
|
+
expect(upstairs).not.toContain('>Ground<')
|
|
281
|
+
expect(section({})).not.toContain('Floor edge band')
|
|
282
|
+
})
|
|
283
|
+
})
|