@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
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { BuildingNode, LevelNode, useScene } from '@pascal-app/core'
|
|
2
|
+
import { BuildingNode, LevelNode, snapWorldXZToBuildingLocal, useScene } from '@pascal-app/core'
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
-
import
|
|
4
|
+
import useEditor from '../store/use-editor'
|
|
5
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
6
|
+
import {
|
|
7
|
+
getActiveBuildingId,
|
|
8
|
+
getActiveGridPose,
|
|
9
|
+
resolveAlignmentForActiveBuilding,
|
|
10
|
+
resolveAlignmentForFloorplanView,
|
|
11
|
+
snapBuildingLocalToWorldGrid,
|
|
12
|
+
snapWorldXZForActiveBuilding,
|
|
13
|
+
} from './world-grid-snap'
|
|
5
14
|
|
|
6
15
|
const originalNodes = useScene.getState().nodes
|
|
7
16
|
const originalSelection = useViewer.getState().selection
|
|
17
|
+
const originalPhase = useEditor.getState().phase
|
|
8
18
|
|
|
9
19
|
afterEach(() => {
|
|
10
20
|
useScene.setState({ nodes: originalNodes } as never)
|
|
11
21
|
useViewer.setState({ selection: originalSelection } as never)
|
|
22
|
+
useEditor.setState({ phase: originalPhase })
|
|
23
|
+
useInteractionScope.getState().end()
|
|
12
24
|
})
|
|
13
25
|
|
|
14
26
|
describe('getActiveBuildingId', () => {
|
|
@@ -46,3 +58,128 @@ describe('getActiveBuildingId', () => {
|
|
|
46
58
|
expect(getActiveBuildingId()).toBe(building.id)
|
|
47
59
|
})
|
|
48
60
|
})
|
|
61
|
+
|
|
62
|
+
// A building the site planner set on its lot: turned and offset. The plan only
|
|
63
|
+
// carries the level, as it often does (`selection.buildingId` null).
|
|
64
|
+
const YAW = (17 * Math.PI) / 180
|
|
65
|
+
const POSITION: [number, number, number] = [12.37, 0, -8.91]
|
|
66
|
+
|
|
67
|
+
function selectTurnedBuilding() {
|
|
68
|
+
const level = LevelNode.parse({})
|
|
69
|
+
const building = BuildingNode.parse({
|
|
70
|
+
position: POSITION,
|
|
71
|
+
rotation: [0, YAW, 0],
|
|
72
|
+
children: [level.id],
|
|
73
|
+
})
|
|
74
|
+
useScene.setState({
|
|
75
|
+
nodes: { [building.id]: building, [level.id]: { ...level, parentId: building.id } },
|
|
76
|
+
} as never)
|
|
77
|
+
useViewer.setState({
|
|
78
|
+
selection: { buildingId: null, levelId: level.id, zoneId: null, selectedIds: [] },
|
|
79
|
+
} as never)
|
|
80
|
+
return building
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("grid snap on the building's own lattice", () => {
|
|
84
|
+
test('a turned, offset building snaps square to itself, not to the world grid', () => {
|
|
85
|
+
selectTurnedBuilding()
|
|
86
|
+
expect(snapBuildingLocalToWorldGrid([1.26, 3.74], 0.5)).toEqual([1.5, 3.5])
|
|
87
|
+
expect(snapBuildingLocalToWorldGrid([1.26, 3.74], 0)).toEqual([1.26, 3.74])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('an unturned building at the origin keeps the world grid, which is its lattice', () => {
|
|
91
|
+
const building = BuildingNode.parse({})
|
|
92
|
+
useScene.setState({ nodes: { [building.id]: building } } as never)
|
|
93
|
+
useViewer.setState({
|
|
94
|
+
selection: { buildingId: building.id, levelId: null, zoneId: null, selectedIds: [] },
|
|
95
|
+
} as never)
|
|
96
|
+
expect(snapBuildingLocalToWorldGrid([1.26, 3.74], 0.5)).toEqual([1.5, 3.5])
|
|
97
|
+
expect(snapWorldXZForActiveBuilding(1.26, 3.74, 0.5)).toEqual({
|
|
98
|
+
world: [1.5, 3.5],
|
|
99
|
+
local: [1.5, 3.5],
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('a world cursor lands on the building lattice, in both frames', () => {
|
|
104
|
+
selectTurnedBuilding()
|
|
105
|
+
const { world, local } = snapWorldXZForActiveBuilding(15.2, -4.4, 0.5)
|
|
106
|
+
const raw = snapWorldXZToBuildingLocal(15.2, -4.4, POSITION, YAW, 0).local
|
|
107
|
+
expect(local).toEqual(snapBuildingLocalToWorldGrid(raw, 0.5))
|
|
108
|
+
const back = snapWorldXZToBuildingLocal(world[0], world[1], POSITION, YAW, 0).local
|
|
109
|
+
expect(back[0]).toBeCloseTo(local[0], 9)
|
|
110
|
+
expect(back[1]).toBeCloseTo(local[1], 9)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('resolveAlignmentForFloorplanView', () => {
|
|
115
|
+
test("aligns on a turned building's own axes and returns building-local guides", () => {
|
|
116
|
+
selectTurnedBuilding()
|
|
117
|
+
const result = resolveAlignmentForFloorplanView({
|
|
118
|
+
moving: [{ nodeId: 'moving', kind: 'corner', x: 2.05, z: 1 }],
|
|
119
|
+
candidates: [{ nodeId: 'wall', kind: 'corner', x: 2, z: 5 }],
|
|
120
|
+
threshold: 0.08,
|
|
121
|
+
})
|
|
122
|
+
expect(result.snap?.dx).toBeCloseTo(-0.05, 9)
|
|
123
|
+
expect(result.snap?.dz).toBeCloseTo(0, 9)
|
|
124
|
+
expect(result.guides).toHaveLength(1)
|
|
125
|
+
expect(result.guides[0]?.axis).toBe('x')
|
|
126
|
+
expect(result.guides[0]?.anchor).toEqual({ x: 2, z: 5 })
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('resolveAlignmentForActiveBuilding', () => {
|
|
131
|
+
test("aligns on a turned building's own axes; guide ends come back in world coords", () => {
|
|
132
|
+
selectTurnedBuilding()
|
|
133
|
+
const result = resolveAlignmentForActiveBuilding({
|
|
134
|
+
moving: [{ nodeId: 'moving', kind: 'corner', x: 2.05, z: 1 }],
|
|
135
|
+
candidates: [{ nodeId: 'wall', kind: 'corner', x: 2, z: 5 }],
|
|
136
|
+
threshold: 0.08,
|
|
137
|
+
})
|
|
138
|
+
expect(result.snap?.dx).toBeCloseTo(-0.05, 9)
|
|
139
|
+
expect(result.snap?.dz).toBeCloseTo(0, 9)
|
|
140
|
+
const anchor = result.guides[0]?.anchor
|
|
141
|
+
const ends = [result.guides[0]?.from, result.guides[0]?.to]
|
|
142
|
+
// The candidate end sits at the anchor's world position.
|
|
143
|
+
const world = ends.find(
|
|
144
|
+
(end) =>
|
|
145
|
+
end &&
|
|
146
|
+
Math.abs(end.x - (POSITION[0] + 2 * Math.cos(YAW) + 5 * Math.sin(YAW))) < 1e-9 &&
|
|
147
|
+
Math.abs(end.z - (POSITION[2] - 2 * Math.sin(YAW) + 5 * Math.cos(YAW))) < 1e-9,
|
|
148
|
+
)
|
|
149
|
+
expect(anchor).toEqual({ x: 2, z: 5 })
|
|
150
|
+
expect(world).toBeDefined()
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('getActiveGridPose (the frame the 3D grid is drawn in)', () => {
|
|
155
|
+
test("is the active building's pose while working in it", () => {
|
|
156
|
+
useEditor.setState({ phase: 'building' })
|
|
157
|
+
selectTurnedBuilding()
|
|
158
|
+
expect(getActiveGridPose()).toEqual({ position: POSITION, rotationY: YAW })
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('is the world grid for site work and while the building itself moves', () => {
|
|
162
|
+
const building = selectTurnedBuilding()
|
|
163
|
+
useEditor.setState({ phase: 'site' })
|
|
164
|
+
expect(getActiveGridPose()).toBeNull()
|
|
165
|
+
|
|
166
|
+
useEditor.setState({ phase: 'building' })
|
|
167
|
+
useInteractionScope.getState().begin({
|
|
168
|
+
kind: 'moving',
|
|
169
|
+
node: building,
|
|
170
|
+
nodeId: building.id,
|
|
171
|
+
nodeType: 'building',
|
|
172
|
+
view: '3d',
|
|
173
|
+
})
|
|
174
|
+
expect(getActiveGridPose()).toBeNull()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('is the world grid with no building', () => {
|
|
178
|
+
useEditor.setState({ phase: 'building' })
|
|
179
|
+
useScene.setState({ nodes: {} } as never)
|
|
180
|
+
useViewer.setState({
|
|
181
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
182
|
+
} as never)
|
|
183
|
+
expect(getActiveGridPose()).toBeNull()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Grid snap and alignment frames for tools that consume `grid:move` /
|
|
3
|
+
* `grid:click` inside a building.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* A building placed on a lot by the site planner is usually turned (yaw) and
|
|
6
|
+
* offset. Everything drawn or placed in it — walls, slabs, rooms, furniture,
|
|
7
|
+
* doors and windows — lives in the building's own frame, and so does the grid
|
|
8
|
+
* both views draw for it: the floor plan's grid sits inside the rotated scene
|
|
9
|
+
* group, and the 3D grid is laid on the building (`getActiveGridPose`). The
|
|
10
|
+
* snaps here follow that lattice, so a wall drawn square to the house stays
|
|
11
|
+
* square to it and lands on the lattice its neighbours were drawn on. A
|
|
12
|
+
* world-XZ lattice sits at the building's yaw to the house and off it by the
|
|
13
|
+
* building's offset, leaving drafted ends a fraction short of the walls they
|
|
14
|
+
* meet. For an unturned building at the origin the two coincide; outside any
|
|
15
|
+
* building the world grid is the lattice.
|
|
10
16
|
*/
|
|
11
17
|
import {
|
|
12
18
|
type AlignmentAnchor,
|
|
@@ -21,6 +27,8 @@ import {
|
|
|
21
27
|
useScene,
|
|
22
28
|
} from '@pascal-app/core'
|
|
23
29
|
import { useViewer } from '@pascal-app/viewer'
|
|
30
|
+
import useEditor from '../store/use-editor'
|
|
31
|
+
import { getMovingNode } from '../store/use-interaction-scope'
|
|
24
32
|
|
|
25
33
|
/**
|
|
26
34
|
* Look up the active building's pose, or null when we're at the site root.
|
|
@@ -68,13 +76,26 @@ export function getActiveBuildingPose(): BuildingPose | null {
|
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* (the
|
|
74
|
-
*
|
|
79
|
+
* The frame of the lattice the 3D grid is drawn on: the active building's pose
|
|
80
|
+
* for everything drawn or placed in it, or null — the world XZ grid — for
|
|
81
|
+
* site work (the site phase, which terrain sculpting also runs in), while a
|
|
82
|
+
* building itself is being moved (it moves on the site), and with no building.
|
|
83
|
+
*/
|
|
84
|
+
export function getActiveGridPose(): BuildingPose | null {
|
|
85
|
+
if (useEditor.getState().phase === 'site') return null
|
|
86
|
+
if (getMovingNode()?.type === 'building') return null
|
|
87
|
+
return getActiveBuildingPose()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve Figma-style alignment for 3D tools whose anchors are in the active
|
|
92
|
+
* building's local frame, on the building's own axes (the axes its grid is
|
|
93
|
+
* drawn on). Wraps `resolveAlignmentInBuildingWorld` with the active building
|
|
94
|
+
* lookup so callers don't repeat the boilerplate.
|
|
75
95
|
*
|
|
76
96
|
* Returns:
|
|
77
|
-
* - `guides` in WORLD coords (
|
|
97
|
+
* - `guides` with `from` / `to` in WORLD coords (callers bring them back
|
|
98
|
+
* with `projectAlignmentGuidesWorldToActiveBuildingLocal`),
|
|
78
99
|
* - `snap` in BUILDING-LOCAL coords (ready to add to a local position).
|
|
79
100
|
*/
|
|
80
101
|
export function resolveAlignmentForActiveBuilding(args: {
|
|
@@ -82,7 +103,24 @@ export function resolveAlignmentForActiveBuilding(args: {
|
|
|
82
103
|
candidates: readonly AlignmentAnchor[]
|
|
83
104
|
threshold: number
|
|
84
105
|
}): ResolveAlignmentInBuildingResult {
|
|
85
|
-
|
|
106
|
+
// Pose `null` resolves in the anchors' own (building-local) frame.
|
|
107
|
+
const result = resolveAlignmentInBuildingWorld({ ...args, pose: null })
|
|
108
|
+
const pose = getActiveBuildingPose()
|
|
109
|
+
if (!pose) return result
|
|
110
|
+
return {
|
|
111
|
+
snap: result.snap,
|
|
112
|
+
guides: result.guides.map((guide) => {
|
|
113
|
+
const [fromX, fromZ] = poseLocalXZToWorld(guide.from.x, guide.from.z, pose)
|
|
114
|
+
const [toX, toZ] = poseLocalXZToWorld(guide.to.x, guide.to.z, pose)
|
|
115
|
+
return { ...guide, from: { x: fromX, z: fromZ }, to: { x: toX, z: toZ } }
|
|
116
|
+
}),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function poseLocalXZToWorld(x: number, z: number, pose: BuildingPose): [number, number] {
|
|
121
|
+
const cos = Math.cos(pose.rotationY)
|
|
122
|
+
const sin = Math.sin(pose.rotationY)
|
|
123
|
+
return [pose.position[0] + x * cos + z * sin, pose.position[2] - x * sin + z * cos]
|
|
86
124
|
}
|
|
87
125
|
|
|
88
126
|
function worldXZToPoseLocal(x: number, z: number, pose: BuildingPose | null): [number, number] {
|
|
@@ -97,9 +135,10 @@ function worldXZToPoseLocal(x: number, z: number, pose: BuildingPose | null): [n
|
|
|
97
135
|
/**
|
|
98
136
|
* Project WORLD-frame alignment guides into the active building's LOCAL frame.
|
|
99
137
|
*
|
|
100
|
-
* The 3D alignment layer is
|
|
101
|
-
*
|
|
102
|
-
*
|
|
138
|
+
* The 3D alignment layer is mounted inside the building-local tool group, so
|
|
139
|
+
* tools that resolve through `resolveAlignmentForActiveBuilding` (item
|
|
140
|
+
* placement, slab move) convert its guides before publishing to
|
|
141
|
+
* `useAlignmentGuides`.
|
|
103
142
|
*/
|
|
104
143
|
export function projectAlignmentGuidesWorldToActiveBuildingLocal(
|
|
105
144
|
guides: readonly AlignmentGuide[],
|
|
@@ -117,119 +156,77 @@ export function projectAlignmentGuidesWorldToActiveBuildingLocal(
|
|
|
117
156
|
}
|
|
118
157
|
|
|
119
158
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* the
|
|
123
|
-
*/
|
|
124
|
-
const FLOORPLAN_VIEW_ROTATION_RAD = 0
|
|
125
|
-
|
|
126
|
-
function rotateAnchorsBy(
|
|
127
|
-
anchors: readonly AlignmentAnchor[],
|
|
128
|
-
cos: number,
|
|
129
|
-
sin: number,
|
|
130
|
-
): AlignmentAnchor[] {
|
|
131
|
-
return anchors.map((a) => ({
|
|
132
|
-
nodeId: a.nodeId,
|
|
133
|
-
kind: a.kind,
|
|
134
|
-
x: a.x * cos - a.z * sin,
|
|
135
|
-
z: a.x * sin + a.z * cos,
|
|
136
|
-
}))
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Resolve alignment in the 2D floor-plan view frame — the frame the user
|
|
141
|
-
* sees the (always axis-aligned) grid lines in, regardless of how the
|
|
142
|
-
* building has been rotated. Use this from EVERY 2D floor-plan path so
|
|
143
|
-
* alignment guides stay parallel to the visible grid.
|
|
159
|
+
* Resolve alignment for the 2D floor plan, on the active building's own axes.
|
|
160
|
+
* Use this from EVERY 2D floor-plan path so alignment guides stay parallel to
|
|
161
|
+
* the plan's grid.
|
|
144
162
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* world axes, which appear diagonal in this view; view-frame alignment
|
|
152
|
-
* lands on the SVG axes the user actually reads.
|
|
163
|
+
* The floor plan rotates its scene `<g>` by the building's yaw (and the user's
|
|
164
|
+
* view rotation) and draws both its grid and the alignment-guide layer inside
|
|
165
|
+
* that group, in building-local metres. So the axes the user reads are the
|
|
166
|
+
* building's local axes, and guides must come back in local coords: resolving
|
|
167
|
+
* on world axes would align a turned building's corners on a diagonal to its
|
|
168
|
+
* walls and draw each guide away from the anchors it matched.
|
|
153
169
|
*
|
|
154
|
-
* Returns guides in
|
|
155
|
-
*
|
|
156
|
-
* group) and a snap delta projected back into building-local (so callers
|
|
157
|
-
* can add it to a local position as-is).
|
|
170
|
+
* Returns guides and the snap delta in building-local coords (add the delta
|
|
171
|
+
* to a local position as-is).
|
|
158
172
|
*/
|
|
159
173
|
export function resolveAlignmentForFloorplanView(args: {
|
|
160
174
|
moving: readonly AlignmentAnchor[]
|
|
161
175
|
candidates: readonly AlignmentAnchor[]
|
|
162
176
|
threshold: number
|
|
163
177
|
}): ResolveAlignmentInBuildingResult {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const rot = FLOORPLAN_VIEW_ROTATION_RAD - buildingRotY
|
|
167
|
-
const cos = Math.cos(rot)
|
|
168
|
-
const sin = Math.sin(rot)
|
|
169
|
-
const result = resolveAlignmentInBuildingWorld({
|
|
170
|
-
moving: rotateAnchorsBy(args.moving, cos, sin),
|
|
171
|
-
candidates: rotateAnchorsBy(args.candidates, cos, sin),
|
|
172
|
-
threshold: args.threshold,
|
|
173
|
-
// Pose `null` keeps `resolveAlignmentInBuildingWorld` in its no-op
|
|
174
|
-
// path: it just runs `resolveAlignment` on the already-rotated
|
|
175
|
-
// anchors and returns the snap delta in the same view frame.
|
|
176
|
-
pose: null,
|
|
177
|
-
})
|
|
178
|
-
if (!result.snap) return result
|
|
179
|
-
// View-frame delta → local-frame delta (transpose of R(rot)).
|
|
180
|
-
const { dx, dz } = result.snap
|
|
181
|
-
return {
|
|
182
|
-
guides: result.guides,
|
|
183
|
-
snap: { dx: dx * cos + dz * sin, dz: -dx * sin + dz * cos },
|
|
184
|
-
}
|
|
178
|
+
// Pose `null` resolves in the anchors' own (building-local) frame.
|
|
179
|
+
return resolveAlignmentInBuildingWorld({ ...args, pose: null })
|
|
185
180
|
}
|
|
186
181
|
|
|
187
182
|
/**
|
|
188
|
-
* Snap a world XZ position
|
|
189
|
-
*
|
|
183
|
+
* Snap a world XZ position onto the active building's grid lattice (see
|
|
184
|
+
* `snapBuildingLocalToWorldGrid`) and return the snapped point in both
|
|
185
|
+
* frames. With no active building, world == local and the world grid is the
|
|
186
|
+
* lattice.
|
|
190
187
|
*/
|
|
191
188
|
export function snapWorldXZForActiveBuilding(
|
|
192
189
|
worldX: number,
|
|
193
190
|
worldZ: number,
|
|
194
191
|
step: number,
|
|
195
192
|
): { world: [number, number]; local: [number, number] } {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const sx = Math.round(worldX / step) * step
|
|
201
|
-
const sz = Math.round(worldZ / step) * step
|
|
202
|
-
return { world: [sx, sz], local: [sx, sz] }
|
|
193
|
+
const pose = getActiveBuildingPose()
|
|
194
|
+
if (!pose) {
|
|
195
|
+
const [x, z] = snapBuildingLocalToWorldGrid([worldX, worldZ], step)
|
|
196
|
+
return { world: [x, z], local: [x, z] }
|
|
203
197
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
building.position,
|
|
208
|
-
building.rotation[1] ?? 0,
|
|
209
|
-
step,
|
|
210
|
-
)
|
|
198
|
+
const { local } = snapWorldXZToBuildingLocal(worldX, worldZ, pose.position, pose.rotationY, 0)
|
|
199
|
+
const [x, z] = snapBuildingLocalToWorldGrid(local, step)
|
|
200
|
+
return { world: poseLocalXZToWorld(x, z, pose), local: [x, z] }
|
|
211
201
|
}
|
|
212
202
|
|
|
213
203
|
/**
|
|
214
|
-
* Snap a
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
204
|
+
* Snap a plan point in the 3D tools' frame (a grid event's `localPosition`)
|
|
205
|
+
* onto the lattice the grid is drawn on, one axis at a time with the caller's
|
|
206
|
+
* per-axis snap (an item snaps its edges by its own size). Inside a building
|
|
207
|
+
* the point is building-local and the lattice runs along the building's axes;
|
|
208
|
+
* outside any building it is the world point and the world grid is the
|
|
209
|
+
* lattice — so no pose is needed either way.
|
|
210
|
+
*/
|
|
211
|
+
export function snapPlanPointToActiveGrid(
|
|
212
|
+
point: readonly [number, number],
|
|
213
|
+
snap: (value: number, axis: 0 | 1) => number,
|
|
214
|
+
): [number, number] {
|
|
215
|
+
return [snap(point[0], 0), snap(point[1], 1)]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Snap a building-local plan point onto the building's own grid lattice:
|
|
220
|
+
* multiples of `step` along its local axes from its origin, the lattice both
|
|
221
|
+
* views draw and the wall tool drafts on. Coordinates in and out are
|
|
222
|
+
* building-local, so it suits a `gridSnap` callback for snapWallDraftPoint /
|
|
223
|
+
* snapFenceDraftPoint etc. (The name predates the building frame: it used to
|
|
224
|
+
* snap onto the world XZ grid, at a turned building's yaw to the house.)
|
|
218
225
|
*/
|
|
219
226
|
export function snapBuildingLocalToWorldGrid(
|
|
220
227
|
local: readonly [number, number],
|
|
221
228
|
step: number,
|
|
222
229
|
): [number, number] {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (building?.type !== 'building') {
|
|
226
|
-
if (step <= 0) return [local[0], local[1]]
|
|
227
|
-
return [Math.round(local[0] / step) * step, Math.round(local[1] / step) * step]
|
|
228
|
-
}
|
|
229
|
-
const rotY = building.rotation[1] ?? 0
|
|
230
|
-
const cos = Math.cos(rotY)
|
|
231
|
-
const sin = Math.sin(rotY)
|
|
232
|
-
const worldX = building.position[0] + local[0] * cos + local[1] * sin
|
|
233
|
-
const worldZ = building.position[2] - local[0] * sin + local[1] * cos
|
|
234
|
-
return snapWorldXZToBuildingLocal(worldX, worldZ, building.position, rotY, step).local
|
|
230
|
+
if (step <= 0) return [local[0], local[1]]
|
|
231
|
+
return snapPlanPointToActiveGrid(local, (value) => Math.round(value / step) * step)
|
|
235
232
|
}
|
|
@@ -6,16 +6,42 @@
|
|
|
6
6
|
import type { AlignmentGuide } from '@pascal-app/core'
|
|
7
7
|
import { create } from 'zustand'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* A wall draft's dashed projection guide, in building-local plan coordinates:
|
|
11
|
+
* the extension of the wall the draft comes from, a line through another wall's
|
|
12
|
+
* end on one of the house's axes, or the same through the run's start.
|
|
13
|
+
*/
|
|
14
|
+
export type DraftGuideLine = {
|
|
15
|
+
kind: 'extension' | 'alignment' | 'run-start'
|
|
16
|
+
from: [number, number]
|
|
17
|
+
to: [number, number]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The projection guides of a wall pulled from a selected wall's end (and of
|
|
22
|
+
* the run it continues): dashed lines, and markers where two of them cross.
|
|
23
|
+
*/
|
|
24
|
+
export type DraftGuides = {
|
|
25
|
+
lines: DraftGuideLine[]
|
|
26
|
+
intersections: Array<[number, number]>
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
type AlignmentGuidesState = {
|
|
10
30
|
guides: AlignmentGuide[]
|
|
31
|
+
/** A wall draft's projection guides, drawn by the same layers; null when none. */
|
|
32
|
+
draft: DraftGuides | null
|
|
11
33
|
set(guides: AlignmentGuide[]): void
|
|
34
|
+
/** Publish a wall draft's projection guides, which take the place of alignment guides. */
|
|
35
|
+
setDraft(draft: DraftGuides | null): void
|
|
12
36
|
clear(): void
|
|
13
37
|
}
|
|
14
38
|
|
|
15
39
|
const useAlignmentGuides = create<AlignmentGuidesState>((set) => ({
|
|
16
40
|
guides: [],
|
|
41
|
+
draft: null,
|
|
17
42
|
set: (guides) => set({ guides }),
|
|
18
|
-
|
|
43
|
+
setDraft: (draft) => set({ guides: [], draft }),
|
|
44
|
+
clear: () => set({ guides: [], draft: null }),
|
|
19
45
|
}))
|
|
20
46
|
|
|
21
47
|
export default useAlignmentGuides
|
package/src/store/use-editor.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ChimneyMaterialRole,
|
|
11
11
|
DEFAULT_BRUSH_SETTINGS,
|
|
12
12
|
type DormerSurfaceMaterialRole,
|
|
13
|
+
isMoveLocked,
|
|
13
14
|
type LevelNode,
|
|
14
15
|
nodeRegistry,
|
|
15
16
|
type RoofSurfaceMaterialRole,
|
|
@@ -528,6 +529,24 @@ type EditorState = {
|
|
|
528
529
|
setReferenceFloorOffset: (offset: number) => void
|
|
529
530
|
referenceFloorOpacity: number
|
|
530
531
|
setReferenceFloorOpacity: (opacity: number) => void
|
|
532
|
+
// The 2D plan's dashed outlines of the walls on the level below / above
|
|
533
|
+
// the active one. "Below" is set apart for the roof plan, where it traces
|
|
534
|
+
// the top storey's walls the roof is checked against and starts on, and the
|
|
535
|
+
// floor plans, where it starts off (`useWallsBelowSetting`). Persisted.
|
|
536
|
+
showWallsBelow: boolean
|
|
537
|
+
setShowWallsBelow: (show: boolean) => void
|
|
538
|
+
showRoofWallsBelow: boolean
|
|
539
|
+
setShowRoofWallsBelow: (show: boolean) => void
|
|
540
|
+
showWallsAbove: boolean
|
|
541
|
+
setShowWallsAbove: (show: boolean) => void
|
|
542
|
+
// Ceilings drawn solid from above as well as from below. Set apart like
|
|
543
|
+
// "Walls below": on for the roof level, where the roof is checked over closed
|
|
544
|
+
// rooms, off for the floor plans, where the camera looks into the rooms
|
|
545
|
+
// (`useCeilingsSetting`). Persisted.
|
|
546
|
+
showCeilings: boolean
|
|
547
|
+
setShowCeilings: (show: boolean) => void
|
|
548
|
+
showRoofCeilings: boolean
|
|
549
|
+
setShowRoofCeilings: (show: boolean) => void
|
|
531
550
|
// Development-only camera debug flag for inspecting underside geometry
|
|
532
551
|
allowUndergroundCamera: boolean
|
|
533
552
|
setAllowUndergroundCamera: (enabled: boolean) => void
|
|
@@ -605,6 +624,11 @@ type PersistedEditorLayoutState = Pick<
|
|
|
605
624
|
| 'showReferenceFloor'
|
|
606
625
|
| 'referenceFloorOffset'
|
|
607
626
|
| 'referenceFloorOpacity'
|
|
627
|
+
| 'showWallsBelow'
|
|
628
|
+
| 'showRoofWallsBelow'
|
|
629
|
+
| 'showWallsAbove'
|
|
630
|
+
| 'showCeilings'
|
|
631
|
+
| 'showRoofCeilings'
|
|
608
632
|
>
|
|
609
633
|
type PersistedEditorState = PersistedEditorUiState & PersistedEditorLayoutState
|
|
610
634
|
|
|
@@ -640,6 +664,11 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
640
664
|
showReferenceFloor: false,
|
|
641
665
|
referenceFloorOffset: 1,
|
|
642
666
|
referenceFloorOpacity: 0.35,
|
|
667
|
+
showWallsBelow: false,
|
|
668
|
+
showRoofWallsBelow: true,
|
|
669
|
+
showWallsAbove: false,
|
|
670
|
+
showCeilings: false,
|
|
671
|
+
showRoofCeilings: true,
|
|
643
672
|
}
|
|
644
673
|
|
|
645
674
|
const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
|
|
@@ -899,6 +928,11 @@ export function normalizePersistedEditorLayoutState(
|
|
|
899
928
|
Number.isFinite(state.referenceFloorOpacity)
|
|
900
929
|
? Math.min(0.8, Math.max(0.1, state.referenceFloorOpacity))
|
|
901
930
|
: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOpacity,
|
|
931
|
+
showWallsBelow: state?.showWallsBelow === true,
|
|
932
|
+
showRoofWallsBelow: state?.showRoofWallsBelow !== false,
|
|
933
|
+
showWallsAbove: state?.showWallsAbove === true,
|
|
934
|
+
showCeilings: state?.showCeilings === true,
|
|
935
|
+
showRoofCeilings: state?.showRoofCeilings !== false,
|
|
902
936
|
}
|
|
903
937
|
}
|
|
904
938
|
|
|
@@ -1223,6 +1257,10 @@ const useEditor = create<EditorState>()(
|
|
|
1223
1257
|
set({ placementDragMode: false })
|
|
1224
1258
|
return
|
|
1225
1259
|
}
|
|
1260
|
+
if (isMoveLocked(node)) {
|
|
1261
|
+
set({ placementDragMode: false })
|
|
1262
|
+
return
|
|
1263
|
+
}
|
|
1226
1264
|
const targetNode = node
|
|
1227
1265
|
const isNew = Boolean((targetNode as { metadata?: { isNew?: boolean } }).metadata?.isNew)
|
|
1228
1266
|
if (isNew) {
|
|
@@ -1568,6 +1606,16 @@ const useEditor = create<EditorState>()(
|
|
|
1568
1606
|
referenceFloorOpacity: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOpacity,
|
|
1569
1607
|
setReferenceFloorOpacity: (opacity) =>
|
|
1570
1608
|
set({ referenceFloorOpacity: Math.min(0.8, Math.max(0.1, opacity)) }),
|
|
1609
|
+
showWallsBelow: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showWallsBelow,
|
|
1610
|
+
setShowWallsBelow: (show) => set({ showWallsBelow: show }),
|
|
1611
|
+
showRoofWallsBelow: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showRoofWallsBelow,
|
|
1612
|
+
setShowRoofWallsBelow: (show) => set({ showRoofWallsBelow: show }),
|
|
1613
|
+
showWallsAbove: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showWallsAbove,
|
|
1614
|
+
setShowWallsAbove: (show) => set({ showWallsAbove: show }),
|
|
1615
|
+
showCeilings: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showCeilings,
|
|
1616
|
+
setShowCeilings: (show) => set({ showCeilings: show }),
|
|
1617
|
+
showRoofCeilings: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showRoofCeilings,
|
|
1618
|
+
setShowRoofCeilings: (show) => set({ showRoofCeilings: show }),
|
|
1571
1619
|
allowUndergroundCamera: false,
|
|
1572
1620
|
setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
|
|
1573
1621
|
show2dVoronoi: false,
|
|
@@ -1672,6 +1720,11 @@ const useEditor = create<EditorState>()(
|
|
|
1672
1720
|
showReferenceFloor: state.showReferenceFloor,
|
|
1673
1721
|
referenceFloorOffset: state.referenceFloorOffset,
|
|
1674
1722
|
referenceFloorOpacity: state.referenceFloorOpacity,
|
|
1723
|
+
showWallsBelow: state.showWallsBelow,
|
|
1724
|
+
showRoofWallsBelow: state.showRoofWallsBelow,
|
|
1725
|
+
showWallsAbove: state.showWallsAbove,
|
|
1726
|
+
showCeilings: state.showCeilings,
|
|
1727
|
+
showRoofCeilings: state.showRoofCeilings,
|
|
1675
1728
|
}),
|
|
1676
1729
|
skipHydration: true,
|
|
1677
1730
|
},
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'pascal-floorplan-annotation-visibility'
|
|
4
|
+
const savedWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
if (savedWindow) Object.defineProperty(globalThis, 'window', savedWindow)
|
|
7
|
+
else Reflect.deleteProperty(globalThis, 'window')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
/** A browser's localStorage, seeded with what an earlier session saved. */
|
|
11
|
+
function stubStorage(state?: Record<string, unknown>) {
|
|
12
|
+
const values = new Map<string, string>(
|
|
13
|
+
state ? [[STORAGE_KEY, JSON.stringify({ state, version: 0 })]] : [],
|
|
14
|
+
)
|
|
15
|
+
const localStorage: Partial<Storage> = {
|
|
16
|
+
getItem: (key) => values.get(key) ?? null,
|
|
17
|
+
setItem: (key, value) => void values.set(key, value),
|
|
18
|
+
removeItem: (key) => void values.delete(key),
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(globalThis, 'window', { configurable: true, value: { localStorage } })
|
|
21
|
+
return { read: () => JSON.parse(values.get(STORAGE_KEY) ?? 'null')?.state }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let fresh = 0
|
|
25
|
+
async function loadStore() {
|
|
26
|
+
const { default: store } = await import(
|
|
27
|
+
`./use-floorplan-annotation-visibility.ts?load=${++fresh}`
|
|
28
|
+
)
|
|
29
|
+
return store as typeof import('./use-floorplan-annotation-visibility').default
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('Display dimension preferences', () => {
|
|
33
|
+
test('a first visit shows dimensions and measures walls to both faces', async () => {
|
|
34
|
+
stubStorage()
|
|
35
|
+
const store = await loadStore()
|
|
36
|
+
await store.persist.rehydrate()
|
|
37
|
+
|
|
38
|
+
expect(store.getState().showDimensions).toBe(true)
|
|
39
|
+
expect(store.getState().wallDimensionFaces).toBe('both')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('a stale saved choice, from the retired Wall dimensions option, opens on Both', async () => {
|
|
43
|
+
stubStorage({ wallDimensionReference: 'stud-faces', wallDimensionFaces: 'centerline' })
|
|
44
|
+
const store = await loadStore()
|
|
45
|
+
await store.persist.rehydrate()
|
|
46
|
+
|
|
47
|
+
expect(store.getState().wallDimensionFaces).toBe('both')
|
|
48
|
+
expect(store.getState()).not.toHaveProperty('wallDimensionReference')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('Dimensions and Measure walls come back after a reload', async () => {
|
|
52
|
+
const storage = stubStorage()
|
|
53
|
+
const firstSession = await loadStore()
|
|
54
|
+
await firstSession.persist.rehydrate()
|
|
55
|
+
firstSession.getState().setShowDimensions(false)
|
|
56
|
+
firstSession.getState().setWallDimensionFaces('interior')
|
|
57
|
+
expect(storage.read()).toMatchObject({ showDimensions: false, wallDimensionFaces: 'interior' })
|
|
58
|
+
|
|
59
|
+
const nextSession = await loadStore()
|
|
60
|
+
await nextSession.persist.rehydrate()
|
|
61
|
+
expect(nextSession.getState().showDimensions).toBe(false)
|
|
62
|
+
expect(nextSession.getState().wallDimensionFaces).toBe('interior')
|
|
63
|
+
})
|
|
64
|
+
})
|