@pascal-app/editor 1.0.6 → 1.1.0
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 +18 -15
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +110 -42
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +77 -68
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +31 -11
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +458 -140
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +23 -1
- package/src/components/editor/index.tsx +150 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +2 -3
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +4 -3
- package/src/components/editor/room-handles-2d.tsx +158 -7
- package/src/components/editor/room-handles.tsx +244 -54
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +33 -17
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +15 -7
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +47 -19
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
- package/src/components/ui/helpers/helper-manager.tsx +136 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
- package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +11 -10
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +9 -5
- package/src/components/ui/panels/panel-wrapper.tsx +63 -127
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +465 -66
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +22 -13
- package/src/components/ui/panels/room-panel.tsx +207 -100
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +62 -5
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15849 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +39331 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +39331 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +39331 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +39331 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +224 -45
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +101 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -2
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +15 -6
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +24 -6
- package/src/lib/measurements.ts +1 -203
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +28 -17
- package/src/lib/room-built-on.ts +21 -17
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +141 -1
- package/src/lib/room-handle-drag.ts +554 -58
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +38 -1
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +11 -5
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +21 -6
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +17 -6
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +44 -0
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +53 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-snap-indicator.ts +15 -2
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -100
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
detectSpacesForLevel,
|
|
6
|
+
LevelNode as LevelSchema,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
registerNode,
|
|
9
|
+
useScene,
|
|
10
|
+
type WallNode,
|
|
11
|
+
WallNode as WallSchema,
|
|
12
|
+
wallClosesRoom,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
15
|
+
import { createWallOnCurrentLevel } from '../components/tools/wall/wall-drafting'
|
|
16
|
+
import type { WallPlanPoint } from '../components/tools/wall/wall-snap-geometry'
|
|
17
|
+
import useEditor from '../store/use-editor'
|
|
18
|
+
import {
|
|
19
|
+
addWallPolygonDraftCorner,
|
|
20
|
+
discardWallPolygonDraft,
|
|
21
|
+
startWallPolygonDraft,
|
|
22
|
+
wallPolygonDraftWalls,
|
|
23
|
+
} from './wall-polygon-draft'
|
|
24
|
+
import { isWallRunFree, snapWallRunDraftPoint, wallRunDirection } from './wall-run-snap'
|
|
25
|
+
|
|
26
|
+
// `updateNodes` batches its dirty-marking through requestAnimationFrame,
|
|
27
|
+
// which bun's test runtime doesn't provide.
|
|
28
|
+
if (typeof globalThis.requestAnimationFrame === 'undefined') {
|
|
29
|
+
globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) =>
|
|
30
|
+
setTimeout(() => callback(0), 0)) as unknown as typeof requestAnimationFrame
|
|
31
|
+
globalThis.cancelAnimationFrame = ((id: number) =>
|
|
32
|
+
clearTimeout(id)) as typeof cancelAnimationFrame
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const LEVEL_ID = 'level_run' as AnyNodeId
|
|
36
|
+
const deg = (degrees: number) => (degrees * Math.PI) / 180
|
|
37
|
+
|
|
38
|
+
function levelWalls(): WallNode[] {
|
|
39
|
+
return Object.values(useScene.getState().nodes).filter(
|
|
40
|
+
(node): node is WallNode => node?.type === 'wall' && node.parentId === LEVEL_ID,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A plain run as the wall tool drafts it once its first point is placed: each
|
|
46
|
+
* wall from the last one's end, square to it, snapped against the level's walls.
|
|
47
|
+
*/
|
|
48
|
+
function plainRun(runStart: WallPlanPoint) {
|
|
49
|
+
let start = runStart
|
|
50
|
+
let reference: WallPlanPoint | null = null
|
|
51
|
+
return (cursor: WallPlanPoint, free = false) => {
|
|
52
|
+
const snapped = snapWallRunDraftPoint({
|
|
53
|
+
point: cursor,
|
|
54
|
+
start,
|
|
55
|
+
runStart,
|
|
56
|
+
reference,
|
|
57
|
+
walls: levelWalls(),
|
|
58
|
+
free,
|
|
59
|
+
})
|
|
60
|
+
const wall = createWallOnCurrentLevel(start, snapped.point)!
|
|
61
|
+
start = wall.end
|
|
62
|
+
reference = wallRunDirection(wall.start, wall.end)
|
|
63
|
+
return { wall, snapped }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('a plain wall run snaps like a pulled one once started', () => {
|
|
68
|
+
let restoreRegistry: (() => void) | undefined
|
|
69
|
+
let previousEditor: Partial<ReturnType<typeof useEditor.getState>>
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
previousEditor = {
|
|
73
|
+
toolDefaults: useEditor.getState().toolDefaults,
|
|
74
|
+
continuationByContext: useEditor.getState().continuationByContext,
|
|
75
|
+
}
|
|
76
|
+
// The wall's snap profile makes the armed wall tool the 'wall' snapping
|
|
77
|
+
// context (grid lengths), as in the app.
|
|
78
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
79
|
+
registerNode({
|
|
80
|
+
kind: 'wall',
|
|
81
|
+
schemaVersion: 10,
|
|
82
|
+
schema: WallSchema,
|
|
83
|
+
category: 'structure',
|
|
84
|
+
snapProfile: 'structural',
|
|
85
|
+
defaults: () => WallSchema.parse({ start: [0, 0], end: [1, 0] }),
|
|
86
|
+
capabilities: {},
|
|
87
|
+
} as never)
|
|
88
|
+
useEditor.getState().setSnappingMode('wall', 'grid')
|
|
89
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
90
|
+
useViewer.setState({
|
|
91
|
+
selection: { buildingId: null, levelId: LEVEL_ID, zoneId: null, selectedIds: [] },
|
|
92
|
+
} as never)
|
|
93
|
+
const level = LevelSchema.parse({ id: LEVEL_ID, level: 0, height: 2.7, children: [] })
|
|
94
|
+
useScene.setState({
|
|
95
|
+
nodes: { [level.id]: level as AnyNode },
|
|
96
|
+
rootNodeIds: [level.id],
|
|
97
|
+
dirtyNodes: new Set(),
|
|
98
|
+
collections: {},
|
|
99
|
+
} as never)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
discardWallPolygonDraft()
|
|
104
|
+
restoreRegistry?.()
|
|
105
|
+
restoreRegistry = undefined
|
|
106
|
+
useEditor.getState().setSnappingMode('wall', 'grid')
|
|
107
|
+
useEditor.setState(previousEditor as never)
|
|
108
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("the first wall holds to the house's axes, steps 15° past them, and Alt frees it", () => {
|
|
112
|
+
const snap = (cursor: WallPlanPoint, free = false) =>
|
|
113
|
+
snapWallRunDraftPoint({
|
|
114
|
+
point: cursor,
|
|
115
|
+
start: [0, 0],
|
|
116
|
+
runStart: [0, 0],
|
|
117
|
+
reference: null,
|
|
118
|
+
walls: [],
|
|
119
|
+
free,
|
|
120
|
+
})
|
|
121
|
+
const axis = snap([4.1, 0.35])
|
|
122
|
+
expect(axis.lock).toBe('axis')
|
|
123
|
+
expect(axis.point).toEqual([4, 0])
|
|
124
|
+
const stepped = snap([Math.cos(deg(40)) * 3, Math.sin(deg(40)) * 3])
|
|
125
|
+
expect(stepped.lock).toBe('angle')
|
|
126
|
+
expect(stepped.point[0]).toBeCloseTo(Math.cos(deg(45)) * 3, 6)
|
|
127
|
+
expect(stepped.point[1]).toBeCloseTo(Math.sin(deg(45)) * 3, 6)
|
|
128
|
+
expect(isWallRunFree({ altKey: true })).toBe(true)
|
|
129
|
+
expect(isWallRunFree()).toBe(false)
|
|
130
|
+
expect(snap([3.13, 1.07], isWallRunFree({ altKey: true })).point).toEqual([3.13, 1.07])
|
|
131
|
+
// Snapping off frees it too.
|
|
132
|
+
useEditor.getState().setSnappingMode('wall', 'off')
|
|
133
|
+
expect(isWallRunFree()).toBe(true)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('a Walls run squares up, lines up with its start and closes into a room', () => {
|
|
137
|
+
// The first point keeps the tool's own snapping; the run starts at (0, 0).
|
|
138
|
+
const place = plainRun([0, 0])
|
|
139
|
+
const first = place([5.1, 0.4])
|
|
140
|
+
expect(first.snapped.lock).toBe('axis')
|
|
141
|
+
expect(first.wall.end).toEqual([5, 0])
|
|
142
|
+
const second = place([5.3, 3.9])
|
|
143
|
+
expect(second.snapped.lock).toBe('square')
|
|
144
|
+
expect(second.wall.end).toEqual([5, 4])
|
|
145
|
+
const third = place([0.1, 4.05])
|
|
146
|
+
expect(third.snapped.snap).toBe('alignment')
|
|
147
|
+
expect(third.snapped.label).toBe('In line with start')
|
|
148
|
+
expect(third.snapped.guides.some((guide) => guide.kind === 'run-start')).toBe(true)
|
|
149
|
+
expect(third.snapped.guides.some((guide) => guide.kind === 'extension')).toBe(true)
|
|
150
|
+
expect(third.wall.end).toEqual([0, 4])
|
|
151
|
+
const last = place([0.12, 0.18])
|
|
152
|
+
expect(last.snapped.snap).toBe('endpoint')
|
|
153
|
+
expect(last.wall.end).toEqual([0, 0])
|
|
154
|
+
expect(wallClosesRoom(levelWalls(), last.wall)).toBe(true)
|
|
155
|
+
const rooms = detectSpacesForLevel(LEVEL_ID, levelWalls()).spaces.filter(
|
|
156
|
+
(space) => !space.isExterior,
|
|
157
|
+
)
|
|
158
|
+
expect(rooms).toHaveLength(1)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('a Polygon room snaps its corners the same way and closes on its first', () => {
|
|
162
|
+
startWallPolygonDraft(LEVEL_ID, [0, 0])
|
|
163
|
+
let start: WallPlanPoint = [0, 0]
|
|
164
|
+
let reference: WallPlanPoint | null = null
|
|
165
|
+
const corner = (cursor: WallPlanPoint) => {
|
|
166
|
+
const snapped = snapWallRunDraftPoint({
|
|
167
|
+
point: cursor,
|
|
168
|
+
start,
|
|
169
|
+
runStart: [0, 0],
|
|
170
|
+
reference,
|
|
171
|
+
walls: [...levelWalls(), ...wallPolygonDraftWalls()],
|
|
172
|
+
free: false,
|
|
173
|
+
})
|
|
174
|
+
const outcome = addWallPolygonDraftCorner(snapped.point)
|
|
175
|
+
reference = wallRunDirection(start, snapped.point)
|
|
176
|
+
start = snapped.point
|
|
177
|
+
return { snapped, outcome }
|
|
178
|
+
}
|
|
179
|
+
expect(corner([3.9, -0.3]).snapped.point).toEqual([4, 0])
|
|
180
|
+
expect(corner([4.2, 2.95]).snapped.point).toEqual([4, 3])
|
|
181
|
+
const across = corner([-0.08, 3.1])
|
|
182
|
+
expect(across.snapped.label).toBe('In line with start')
|
|
183
|
+
expect(across.snapped.point).toEqual([0, 3])
|
|
184
|
+
const close = corner([0.1, 0.2])
|
|
185
|
+
expect(close.snapped.snap).toBe('endpoint')
|
|
186
|
+
expect(close.outcome).toBe('closed')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapping for the wall tool's line runs once their first point is placed —
|
|
3
|
+
* a Polygon room's sides and a run pulled from a wall end (`wall-pull.ts`,
|
|
4
|
+
* `wallDraftIsRun`); a Walls segment is one wall and snaps as its first point.
|
|
5
|
+
*
|
|
6
|
+
* Each wall holds square: straight on from the wall before it, square to it,
|
|
7
|
+
* or along the house's axes within about 7°, and on 15° steps past that, with
|
|
8
|
+
* its length on the grid; existing walls, the run's start and the projection
|
|
9
|
+
* guides win over that (`snapWallPullPoint`). Alt, or snapping off, leaves the
|
|
10
|
+
* end at the cursor. The run's first point keeps the tool's own snapping.
|
|
11
|
+
*
|
|
12
|
+
* The 3D wall tool and the floor plan each draft a run from their own start,
|
|
13
|
+
* run start and previous wall, and snap through here, so both views place the
|
|
14
|
+
* same point; the view that owns the input publishes the guides and marker.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { WallNode, WallPlanPoint } from '@pascal-app/core'
|
|
18
|
+
import { getSegmentGridStep } from '../components/tools/wall/wall-drafting'
|
|
19
|
+
import { snapWallPullPoint, type WallPullSnapResult } from '../components/tools/wall/wall-pull-snap'
|
|
20
|
+
import useAlignmentGuides from '../store/use-alignment-guides'
|
|
21
|
+
import { getActiveSnappingMode } from '../store/use-editor'
|
|
22
|
+
import useWallSnapIndicator from '../store/use-wall-snap-indicator'
|
|
23
|
+
|
|
24
|
+
/** True while a run's walls follow the cursor freely: Alt, or snapping off. */
|
|
25
|
+
export function isWallRunFree(event?: { altKey?: boolean } | null): boolean {
|
|
26
|
+
return Boolean(event?.altKey) || getActiveSnappingMode() === 'off'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Unit direction of a placed wall `start` → `end`: the next wall holds square to it. */
|
|
30
|
+
export function wallRunDirection(start: WallPlanPoint, end: WallPlanPoint): WallPlanPoint | null {
|
|
31
|
+
const dx = end[0] - start[0]
|
|
32
|
+
const dz = end[1] - start[1]
|
|
33
|
+
const length = Math.hypot(dx, dz)
|
|
34
|
+
return length > 1e-9 ? [dx / length, dz / length] : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Snap the drafted end of a run's current wall, which starts at `start`.
|
|
39
|
+
* `runStart` is where the run began, `reference` the direction of the wall
|
|
40
|
+
* before (null for the run's first wall), and `walls` the level's walls with
|
|
41
|
+
* any open Polygon room's sides.
|
|
42
|
+
*/
|
|
43
|
+
export function snapWallRunDraftPoint(args: {
|
|
44
|
+
point: WallPlanPoint
|
|
45
|
+
start: WallPlanPoint
|
|
46
|
+
runStart: WallPlanPoint
|
|
47
|
+
reference: WallPlanPoint | null
|
|
48
|
+
walls: readonly WallNode[]
|
|
49
|
+
free: boolean
|
|
50
|
+
}): WallPullSnapResult {
|
|
51
|
+
return snapWallPullPoint({ ...args, gridStep: getSegmentGridStep() })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Show a run's snap: its projection guides and crossings (the alignment guide
|
|
56
|
+
* layers draw them) and the snap marker with what it caught. Null clears both.
|
|
57
|
+
*/
|
|
58
|
+
export function publishWallRunFeedback(result: WallPullSnapResult | null): void {
|
|
59
|
+
if (!result) {
|
|
60
|
+
useAlignmentGuides.getState().clear()
|
|
61
|
+
useWallSnapIndicator.getState().clear()
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
useAlignmentGuides.getState().setDraft({
|
|
65
|
+
lines: result.guides.map((guide) => ({
|
|
66
|
+
kind: guide.kind,
|
|
67
|
+
from: [guide.from[0], guide.from[1]],
|
|
68
|
+
to: [guide.to[0], guide.to[1]],
|
|
69
|
+
})),
|
|
70
|
+
intersections: result.intersections.map((point) => [point[0], point[1]]),
|
|
71
|
+
})
|
|
72
|
+
useWallSnapIndicator.getState().set(
|
|
73
|
+
result.snap
|
|
74
|
+
? {
|
|
75
|
+
x: result.point[0],
|
|
76
|
+
z: result.point[1],
|
|
77
|
+
kind: result.snap,
|
|
78
|
+
wallIds: result.targetWallIds.length > 0 ? result.targetWallIds : undefined,
|
|
79
|
+
label: result.label ?? undefined,
|
|
80
|
+
}
|
|
81
|
+
: null,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
GYPSUM_HALF,
|
|
4
|
+
getSceneDocument,
|
|
5
|
+
projectWallTypes,
|
|
6
|
+
SiteNode,
|
|
7
|
+
STUD_2X8,
|
|
8
|
+
useScene,
|
|
9
|
+
WallTypeDefinition,
|
|
10
|
+
wallTypeAssembly,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import {
|
|
13
|
+
deleteProjectWallType,
|
|
14
|
+
newWallTypeBlockedReason,
|
|
15
|
+
PROJECT_WALL_TYPE_LIMIT,
|
|
16
|
+
saveProjectWallType,
|
|
17
|
+
WALL_TYPE_LAYER_LIMIT,
|
|
18
|
+
WALL_TYPE_SAVE_FAILURE,
|
|
19
|
+
} from './wall-type'
|
|
20
|
+
|
|
21
|
+
// Scene writes schedule their dirty pass on a frame; bun has none.
|
|
22
|
+
globalThis.requestAnimationFrame ??= () => 0
|
|
23
|
+
globalThis.cancelAnimationFrame ??= () => {}
|
|
24
|
+
|
|
25
|
+
const currentProjectWallTypes = () => {
|
|
26
|
+
const { nodes, rootNodeIds } = useScene.getState()
|
|
27
|
+
return projectWallTypes(nodes, rootNodeIds)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A 2x8 at 24 in partition, drywall both faces. */
|
|
31
|
+
const SOUND_WALL: WallTypeDefinition = {
|
|
32
|
+
id: 'custom-soundwall',
|
|
33
|
+
label: '2x8 sound wall',
|
|
34
|
+
assembly: wallTypeAssembly('custom-soundwall', [
|
|
35
|
+
{ role: 'lining', material: 'drywall', thickness: GYPSUM_HALF },
|
|
36
|
+
{ role: 'structure', core: true, material: 'wood', thickness: STUD_2X8, spacing: 0.6096 },
|
|
37
|
+
{ role: 'lining', material: 'drywall', thickness: GYPSUM_HALF },
|
|
38
|
+
]),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
useScene.getState().unloadScene()
|
|
43
|
+
useScene.getState().loadScene()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
useScene.getState().unloadScene()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('project wall types', () => {
|
|
51
|
+
test('save with the project and come back from a saved scene', () => {
|
|
52
|
+
expect(saveProjectWallType(SOUND_WALL)).toBe('saved')
|
|
53
|
+
expect(currentProjectWallTypes()).toEqual([SOUND_WALL])
|
|
54
|
+
const document = JSON.parse(JSON.stringify(getSceneDocument()))
|
|
55
|
+
useScene.getState().unloadScene()
|
|
56
|
+
expect(currentProjectWallTypes()).toEqual([])
|
|
57
|
+
useScene.getState().setScene(document.nodes, document.rootNodeIds)
|
|
58
|
+
expect(currentProjectWallTypes()).toEqual([SOUND_WALL])
|
|
59
|
+
// Saving again under the same id edits it in place.
|
|
60
|
+
saveProjectWallType({ ...SOUND_WALL, label: 'Sound wall' })
|
|
61
|
+
expect(currentProjectWallTypes().map((type) => type.label)).toEqual(['Sound wall'])
|
|
62
|
+
deleteProjectWallType(SOUND_WALL.id)
|
|
63
|
+
expect(currentProjectWallTypes()).toEqual([])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('a 13th layer and a 65th type are refused, each saying why, and nothing is written', () => {
|
|
67
|
+
const stack = (linings: number) =>
|
|
68
|
+
wallTypeAssembly('custom-thick', [
|
|
69
|
+
{ role: 'structure', core: true, material: 'wood', thickness: STUD_2X8 },
|
|
70
|
+
...Array.from({ length: linings }, () => ({
|
|
71
|
+
role: 'lining' as const,
|
|
72
|
+
material: 'drywall',
|
|
73
|
+
thickness: GYPSUM_HALF,
|
|
74
|
+
})),
|
|
75
|
+
])
|
|
76
|
+
const thick = { id: 'custom-thick', label: 'Thick', assembly: stack(WALL_TYPE_LAYER_LIMIT) }
|
|
77
|
+
expect(saveProjectWallType(thick)).toBe('too-many-layers')
|
|
78
|
+
expect(currentProjectWallTypes()).toEqual([])
|
|
79
|
+
// The limits are the schema's own.
|
|
80
|
+
expect(WallTypeDefinition.safeParse(thick).success).toBe(false)
|
|
81
|
+
expect(saveProjectWallType({ ...thick, assembly: stack(WALL_TYPE_LAYER_LIMIT - 1) })).toBe(
|
|
82
|
+
'saved',
|
|
83
|
+
)
|
|
84
|
+
deleteProjectWallType(thick.id)
|
|
85
|
+
|
|
86
|
+
const numbered = (n: number) => ({
|
|
87
|
+
...SOUND_WALL,
|
|
88
|
+
id: `custom-sound-${n}`,
|
|
89
|
+
label: `Sound ${n}`,
|
|
90
|
+
})
|
|
91
|
+
for (let n = 1; n <= PROJECT_WALL_TYPE_LIMIT; n += 1)
|
|
92
|
+
expect(saveProjectWallType(numbered(n))).toBe('saved')
|
|
93
|
+
expect(newWallTypeBlockedReason(currentProjectWallTypes())).toContain(
|
|
94
|
+
`${PROJECT_WALL_TYPE_LIMIT} wall types`,
|
|
95
|
+
)
|
|
96
|
+
expect(saveProjectWallType(numbered(PROJECT_WALL_TYPE_LIMIT + 1))).toBe('too-many-types')
|
|
97
|
+
expect(currentProjectWallTypes()).toHaveLength(PROJECT_WALL_TYPE_LIMIT)
|
|
98
|
+
const site = Object.values(useScene.getState().nodes).find((node) => node.type === 'site')
|
|
99
|
+
expect(SiteNode.safeParse(site).success).toBe(true)
|
|
100
|
+
const over = [...currentProjectWallTypes(), numbered(PROJECT_WALL_TYPE_LIMIT + 1)]
|
|
101
|
+
expect(SiteNode.safeParse({ ...site, wallTypes: over }).success).toBe(false)
|
|
102
|
+
// A type the project keeps can still be edited in place.
|
|
103
|
+
expect(saveProjectWallType({ ...numbered(1), label: 'Quiet wall' })).toBe('saved')
|
|
104
|
+
expect(currentProjectWallTypes()[0]?.label).toBe('Quiet wall')
|
|
105
|
+
deleteProjectWallType(numbered(1).id)
|
|
106
|
+
expect(newWallTypeBlockedReason(currentProjectWallTypes())).toBeNull()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('a type that does not parse says so, apart from a scene with no site', () => {
|
|
110
|
+
const coreless = {
|
|
111
|
+
...SOUND_WALL,
|
|
112
|
+
assembly: {
|
|
113
|
+
...SOUND_WALL.assembly,
|
|
114
|
+
layers: SOUND_WALL.assembly.layers.filter((l) => !l.core),
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
expect(saveProjectWallType(coreless)).toBe('invalid')
|
|
118
|
+
expect(saveProjectWallType({ ...SOUND_WALL, label: ' ' })).toBe('invalid')
|
|
119
|
+
expect(currentProjectWallTypes()).toEqual([])
|
|
120
|
+
useScene.getState().unloadScene()
|
|
121
|
+
expect(saveProjectWallType(SOUND_WALL)).toBe('no-site')
|
|
122
|
+
expect(WALL_TYPE_SAVE_FAILURE.invalid).not.toBe(WALL_TYPE_SAVE_FAILURE['no-site'])
|
|
123
|
+
expect(WALL_TYPE_SAVE_FAILURE.invalid).not.toContain('site')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
type Assembly,
|
|
4
|
+
isExteriorWallAssembly,
|
|
5
|
+
parseWallTypes,
|
|
6
|
+
projectWallTypes,
|
|
7
|
+
type SiteNode,
|
|
8
|
+
useScene,
|
|
9
|
+
WallTypeDefinition,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { useMemo } from 'react'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A project's own wall types (`SiteNode.wallTypes`), made and edited from a wall's
|
|
15
|
+
* Assembly controls (`WallTypeMenu`): saved on the site, so they travel with the
|
|
16
|
+
* project and undo like any edit.
|
|
17
|
+
*
|
|
18
|
+
* A placement is the kind of wall a stack builds, read from its layers: `exterior`
|
|
19
|
+
* with a cladding side, `interior` lined alike on both faces. A new type starts from
|
|
20
|
+
* a typical stack of its placement.
|
|
21
|
+
*/
|
|
22
|
+
export type WallTypePlacement = 'exterior' | 'interior'
|
|
23
|
+
|
|
24
|
+
function siteOf(state: ReturnType<typeof useScene.getState>): SiteNode | undefined {
|
|
25
|
+
for (const id of state.rootNodeIds) {
|
|
26
|
+
const node = state.nodes[id]
|
|
27
|
+
if (node?.type === 'site') return node
|
|
28
|
+
}
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The project's own wall types, kept current as they are saved, edited or undone. */
|
|
33
|
+
export function useProjectWallTypes(): WallTypeDefinition[] {
|
|
34
|
+
const saved = useScene((state) => siteOf(state)?.wallTypes)
|
|
35
|
+
return useMemo(() => parseWallTypes(saved), [saved])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The placement a stack builds, read from its layers (`isExteriorWallAssembly`). */
|
|
39
|
+
export function wallTypePlacementOf(assembly: Assembly): WallTypePlacement {
|
|
40
|
+
return isExteriorWallAssembly(assembly) ? 'exterior' : 'interior'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The most layers a wall type's stack holds (`Assembly.layers`). */
|
|
44
|
+
export const WALL_TYPE_LAYER_LIMIT = 12
|
|
45
|
+
|
|
46
|
+
/** The most wall types a project keeps on its site (`SiteNode.wallTypes`). */
|
|
47
|
+
export const PROJECT_WALL_TYPE_LIMIT = 64
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Why no new wall type can be made in this project — it keeps `PROJECT_WALL_TYPE_LIMIT`
|
|
51
|
+
* already — or null while it has room: what "New wall type…" and "Duplicate as a new
|
|
52
|
+
* type…" say when they are off.
|
|
53
|
+
*/
|
|
54
|
+
export function newWallTypeBlockedReason(
|
|
55
|
+
projectTypes: readonly WallTypeDefinition[],
|
|
56
|
+
): string | null {
|
|
57
|
+
return projectTypes.length >= PROJECT_WALL_TYPE_LIMIT
|
|
58
|
+
? `This project keeps ${PROJECT_WALL_TYPE_LIMIT} wall types, the most it can. Delete one to make another.`
|
|
59
|
+
: null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What saving a project wall type came to: `saved`, or why not — the scene has no
|
|
64
|
+
* site to keep it on, its stack has more layers than a wall type holds, the project
|
|
65
|
+
* keeps as many types as it can and this one is new, or it does not parse.
|
|
66
|
+
*/
|
|
67
|
+
export type WallTypeSaveResult =
|
|
68
|
+
| 'saved'
|
|
69
|
+
| 'no-site'
|
|
70
|
+
| 'too-many-layers'
|
|
71
|
+
| 'too-many-types'
|
|
72
|
+
| 'invalid'
|
|
73
|
+
|
|
74
|
+
/** What the wall type editor says when a type was not saved. */
|
|
75
|
+
export const WALL_TYPE_SAVE_FAILURE: Record<Exclude<WallTypeSaveResult, 'saved'>, string> = {
|
|
76
|
+
'no-site': 'This project has no site to keep wall types on, so the type was not saved.',
|
|
77
|
+
'too-many-layers': `A wall type holds at most ${WALL_TYPE_LAYER_LIMIT} layers. Remove one to save it.`,
|
|
78
|
+
'too-many-types': `This project keeps ${PROJECT_WALL_TYPE_LIMIT} wall types, the most it can. Delete one to save another.`,
|
|
79
|
+
invalid:
|
|
80
|
+
'These layers do not make a wall type (one framing core, every layer a thickness), so it was not saved.',
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Saves a project wall type on the site, in place of the one with its id or after
|
|
85
|
+
* the others: one undo step, saved and shared with the project. Nothing is written
|
|
86
|
+
* unless it comes to `saved` (`WallTypeSaveResult`): a stack of more than
|
|
87
|
+
* `WALL_TYPE_LAYER_LIMIT` layers, or a new type past `PROJECT_WALL_TYPE_LIMIT`, would
|
|
88
|
+
* leave a site that no longer parses.
|
|
89
|
+
*/
|
|
90
|
+
export function saveProjectWallType(type: WallTypeDefinition): WallTypeSaveResult {
|
|
91
|
+
const state = useScene.getState()
|
|
92
|
+
const site = siteOf(state)
|
|
93
|
+
if (!site) return 'no-site'
|
|
94
|
+
if ((type.assembly?.layers?.length ?? 0) > WALL_TYPE_LAYER_LIMIT) return 'too-many-layers'
|
|
95
|
+
const parsed = WallTypeDefinition.safeParse(type)
|
|
96
|
+
if (!parsed.success) return 'invalid'
|
|
97
|
+
const current = projectWallTypes(state.nodes, state.rootNodeIds)
|
|
98
|
+
const editing = current.some((entry) => entry.id === type.id)
|
|
99
|
+
if (!editing && current.length >= PROJECT_WALL_TYPE_LIMIT) return 'too-many-types'
|
|
100
|
+
const next = editing
|
|
101
|
+
? current.map((entry) => (entry.id === type.id ? parsed.data : entry))
|
|
102
|
+
: [...current, parsed.data]
|
|
103
|
+
state.updateNode(site.id as AnyNodeId, { wallTypes: next } as Partial<SiteNode>)
|
|
104
|
+
return 'saved'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Removes a project wall type from the site. Walls drawn with it keep their stack. */
|
|
108
|
+
export function deleteProjectWallType(id: string): void {
|
|
109
|
+
const state = useScene.getState()
|
|
110
|
+
const site = siteOf(state)
|
|
111
|
+
if (!site) return
|
|
112
|
+
const next = projectWallTypes(state.nodes, state.rootNodeIds).filter((type) => type.id !== id)
|
|
113
|
+
state.updateNode(
|
|
114
|
+
site.id as AnyNodeId,
|
|
115
|
+
{
|
|
116
|
+
wallTypes: next.length > 0 ? next : undefined,
|
|
117
|
+
} as Partial<SiteNode>,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
getDisplayedWindowShutterValue,
|
|
4
|
+
getDisplayedWindowValue,
|
|
5
|
+
openWindowOpenState,
|
|
6
|
+
setWindowShuttersOpenState,
|
|
7
|
+
useInteractive,
|
|
8
|
+
useScene,
|
|
9
|
+
WindowNode,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
|
|
12
|
+
const originalScene = useScene.getState()
|
|
13
|
+
const originalInteractive = useInteractive.getState()
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
useScene.setState(originalScene)
|
|
16
|
+
useInteractive.setState(originalInteractive)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test.each(['box', 'concealed'] as const)(
|
|
20
|
+
'%s shutter previews use saved defaults, can reverse, and leave sash animation queued',
|
|
21
|
+
(rollerShutter) => {
|
|
22
|
+
const node = WindowNode.parse({ windowType: 'casement', shutters: 'panel', rollerShutter })
|
|
23
|
+
useScene.setState({ nodes: { [node.id]: node } })
|
|
24
|
+
useInteractive.setState({ windows: {}, windowAnimations: {} })
|
|
25
|
+
expect(getDisplayedWindowShutterValue(node.id, 'shutterOpen', undefined)).toBe(1)
|
|
26
|
+
expect(getDisplayedWindowShutterValue(node.id, 'rollerShutterOpen', 0.7)).toBe(0.7)
|
|
27
|
+
openWindowOpenState(node.id, { persist: false })
|
|
28
|
+
setWindowShuttersOpenState(node.id, false, { persist: false })
|
|
29
|
+
expect(getDisplayedWindowValue(node.id, node.operationState)).toBe(0)
|
|
30
|
+
expect(useInteractive.getState().windowAnimations[node.id]?.operationState?.to).toBe(1)
|
|
31
|
+
useInteractive
|
|
32
|
+
.getState()
|
|
33
|
+
.setWindowOpenState(node.id, { shutterOpen: 0.3, rollerShutterOpen: 0.5 })
|
|
34
|
+
setWindowShuttersOpenState(node.id, true, { persist: false })
|
|
35
|
+
expect(useInteractive.getState().windowAnimations[node.id]).toMatchObject({
|
|
36
|
+
shutterOpen: { from: 0.3, to: 1, persist: false },
|
|
37
|
+
rollerShutterOpen: { from: 0.5, to: 1, persist: false },
|
|
38
|
+
operationState: { to: 1 },
|
|
39
|
+
})
|
|
40
|
+
useInteractive.getState().cancelWindowAnimation(node.id)
|
|
41
|
+
useInteractive.getState().removeWindowOpenState(node.id)
|
|
42
|
+
expect(getDisplayedWindowShutterValue(node.id, 'shutterOpen', 0.7)).toBe(0.7)
|
|
43
|
+
},
|
|
44
|
+
)
|