@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,90 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { type AnyNode, ZoneNode } from '@pascal-app/core'
|
|
3
|
+
import {
|
|
4
|
+
MEZZANINE_NAME_SECTION,
|
|
5
|
+
numberedRoomName,
|
|
6
|
+
ROOM_NAME_SECTIONS,
|
|
7
|
+
roomNameContext,
|
|
8
|
+
roomNameSections,
|
|
9
|
+
roomNameUseCount,
|
|
10
|
+
} from './room-name-catalog'
|
|
11
|
+
|
|
12
|
+
const square: [number, number][] = [
|
|
13
|
+
[0, 0],
|
|
14
|
+
[4, 0],
|
|
15
|
+
[4, 4],
|
|
16
|
+
[0, 4],
|
|
17
|
+
]
|
|
18
|
+
function zone(id: string, patch: Record<string, unknown> = {}) {
|
|
19
|
+
return ZoneNode.parse({
|
|
20
|
+
id,
|
|
21
|
+
name: '',
|
|
22
|
+
spaceRole: 'room',
|
|
23
|
+
parentId: 'level_names',
|
|
24
|
+
polygon: square,
|
|
25
|
+
boundaryWallIds: ['wall_a'],
|
|
26
|
+
...patch,
|
|
27
|
+
}) as AnyNode
|
|
28
|
+
}
|
|
29
|
+
const graph = (...zones: AnyNode[]) => Object.fromEntries(zones.map((z) => [z.id, z]))
|
|
30
|
+
const ids = (nodes: Record<string, AnyNode>, zoneId: string) =>
|
|
31
|
+
roomNameSections(nodes, zoneId).map((section) => section.id)
|
|
32
|
+
|
|
33
|
+
describe('room name catalog', () => {
|
|
34
|
+
test('every catalog name is sentence case and listed once', () => {
|
|
35
|
+
const names = ROOM_NAME_SECTIONS.flatMap((section) => section.names)
|
|
36
|
+
expect(new Set(names).size).toBe(names.length)
|
|
37
|
+
for (const name of [...names, ...MEZZANINE_NAME_SECTION.names])
|
|
38
|
+
expect(name[0]).toBe(name[0]!.toUpperCase())
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('an indoor room lists the catalog in the usual order: no project names, no mezzanine names', () => {
|
|
42
|
+
const nodes = graph(
|
|
43
|
+
zone('zone_a', { name: 'Kitchen' }),
|
|
44
|
+
zone('zone_c', { name: 'Snug' }),
|
|
45
|
+
zone('zone_d', { name: '' }),
|
|
46
|
+
)
|
|
47
|
+
expect(ids(nodes, 'zone_d')).toEqual(ROOM_NAME_SECTIONS.map((s) => s.id))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('a picked name that is taken gets the next free number; counts ignore the number', () => {
|
|
51
|
+
const nodes = graph(
|
|
52
|
+
zone('zone_a', { name: 'Bedroom' }),
|
|
53
|
+
zone('zone_b', { name: 'Bedroom 2' }),
|
|
54
|
+
zone('zone_c', { name: 'Kitchen' }),
|
|
55
|
+
zone('zone_self', { name: 'Bedroom 3' }),
|
|
56
|
+
)
|
|
57
|
+
expect(roomNameUseCount(nodes, 'zone_self', 'Bedroom')).toBe(2)
|
|
58
|
+
expect(roomNameUseCount(nodes, 'zone_self', 'Office')).toBe(0)
|
|
59
|
+
expect(numberedRoomName(nodes, 'zone_self', 'Bedroom')).toBe('Bedroom 3')
|
|
60
|
+
expect(numberedRoomName(nodes, 'zone_self', 'Office')).toBe('Office')
|
|
61
|
+
expect(numberedRoomName(nodes, 'zone_c', 'Kitchen')).toBe('Kitchen')
|
|
62
|
+
// Only the level counts: the same name upstairs is not a repeat.
|
|
63
|
+
const upstairs = zone('zone_up', { name: '', parentId: 'level_upstairs' })
|
|
64
|
+
expect(numberedRoomName({ ...nodes, [upstairs.id]: upstairs }, 'zone_up', 'Bedroom')).toBe(
|
|
65
|
+
'Bedroom',
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('outdoor-looking rooms list Outdoor first', () => {
|
|
70
|
+
const cases = [
|
|
71
|
+
zone('zone_open_sky', { hasCeiling: false }),
|
|
72
|
+
zone('zone_terrace', { boundaryWallIds: [] }),
|
|
73
|
+
zone('zone_outside', { spaceRole: 'generic' }),
|
|
74
|
+
]
|
|
75
|
+
for (const node of cases) {
|
|
76
|
+
expect(roomNameContext(graph(node), node.id)).toBe('outdoor')
|
|
77
|
+
expect(ids(graph(node), node.id)[0]).toBe('outdoor')
|
|
78
|
+
expect(ids(graph(node), node.id).filter((id) => id === 'outdoor')).toHaveLength(1)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('a mezzanine gets the Mezzanine names first; other rooms never see them', () => {
|
|
83
|
+
const mezzanine = zone('zone_mezz', {
|
|
84
|
+
floor: { support: 'open', elevation: 1.4, thickness: 0.2 },
|
|
85
|
+
})
|
|
86
|
+
expect(roomNameContext(graph(mezzanine), mezzanine.id)).toBe('mezzanine')
|
|
87
|
+
expect(ids(graph(mezzanine), mezzanine.id)[0]).toBe('mezzanine')
|
|
88
|
+
expect(ids(graph(zone('zone_room')), 'zone_room')).not.toContain('mezzanine')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
/** A titled group of the room-name combobox. */
|
|
4
|
+
export type RoomNameSection = { id: string; title: string; names: readonly string[] }
|
|
5
|
+
|
|
6
|
+
/** What the room reads as, which decides the section shown first. */
|
|
7
|
+
export type RoomNameContext = 'room' | 'outdoor' | 'mezzanine'
|
|
8
|
+
|
|
9
|
+
/** Names for any room, in the order the dropdown lists their sections. */
|
|
10
|
+
export const ROOM_NAME_SECTIONS: readonly RoomNameSection[] = [
|
|
11
|
+
{
|
|
12
|
+
id: 'living',
|
|
13
|
+
title: 'Living',
|
|
14
|
+
names: [
|
|
15
|
+
'Living room',
|
|
16
|
+
'Family room',
|
|
17
|
+
'Dining room',
|
|
18
|
+
'Kitchen',
|
|
19
|
+
'Kitchenette',
|
|
20
|
+
'Pantry',
|
|
21
|
+
'Lounge',
|
|
22
|
+
'Den',
|
|
23
|
+
'Library',
|
|
24
|
+
'Playroom',
|
|
25
|
+
'Home cinema',
|
|
26
|
+
'Games room',
|
|
27
|
+
'Sunroom',
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'sleeping',
|
|
32
|
+
title: 'Sleeping',
|
|
33
|
+
names: [
|
|
34
|
+
'Bedroom',
|
|
35
|
+
'Primary bedroom',
|
|
36
|
+
'Guest room',
|
|
37
|
+
"Kids' room",
|
|
38
|
+
'Nursery',
|
|
39
|
+
'Dressing room',
|
|
40
|
+
'Walk-in closet',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'bathrooms',
|
|
45
|
+
title: 'Bathrooms',
|
|
46
|
+
names: ['Bathroom', 'En-suite', 'Shower room', 'Toilet', 'Powder room'],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'work',
|
|
50
|
+
title: 'Work & hobbies',
|
|
51
|
+
names: ['Office', 'Study', 'Studio', 'Workshop', 'Gym'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'circulation',
|
|
55
|
+
title: 'Circulation',
|
|
56
|
+
names: ['Entrance', 'Hallway', 'Corridor', 'Landing', 'Stairwell', 'Mudroom'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'utility',
|
|
60
|
+
title: 'Utility & storage',
|
|
61
|
+
names: [
|
|
62
|
+
'Laundry',
|
|
63
|
+
'Utility room',
|
|
64
|
+
'Storage',
|
|
65
|
+
'Closet',
|
|
66
|
+
'Boiler room',
|
|
67
|
+
'Cellar',
|
|
68
|
+
'Basement',
|
|
69
|
+
'Attic',
|
|
70
|
+
'Garage',
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'outdoor',
|
|
75
|
+
title: 'Outdoor',
|
|
76
|
+
names: [
|
|
77
|
+
'Garden',
|
|
78
|
+
'Patio',
|
|
79
|
+
'Terrace',
|
|
80
|
+
'Balcony',
|
|
81
|
+
'Porch',
|
|
82
|
+
'Veranda',
|
|
83
|
+
'Deck',
|
|
84
|
+
'Courtyard',
|
|
85
|
+
'Pool area',
|
|
86
|
+
'Carport',
|
|
87
|
+
'Garden shed',
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
/** Only offered, and first, when the selection is a mezzanine. */
|
|
93
|
+
export const MEZZANINE_NAME_SECTION: RoomNameSection = {
|
|
94
|
+
id: 'mezzanine',
|
|
95
|
+
title: 'Mezzanine',
|
|
96
|
+
names: ['Mezzanine', 'Loft', 'Reading nook', 'Gallery'],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A mezzanine is one; a room reads as outdoor with no ceiling, without a wall
|
|
101
|
+
* of its own (a terrace closed by separators) or when it is not a room inside
|
|
102
|
+
* walls at all. Everything else is an indoor room.
|
|
103
|
+
*/
|
|
104
|
+
export function roomNameContext(nodes: Record<string, AnyNode>, zoneId: string): RoomNameContext {
|
|
105
|
+
const zone = nodes[zoneId]
|
|
106
|
+
if (zone?.type !== 'zone') return 'room'
|
|
107
|
+
if (zone.floor?.support === 'open') return 'mezzanine'
|
|
108
|
+
if (zone.spaceRole !== 'room' || zone.hasCeiling === false || !zone.boundaryWallIds.length)
|
|
109
|
+
return 'outdoor'
|
|
110
|
+
return 'room'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A name without the number the picker adds to a repeat: "Bedroom 2" → "Bedroom". */
|
|
114
|
+
export function baseRoomName(name: string) {
|
|
115
|
+
return name.trim().replace(/\s+\d+$/, '')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The names the level's other rooms go by (a mezzanine's level is its host's). */
|
|
119
|
+
function otherRoomNames(nodes: Record<string, AnyNode>, zoneId: string) {
|
|
120
|
+
const levelId = nodes[zoneId]?.parentId
|
|
121
|
+
const names: string[] = []
|
|
122
|
+
for (const node of Object.values(nodes)) {
|
|
123
|
+
if (node.type !== 'zone' || node.id === zoneId || node.parentId !== levelId) continue
|
|
124
|
+
const name = node.name?.trim()
|
|
125
|
+
if (name) names.push(name)
|
|
126
|
+
}
|
|
127
|
+
return names
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** How many of the level's other rooms already go by `name` (numbered or not). */
|
|
131
|
+
export function roomNameUseCount(nodes: Record<string, AnyNode>, zoneId: string, name: string) {
|
|
132
|
+
const wanted = name.trim().toLowerCase()
|
|
133
|
+
return otherRoomNames(nodes, zoneId).filter(
|
|
134
|
+
(other) => baseRoomName(other).toLowerCase() === wanted,
|
|
135
|
+
).length
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The name a picked suggestion becomes: itself while no other room on the
|
|
140
|
+
* level has it, otherwise the first free "Name 2", "Name 3"…
|
|
141
|
+
*/
|
|
142
|
+
export function numberedRoomName(nodes: Record<string, AnyNode>, zoneId: string, name: string) {
|
|
143
|
+
const taken = new Set(otherRoomNames(nodes, zoneId).map((other) => other.toLowerCase()))
|
|
144
|
+
if (!taken.has(name.toLowerCase())) return name
|
|
145
|
+
let index = 2
|
|
146
|
+
while (taken.has(`${name} ${index}`.toLowerCase())) index++
|
|
147
|
+
return `${name} ${index}`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The combobox's sections for one room: the catalog, with the section its
|
|
152
|
+
* context calls for first (Mezzanine, only for a mezzanine; Outdoor for an
|
|
153
|
+
* outdoor room), then the rest in their usual order. Other rooms' names are
|
|
154
|
+
* not offered — a name already used on the level is numbered when picked.
|
|
155
|
+
*/
|
|
156
|
+
export function roomNameSections(
|
|
157
|
+
nodes: Record<string, AnyNode>,
|
|
158
|
+
zoneId: string,
|
|
159
|
+
): RoomNameSection[] {
|
|
160
|
+
const context = roomNameContext(nodes, zoneId)
|
|
161
|
+
const first =
|
|
162
|
+
context === 'mezzanine'
|
|
163
|
+
? [MEZZANINE_NAME_SECTION]
|
|
164
|
+
: context === 'outdoor'
|
|
165
|
+
? ROOM_NAME_SECTIONS.filter((section) => section.id === 'outdoor')
|
|
166
|
+
: []
|
|
167
|
+
return [...first, ...ROOM_NAME_SECTIONS.filter((section) => !first.includes(section))]
|
|
168
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
createRoomTopologyIndex,
|
|
5
|
+
LevelNode,
|
|
6
|
+
type Point,
|
|
7
|
+
WallNode,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { formatLinearMeasurement } from './measurements'
|
|
10
|
+
import {
|
|
11
|
+
DIMENSION_FLOOR_LIFT,
|
|
12
|
+
mezzanineEdgeDimensions,
|
|
13
|
+
type RoomDimension,
|
|
14
|
+
wallPushDimensionRooms,
|
|
15
|
+
wallPushDimensions,
|
|
16
|
+
} from './room-push-dimensions'
|
|
17
|
+
|
|
18
|
+
// Rooms are real: the walls go through core's room topology, so the faces the
|
|
19
|
+
// dimensions read are the ones the push arrows and the room outline use. Walls
|
|
20
|
+
// are 0.2 m thick and centred, so a clear width is the centreline span − 0.2.
|
|
21
|
+
|
|
22
|
+
const LEVEL = 'level_push_dimensions'
|
|
23
|
+
const THICKNESS = 0.2
|
|
24
|
+
|
|
25
|
+
type Scene = { nodes: Record<string, AnyNode>; walls: WallNode[] }
|
|
26
|
+
|
|
27
|
+
function scene(segments: Array<[Point, Point]>, extra: Partial<WallNode> = {}): Scene {
|
|
28
|
+
const walls = segments.map(([start, end], i) =>
|
|
29
|
+
WallNode.parse({
|
|
30
|
+
id: `wall_d${i}`,
|
|
31
|
+
parentId: LEVEL,
|
|
32
|
+
start,
|
|
33
|
+
end,
|
|
34
|
+
thickness: THICKNESS,
|
|
35
|
+
...extra,
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
const level = LevelNode.parse({ id: LEVEL, children: walls.map((wall) => wall.id) })
|
|
39
|
+
return {
|
|
40
|
+
walls,
|
|
41
|
+
nodes: Object.fromEntries([level, ...walls].map((node) => [node.id, node as AnyNode])),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ring(points: Point[]): Array<[Point, Point]> {
|
|
46
|
+
return points.map((p, i) => [p, points[(i + 1) % points.length]!])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function measure(
|
|
50
|
+
{ nodes }: Scene,
|
|
51
|
+
wall: WallNode,
|
|
52
|
+
outward: Point,
|
|
53
|
+
distance: number,
|
|
54
|
+
span: { t0: number; t1: number } = { t0: 0, t1: 1 },
|
|
55
|
+
elevationOf: (roomId: string) => number = () => 0,
|
|
56
|
+
): RoomDimension[] {
|
|
57
|
+
const index = createRoomTopologyIndex()
|
|
58
|
+
index.rebuildLevel(LEVEL, nodes)
|
|
59
|
+
const rooms = index.getLevelTopology(LEVEL)?.rooms ?? []
|
|
60
|
+
return wallPushDimensions({
|
|
61
|
+
rooms: wallPushDimensionRooms(rooms, { wallId: wall.id, ...span }, elevationOf),
|
|
62
|
+
wall,
|
|
63
|
+
outward,
|
|
64
|
+
distance,
|
|
65
|
+
resolve: (id) => nodes[id],
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const distances = (dimensions: RoomDimension[]) =>
|
|
70
|
+
dimensions.map((dimension) => Number(dimension.distance.toFixed(4)))
|
|
71
|
+
|
|
72
|
+
const wallAt = ({ walls }: Scene, start: Point, end: Point) =>
|
|
73
|
+
walls.find(
|
|
74
|
+
(wall) =>
|
|
75
|
+
Math.hypot(wall.start[0] - start[0], wall.start[1] - start[1]) < 1e-9 &&
|
|
76
|
+
Math.hypot(wall.end[0] - end[0], wall.end[1] - end[1]) < 1e-9,
|
|
77
|
+
)!
|
|
78
|
+
|
|
79
|
+
describe('a rectangular room', () => {
|
|
80
|
+
const room = scene(
|
|
81
|
+
ring([
|
|
82
|
+
[0, 0],
|
|
83
|
+
[6, 0],
|
|
84
|
+
[6, 4],
|
|
85
|
+
[0, 4],
|
|
86
|
+
]),
|
|
87
|
+
)
|
|
88
|
+
const east = wallAt(room, [6, 0], [6, 4])
|
|
89
|
+
|
|
90
|
+
test('at rest: the clear width, east face to west face, across the room', () => {
|
|
91
|
+
const [dimension, ...rest] = measure(room, east, [1, 0], 0)
|
|
92
|
+
expect(rest).toEqual([])
|
|
93
|
+
expect(dimension!.distance).toBeCloseTo(6 - THICKNESS)
|
|
94
|
+
// Face to face along the push axis, at the middle of the shared stretch.
|
|
95
|
+
expect(dimension!.from[0]).toBeCloseTo(6 - THICKNESS / 2)
|
|
96
|
+
expect(dimension!.to[0]).toBeCloseTo(THICKNESS / 2)
|
|
97
|
+
expect(dimension!.from[1]).toBeCloseTo(dimension!.to[1])
|
|
98
|
+
expect(dimension!.from[1]).toBeGreaterThan(0)
|
|
99
|
+
expect(dimension!.from[1]).toBeLessThan(4)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('follows the push both ways; only the room side is measured', () => {
|
|
103
|
+
expect(distances(measure(room, east, [1, 0], 1.5))).toEqual([7.3])
|
|
104
|
+
expect(distances(measure(room, east, [1, 0], -2))).toEqual([3.8])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('the room on a raised floor measures just above that floor', () => {
|
|
108
|
+
const [dimension] = measure(room, east, [1, 0], 0, undefined, () => 0.45)
|
|
109
|
+
expect(dimension!.elevation).toBeCloseTo(0.45 + DIMENSION_FLOOR_LIFT)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('a far wall split in two collinear pieces is one dimension', () => {
|
|
113
|
+
const split = scene([
|
|
114
|
+
[
|
|
115
|
+
[0, 0],
|
|
116
|
+
[6, 0],
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
[6, 0],
|
|
120
|
+
[6, 4],
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
[6, 4],
|
|
124
|
+
[0, 4],
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
[0, 4],
|
|
128
|
+
[0, 2],
|
|
129
|
+
],
|
|
130
|
+
[
|
|
131
|
+
[0, 2],
|
|
132
|
+
[0, 0],
|
|
133
|
+
],
|
|
134
|
+
])
|
|
135
|
+
expect(distances(measure(split, wallAt(split, [6, 0], [6, 4]), [1, 0], 0))).toEqual([5.8])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('a curved moving wall gets no dimension', () => {
|
|
139
|
+
const curved = scene(
|
|
140
|
+
ring([
|
|
141
|
+
[0, 0],
|
|
142
|
+
[6, 0],
|
|
143
|
+
[6, 4],
|
|
144
|
+
[0, 4],
|
|
145
|
+
]),
|
|
146
|
+
)
|
|
147
|
+
const wall = wallAt(curved, [6, 0], [6, 4])
|
|
148
|
+
expect(measure(curved, { ...wall, curveOffset: 0.8 }, [1, 0], 0)).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('an L-shaped room', () => {
|
|
153
|
+
// The notch is the top right: the south wall faces the notch wall (y = 2)
|
|
154
|
+
// and the north wall (y = 4), two distinct widths, nearest first.
|
|
155
|
+
const room = scene(
|
|
156
|
+
ring([
|
|
157
|
+
[0, 0],
|
|
158
|
+
[6, 0],
|
|
159
|
+
[6, 2],
|
|
160
|
+
[3, 2],
|
|
161
|
+
[3, 4],
|
|
162
|
+
[0, 4],
|
|
163
|
+
]),
|
|
164
|
+
)
|
|
165
|
+
const south = wallAt(room, [0, 0], [6, 0])
|
|
166
|
+
|
|
167
|
+
test('every facing parallel wall, nearest first', () => {
|
|
168
|
+
const dimensions = measure(room, south, [0, -1], 0)
|
|
169
|
+
expect(distances(dimensions)).toEqual([1.8, 3.8])
|
|
170
|
+
// Each is measured where the two faces overlap: the notch over x 3..6, the north wall over 0..3.
|
|
171
|
+
expect(dimensions[0]!.from[0]).toBeGreaterThan(3)
|
|
172
|
+
expect(dimensions[1]!.from[0]).toBeLessThan(3)
|
|
173
|
+
expect(new Set(dimensions.map((dimension) => dimension.key)).size).toBe(2)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('a wall that no longer faces the moving one drops out', () => {
|
|
177
|
+
// Pulled north past the notch wall (y = 2), only the north wall is ahead.
|
|
178
|
+
expect(distances(measure(room, south, [0, -1], -2.5))).toEqual([1.3])
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('two rooms sharing the moving wall', () => {
|
|
183
|
+
const rooms = scene([
|
|
184
|
+
...ring([
|
|
185
|
+
[0, 0],
|
|
186
|
+
[8, 0],
|
|
187
|
+
[8, 4],
|
|
188
|
+
[0, 4],
|
|
189
|
+
]),
|
|
190
|
+
[
|
|
191
|
+
[4, 0],
|
|
192
|
+
[4, 4],
|
|
193
|
+
],
|
|
194
|
+
])
|
|
195
|
+
const middle = wallAt(rooms, [4, 0], [4, 4])
|
|
196
|
+
|
|
197
|
+
test('one width per side, each from its own face of the wall', () => {
|
|
198
|
+
const dimensions = measure(rooms, middle, [1, 0], 1)
|
|
199
|
+
expect(dimensions).toHaveLength(2)
|
|
200
|
+
const west = dimensions.find((dimension) => dimension.to[0] < 4)!
|
|
201
|
+
const east = dimensions.find((dimension) => dimension.to[0] > 4)!
|
|
202
|
+
expect(west.distance).toBeCloseTo(5 - THICKNESS)
|
|
203
|
+
expect(east.distance).toBeCloseTo(3 - THICKNESS)
|
|
204
|
+
expect(west.from[0]).toBeCloseTo(5 - THICKNESS / 2)
|
|
205
|
+
expect(east.from[0]).toBeCloseTo(5 + THICKNESS / 2)
|
|
206
|
+
expect(west.key).not.toBe(east.key)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('a push over part of the wall measures only the rooms along that part', () => {
|
|
210
|
+
const dimensions = measure(rooms, middle, [1, 0], 0, { t0: 0, t1: 0.5 })
|
|
211
|
+
expect(dimensions).toHaveLength(2)
|
|
212
|
+
for (const dimension of dimensions) {
|
|
213
|
+
expect(dimension.from[1]).toBeGreaterThan(0)
|
|
214
|
+
expect(dimension.from[1]).toBeLessThan(2)
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('a rotated room', () => {
|
|
220
|
+
const angle = Math.PI / 6
|
|
221
|
+
const rotate = ([x, z]: Point): Point => [
|
|
222
|
+
x * Math.cos(angle) - z * Math.sin(angle),
|
|
223
|
+
x * Math.sin(angle) + z * Math.cos(angle),
|
|
224
|
+
]
|
|
225
|
+
const corners = (
|
|
226
|
+
[
|
|
227
|
+
[0, 0],
|
|
228
|
+
[6, 0],
|
|
229
|
+
[6, 4],
|
|
230
|
+
[0, 4],
|
|
231
|
+
] as Point[]
|
|
232
|
+
).map(rotate)
|
|
233
|
+
const room = scene(ring(corners))
|
|
234
|
+
const east = wallAt(room, corners[1]!, corners[2]!)
|
|
235
|
+
const outward = rotate([1, 0])
|
|
236
|
+
|
|
237
|
+
test('measures along the rotated axis, the same widths', () => {
|
|
238
|
+
const [dimension] = measure(room, east, outward, 0.5)
|
|
239
|
+
expect(dimension!.distance).toBeCloseTo(6.5 - THICKNESS)
|
|
240
|
+
const { from, to, distance } = dimension!
|
|
241
|
+
expect((to[0] - from[0]) / distance).toBeCloseTo(-outward[0])
|
|
242
|
+
expect((to[1] - from[1]) / distance).toBeCloseTo(-outward[1])
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('a wall about 2° off parallel is not measured', () => {
|
|
246
|
+
const skewed = scene(
|
|
247
|
+
ring([
|
|
248
|
+
[0, 0],
|
|
249
|
+
[6, 0],
|
|
250
|
+
[6, 4],
|
|
251
|
+
[0.15, 4],
|
|
252
|
+
]),
|
|
253
|
+
)
|
|
254
|
+
expect(measure(skewed, wallAt(skewed, [6, 0], [6, 4]), [1, 0], 0)).toEqual([])
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('a mezzanine edge', () => {
|
|
259
|
+
const rest: Point[] = [
|
|
260
|
+
[0, 0],
|
|
261
|
+
[5, 0],
|
|
262
|
+
[5, 3],
|
|
263
|
+
[2, 3],
|
|
264
|
+
[2, 2],
|
|
265
|
+
[0, 2],
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
test('measures to each facing parallel edge of the outline it would leave', () => {
|
|
269
|
+
// Edge 0 (south, y = 0) pushed 0.5 m south: the outline follows.
|
|
270
|
+
const outline = rest.map(([x, z], i): Point => (i < 2 ? [x, z - 0.5] : [x, z]))
|
|
271
|
+
const dimensions = mezzanineEdgeDimensions({
|
|
272
|
+
rest,
|
|
273
|
+
outline,
|
|
274
|
+
edgeIndex: 0,
|
|
275
|
+
outward: [0, -1],
|
|
276
|
+
distance: 0.5,
|
|
277
|
+
elevation: 1.2,
|
|
278
|
+
})
|
|
279
|
+
expect(distances(dimensions)).toEqual([2.5, 3.5])
|
|
280
|
+
expect(dimensions[0]!.elevation).toBeCloseTo(1.2 + DIMENSION_FLOOR_LIFT)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('a refused push still measures, from the edge where the arrow is', () => {
|
|
284
|
+
const dimensions = mezzanineEdgeDimensions({
|
|
285
|
+
rest,
|
|
286
|
+
outline: rest,
|
|
287
|
+
edgeIndex: 0,
|
|
288
|
+
outward: [0, -1],
|
|
289
|
+
distance: -1,
|
|
290
|
+
elevation: 1.2,
|
|
291
|
+
})
|
|
292
|
+
expect(distances(dimensions)).toEqual([1, 2])
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
describe('units', () => {
|
|
297
|
+
test('a dimension reads in the viewer’s units', () => {
|
|
298
|
+
const room = scene(
|
|
299
|
+
ring([
|
|
300
|
+
[0, 0],
|
|
301
|
+
[6, 0],
|
|
302
|
+
[6, 4],
|
|
303
|
+
[0, 4],
|
|
304
|
+
]),
|
|
305
|
+
)
|
|
306
|
+
const [dimension] = measure(room, wallAt(room, [6, 0], [6, 4]), [1, 0], 0)
|
|
307
|
+
expect(formatLinearMeasurement(dimension!.distance, 'metric')).toBe('5.8m')
|
|
308
|
+
expect(formatLinearMeasurement(dimension!.distance, 'metric', 'millimeters')).toBe('5800mm')
|
|
309
|
+
expect(formatLinearMeasurement(dimension!.distance, 'imperial')).toBe(`19'0"`)
|
|
310
|
+
})
|
|
311
|
+
})
|