@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,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
joinedSeparatorIds,
|
|
5
|
+
runAsSingleSceneHistoryStep,
|
|
6
|
+
type SeparatorNode,
|
|
7
|
+
setSeparatorEnd,
|
|
8
|
+
useLiveNodeOverrides,
|
|
9
|
+
useScene,
|
|
10
|
+
type WallNode,
|
|
11
|
+
type WallPlanPoint,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import { runRoomHandleDrag } from './room-handle-drag'
|
|
14
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
15
|
+
import { levelSeparatorLines } from './separator-lines'
|
|
16
|
+
import { isWallRunFree, publishWallRunFeedback, snapWallRunDraftPoint } from './wall-run-snap'
|
|
17
|
+
|
|
18
|
+
/** Scope label of an invisible wall's end drag (its 3D grip). */
|
|
19
|
+
export const SEPARATOR_END_DRAG_LABEL = 'separator-end'
|
|
20
|
+
|
|
21
|
+
const SAME_POINT = 1e-6
|
|
22
|
+
|
|
23
|
+
export type SeparatorEndDrag = {
|
|
24
|
+
/** The invisible walls the drag may move: the dragged one and those it can carry. */
|
|
25
|
+
affectedIds: AnyNodeId[]
|
|
26
|
+
/** Snaps the end to `point` (level plan) and previews it; `detach` (Alt) leaves the joined ones. */
|
|
27
|
+
move: (point: WallPlanPoint, detach: boolean) => void
|
|
28
|
+
/** Whether the end has moved somewhere it may stay. */
|
|
29
|
+
canCommit: () => boolean
|
|
30
|
+
/** Writes the end as one undo step; false when it did not move or core refused it. */
|
|
31
|
+
commit: () => boolean
|
|
32
|
+
/** Drops the preview; writes nothing. */
|
|
33
|
+
cancel: () => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* An invisible wall's end dragged: its handle on the plan, its grip in 3D. The
|
|
38
|
+
* end snaps as a wall run does — square to the house or to its own line, its
|
|
39
|
+
* length on the grid, onto wall and invisible-wall ends and bodies, so it
|
|
40
|
+
* comes back together on its old corner — the invisible walls joined at that
|
|
41
|
+
* end follow it live, and the release writes it once (`setSeparatorEnd`).
|
|
42
|
+
* The scene is untouched until then.
|
|
43
|
+
*/
|
|
44
|
+
export function beginSeparatorEndDrag(
|
|
45
|
+
separatorId: string,
|
|
46
|
+
endpoint: 'start' | 'end',
|
|
47
|
+
): SeparatorEndDrag | null {
|
|
48
|
+
const nodes = useScene.getState().nodes
|
|
49
|
+
const separator = nodes[separatorId as AnyNodeId]
|
|
50
|
+
if (separator?.type !== 'separator') return null
|
|
51
|
+
const from = separator[endpoint]
|
|
52
|
+
const fixed = endpoint === 'start' ? separator.end : separator.start
|
|
53
|
+
const levelId = separator.parentId
|
|
54
|
+
const atFrom = (point: WallPlanPoint) =>
|
|
55
|
+
Math.abs(point[0] - from[0]) <= SAME_POINT && Math.abs(point[1] - from[1]) <= SAME_POINT
|
|
56
|
+
const others = Object.values(nodes).filter(
|
|
57
|
+
(node): node is SeparatorNode =>
|
|
58
|
+
node.type === 'separator' && node.parentId === levelId && node.id !== separator.id,
|
|
59
|
+
)
|
|
60
|
+
// The ends following this one are no snap targets while they do: they sit where it left.
|
|
61
|
+
const joined = joinedSeparatorIds(nodes, separator, endpoint)
|
|
62
|
+
const walls = Object.values(nodes).filter(
|
|
63
|
+
(node): node is WallNode => node.type === 'wall' && node.parentId === levelId,
|
|
64
|
+
)
|
|
65
|
+
const targets = {
|
|
66
|
+
attached: [
|
|
67
|
+
...walls,
|
|
68
|
+
...levelSeparatorLines(nodes, levelId, new Set([separator.id, ...joined])),
|
|
69
|
+
],
|
|
70
|
+
detached: [...walls, ...levelSeparatorLines(nodes, levelId, new Set([separator.id]))],
|
|
71
|
+
}
|
|
72
|
+
const length = Math.hypot(from[0] - fixed[0], from[1] - fixed[1])
|
|
73
|
+
const reference: WallPlanPoint | null =
|
|
74
|
+
length > 1e-9 ? [(from[0] - fixed[0]) / length, (from[1] - fixed[1]) / length] : null
|
|
75
|
+
let target: { point: WallPlanPoint; detach: boolean } | null = null
|
|
76
|
+
const previewIds = new Set<AnyNodeId>()
|
|
77
|
+
const preview = (updates: Array<[AnyNodeId, Partial<AnyNode>]>) => {
|
|
78
|
+
const overrides = useLiveNodeOverrides.getState()
|
|
79
|
+
const next = new Set(updates.map(([id]) => id))
|
|
80
|
+
for (const id of previewIds) {
|
|
81
|
+
if (next.has(id)) continue
|
|
82
|
+
overrides.clear(id)
|
|
83
|
+
useScene.getState().markDirty(id)
|
|
84
|
+
previewIds.delete(id)
|
|
85
|
+
}
|
|
86
|
+
if (updates.length) overrides.setMany(updates)
|
|
87
|
+
for (const id of next) {
|
|
88
|
+
previewIds.add(id)
|
|
89
|
+
useScene.getState().markDirty(id)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
affectedIds: [separator.id, ...others.map((node) => node.id)] as AnyNodeId[],
|
|
94
|
+
move(point, detach) {
|
|
95
|
+
const snap = snapWallRunDraftPoint({
|
|
96
|
+
point,
|
|
97
|
+
start: fixed,
|
|
98
|
+
runStart: fixed,
|
|
99
|
+
reference,
|
|
100
|
+
walls: detach ? targets.detached : targets.attached,
|
|
101
|
+
free: isWallRunFree(null),
|
|
102
|
+
})
|
|
103
|
+
publishWallRunFeedback(snap)
|
|
104
|
+
const plan = setSeparatorEnd(nodes, { separatorId, endpoint, point: snap.point, detach })
|
|
105
|
+
if (plan.conflicts?.length) {
|
|
106
|
+
target = null
|
|
107
|
+
preview([])
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
target = { point: snap.point, detach }
|
|
111
|
+
preview(
|
|
112
|
+
plan.changes.flatMap(
|
|
113
|
+
(change): Array<[AnyNodeId, Partial<AnyNode>]> =>
|
|
114
|
+
change.op === 'update' ? [[change.id as AnyNodeId, change.data]] : [],
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
},
|
|
118
|
+
canCommit: () => !!target && !atFrom(target.point),
|
|
119
|
+
commit() {
|
|
120
|
+
preview([])
|
|
121
|
+
publishWallRunFeedback(null)
|
|
122
|
+
if (!target || atFrom(target.point)) return false
|
|
123
|
+
const current = useScene.getState().nodes
|
|
124
|
+
if (current[separatorId as AnyNodeId]?.type !== 'separator') return false
|
|
125
|
+
const plan = setSeparatorEnd(current, { separatorId, endpoint, ...target })
|
|
126
|
+
return runAsSingleSceneHistoryStep(useScene, () => applyRoomPlan(plan))
|
|
127
|
+
},
|
|
128
|
+
cancel() {
|
|
129
|
+
preview([])
|
|
130
|
+
publishWallRunFeedback(null)
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* An invisible wall's 3D end grip: `toPlan` reads the pointer on the floor the
|
|
137
|
+
* separator lies on, in level plan metres. One gesture under the lifecycle
|
|
138
|
+
* owner (Escape or ⌘Z cancels), one undo step; Alt leaves the joined
|
|
139
|
+
* invisible walls where they are.
|
|
140
|
+
*/
|
|
141
|
+
export function runSeparatorEndDrag({
|
|
142
|
+
separatorId,
|
|
143
|
+
endpoint,
|
|
144
|
+
levelId,
|
|
145
|
+
toPlan,
|
|
146
|
+
}: {
|
|
147
|
+
separatorId: string
|
|
148
|
+
endpoint: 'start' | 'end'
|
|
149
|
+
levelId: string
|
|
150
|
+
toPlan: (clientX: number, clientY: number) => WallPlanPoint | null
|
|
151
|
+
}) {
|
|
152
|
+
const drag = beginSeparatorEndDrag(separatorId, endpoint)
|
|
153
|
+
if (!drag) return null
|
|
154
|
+
let altHeld = false
|
|
155
|
+
const trackAlt = (event: KeyboardEvent | PointerEvent) => {
|
|
156
|
+
altHeld = event.altKey
|
|
157
|
+
}
|
|
158
|
+
const events = ['pointermove', 'keydown', 'keyup'] as const
|
|
159
|
+
for (const type of events) window.addEventListener(type, trackAlt, true)
|
|
160
|
+
const stop = () => {
|
|
161
|
+
for (const type of events) window.removeEventListener(type, trackAlt, true)
|
|
162
|
+
}
|
|
163
|
+
return runRoomHandleDrag<WallPlanPoint>({
|
|
164
|
+
label: SEPARATOR_END_DRAG_LABEL,
|
|
165
|
+
nodeId: separatorId,
|
|
166
|
+
levelId,
|
|
167
|
+
requires: [separatorId],
|
|
168
|
+
sample: toPlan,
|
|
169
|
+
onValue: (point) => drag.move(point, altHeld),
|
|
170
|
+
onCommit: () => {
|
|
171
|
+
stop()
|
|
172
|
+
drag.commit()
|
|
173
|
+
},
|
|
174
|
+
onCancel: () => {
|
|
175
|
+
stop()
|
|
176
|
+
drag.cancel()
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AnyNode, SeparatorNode, WallNode } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Invisible walls (room separators) as the wall snapping reads walls: a line
|
|
5
|
+
* with no thickness. A drafted or dragged invisible-wall end catches another's
|
|
6
|
+
* ends and body this way, and a pushed one lines up with them, as walls do
|
|
7
|
+
* with walls. Never written to the scene.
|
|
8
|
+
*/
|
|
9
|
+
export function separatorWallLine(separator: SeparatorNode): WallNode {
|
|
10
|
+
return {
|
|
11
|
+
object: 'node',
|
|
12
|
+
id: separator.id as string as WallNode['id'],
|
|
13
|
+
type: 'wall',
|
|
14
|
+
name: separator.name,
|
|
15
|
+
parentId: separator.parentId,
|
|
16
|
+
visible: true,
|
|
17
|
+
metadata: {},
|
|
18
|
+
children: [],
|
|
19
|
+
start: separator.start,
|
|
20
|
+
end: separator.end,
|
|
21
|
+
thickness: 0,
|
|
22
|
+
frontSide: 'unknown',
|
|
23
|
+
backSide: 'unknown',
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The invisible walls on `levelId` as wall lines, leaving out `exclude`. */
|
|
28
|
+
export function levelSeparatorLines(
|
|
29
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
30
|
+
levelId: string | null,
|
|
31
|
+
exclude: ReadonlySet<string> = new Set(),
|
|
32
|
+
): WallNode[] {
|
|
33
|
+
return Object.values(nodes).flatMap((node) =>
|
|
34
|
+
node.type === 'separator' && node.parentId === levelId && !exclude.has(node.id)
|
|
35
|
+
? [separatorWallLine(node)]
|
|
36
|
+
: [],
|
|
37
|
+
)
|
|
38
|
+
}
|
package/src/lib/sfx-bus.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { emitter, type InteractionSound, type TerrainVerb } from '@pascal-app/core'
|
|
2
2
|
import mitt from 'mitt'
|
|
3
3
|
import useAudio from '../store/use-audio'
|
|
4
4
|
import {
|
|
5
5
|
disposeSFX,
|
|
6
6
|
type LoopSFXName,
|
|
7
7
|
playSFX,
|
|
8
|
+
type SFXName,
|
|
8
9
|
startLoopSFX,
|
|
9
10
|
stopLoopSFX,
|
|
10
11
|
updateSFXVolumes,
|
|
@@ -74,6 +75,29 @@ const handleTerrainSculptStart = (verb: TerrainVerb) => startLoopSFX(TERRAIN_LOO
|
|
|
74
75
|
const handleTerrainSculptStop = () => stopLoopSFX()
|
|
75
76
|
let unsubscribeAudio: (() => void) | null = null
|
|
76
77
|
|
|
78
|
+
/** Windows ride the door recordings, softer: a sash weighs less than a leaf. */
|
|
79
|
+
const INTERACTION_SFX: Record<InteractionSound, [SFXName, number]> = {
|
|
80
|
+
'door-open': ['doorOpen', 1],
|
|
81
|
+
'door-swing': ['doorSwing', 1],
|
|
82
|
+
'door-close': ['doorClose', 1],
|
|
83
|
+
'window-open': ['doorOpen', 0.6],
|
|
84
|
+
'window-swing': ['doorSwing', 0.6],
|
|
85
|
+
'window-close': ['doorClose', 0.6],
|
|
86
|
+
garage: ['garageDoor', 1],
|
|
87
|
+
'shutters-open': ['shuttersOpen', 1],
|
|
88
|
+
'shutters-close': ['shuttersClose', 1],
|
|
89
|
+
// Cabinet doors and drawers: the panel recordings, as the marketing house plays its cabinets.
|
|
90
|
+
'panel-open': ['shuttersOpen', 0.75],
|
|
91
|
+
'panel-swing': ['doorSwing', 0.45],
|
|
92
|
+
'panel-close': ['shuttersClose', 0.75],
|
|
93
|
+
'switch-on': ['switchOn', 1],
|
|
94
|
+
'switch-off': ['switchOff', 1],
|
|
95
|
+
}
|
|
96
|
+
const handleInteractionSound = (sound: InteractionSound) => {
|
|
97
|
+
const [name, volumeMultiplier] = INTERACTION_SFX[sound]
|
|
98
|
+
playSFX(name, { volumeMultiplier })
|
|
99
|
+
}
|
|
100
|
+
|
|
77
101
|
/**
|
|
78
102
|
* Initialize SFX Bus - connects SFX events to actual sound playback.
|
|
79
103
|
* Safe to call multiple times; re-registration is a no-op once initialized.
|
|
@@ -100,6 +124,7 @@ export function initSFXBus() {
|
|
|
100
124
|
sfxEmitter.on('sfx:turn-needs-input', handleTurnNeedsInput)
|
|
101
125
|
sfxEmitter.on('sfx:terrain-sculpt-start', handleTerrainSculptStart)
|
|
102
126
|
sfxEmitter.on('sfx:terrain-sculpt-stop', handleTerrainSculptStop)
|
|
127
|
+
emitter.on('interaction:sound', handleInteractionSound)
|
|
103
128
|
unsubscribeAudio = useAudio.subscribe(updateSFXVolumes)
|
|
104
129
|
updateSFXVolumes()
|
|
105
130
|
}
|
|
@@ -125,6 +150,7 @@ export function disposeSFXBus() {
|
|
|
125
150
|
sfxEmitter.off('sfx:turn-needs-input', handleTurnNeedsInput)
|
|
126
151
|
sfxEmitter.off('sfx:terrain-sculpt-start', handleTerrainSculptStart)
|
|
127
152
|
sfxEmitter.off('sfx:terrain-sculpt-stop', handleTerrainSculptStop)
|
|
153
|
+
emitter.off('interaction:sound', handleInteractionSound)
|
|
128
154
|
unsubscribeAudio?.()
|
|
129
155
|
unsubscribeAudio = null
|
|
130
156
|
sfxBusInitialized = false
|
package/src/lib/sfx-player.ts
CHANGED
|
@@ -126,6 +126,22 @@ export const SFX: Record<string, SFXConfig> = {
|
|
|
126
126
|
turnDone: { src: '/audios/sfx/turn_done.mp3' },
|
|
127
127
|
turnFailed: { src: '/audios/sfx/turn_failed.mp3' },
|
|
128
128
|
turnNeedsInput: { src: '/audios/sfx/turn_needs_input.mp3' },
|
|
129
|
+
// Operating the building from a viewer's callout or a walkthrough: recorded
|
|
130
|
+
// door and shutter movements; the switches and the viewer's hover, select and
|
|
131
|
+
// deselect ticks are the marketing house's synth cues (`scripts/synth-viewer-cues.ts`),
|
|
132
|
+
// already mixed against the doors, so they play at full volume without jitter.
|
|
133
|
+
doorOpen: { src: '/audios/sfx/door_open.mp3', rateRange: [0.96, 1.04], volumeRange: [0.9, 1] },
|
|
134
|
+
doorClose: { src: '/audios/sfx/door_close.mp3', rateRange: [0.96, 1.04], volumeRange: [0.9, 1] },
|
|
135
|
+
// Air moving as a leaf swings shut, ahead of the latch (`scripts/synth-viewer-cues.ts`).
|
|
136
|
+
doorSwing: { src: '/audios/sfx/door_swing.mp3', rateRange: [0.97, 1.03] },
|
|
137
|
+
garageDoor: { src: '/audios/sfx/garage_door.mp3', volumeRange: [0.9, 1] },
|
|
138
|
+
shuttersOpen: { src: '/audios/sfx/shutters_open.mp3', rateRange: [0.96, 1.04] },
|
|
139
|
+
shuttersClose: { src: '/audios/sfx/shutters_close.mp3', rateRange: [0.96, 1.04] },
|
|
140
|
+
switchOn: { src: '/audios/sfx/switch_on.mp3' },
|
|
141
|
+
switchOff: { src: '/audios/sfx/switch_off.mp3' },
|
|
142
|
+
viewerHover: { src: '/audios/sfx/viewer_hover.mp3', minIntervalMs: 95 },
|
|
143
|
+
viewerSelect: { src: '/audios/sfx/viewer_select.mp3', minIntervalMs: 90 },
|
|
144
|
+
viewerDeselect: { src: '/audios/sfx/viewer_deselect.mp3' },
|
|
129
145
|
} as const
|
|
130
146
|
|
|
131
147
|
export type SFXName = keyof typeof SFX
|
|
@@ -106,6 +106,8 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
|
|
|
106
106
|
roof: 'structural',
|
|
107
107
|
zone: 'structural',
|
|
108
108
|
block: 'structural',
|
|
109
|
+
door: 'item',
|
|
110
|
+
window: 'item',
|
|
109
111
|
}
|
|
110
112
|
const profileOf = (t: string) => declared[t]
|
|
111
113
|
const profileOfNode = (id: string) =>
|
|
@@ -173,7 +175,7 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
|
|
|
173
175
|
})
|
|
174
176
|
|
|
175
177
|
it('an undeclared kind (no snapProfile) gets no snap context', () => {
|
|
176
|
-
expect(ctx({ kind: 'moving', nodeType: '
|
|
178
|
+
expect(ctx({ kind: 'moving', nodeType: 'unknown_plugin_kind' })).toBeNull()
|
|
177
179
|
expect(ctx({ kind: 'idle' }, 'build', 'shelf')).toBeNull()
|
|
178
180
|
})
|
|
179
181
|
|
|
@@ -205,4 +207,24 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
|
|
|
205
207
|
}),
|
|
206
208
|
).toBe('polygon')
|
|
207
209
|
})
|
|
210
|
+
it('moving scope resolves active snap context when moving a window or door', () => {
|
|
211
|
+
expect(
|
|
212
|
+
snapContextOf({
|
|
213
|
+
scope: { kind: 'moving', nodeType: 'window' },
|
|
214
|
+
mode: 'select',
|
|
215
|
+
tool: null,
|
|
216
|
+
profileOf,
|
|
217
|
+
profileOfNode,
|
|
218
|
+
}),
|
|
219
|
+
).toBe('item')
|
|
220
|
+
expect(
|
|
221
|
+
snapContextOf({
|
|
222
|
+
scope: { kind: 'moving', nodeType: 'door' },
|
|
223
|
+
mode: 'select',
|
|
224
|
+
tool: null,
|
|
225
|
+
profileOf,
|
|
226
|
+
profileOfNode,
|
|
227
|
+
}),
|
|
228
|
+
).toBe('item')
|
|
229
|
+
})
|
|
208
230
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MessageDescriptor } from '@pascal-app/core'
|
|
1
2
|
import type { ContinuationMode } from './continuation'
|
|
2
3
|
import { getWallDrawVariantInfo, wallDrawVariantOf } from './wall-draw-variant'
|
|
3
4
|
|
|
@@ -5,7 +6,10 @@ import { getWallDrawVariantInfo, wallDrawVariantOf } from './wall-draw-variant'
|
|
|
5
6
|
* The cursor bubble's icon for the wall tool: the Rooms variant in hand
|
|
6
7
|
* (Rectangle / Polygon / Walls), the same art as the Build panel tile.
|
|
7
8
|
*/
|
|
8
|
-
export function wallCursorIcon(mode: ContinuationMode): {
|
|
9
|
+
export function wallCursorIcon(mode: ContinuationMode): {
|
|
10
|
+
label: MessageDescriptor
|
|
11
|
+
iconSrc: string
|
|
12
|
+
} {
|
|
9
13
|
const variant = getWallDrawVariantInfo(wallDrawVariantOf(mode))
|
|
10
14
|
return { label: variant.title, iconSrc: variant.iconSrc }
|
|
11
15
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import useEditor from '../store/use-editor'
|
|
2
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
3
|
+
import { openSidebarPanel } from './sidebar-panel'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The single-key tools of the editor, and the keys tooltips print for them.
|
|
7
|
+
*
|
|
8
|
+
* The drafting tools (Door, Window, Stairs, Roof) sit on Shift with their
|
|
9
|
+
* letter, as the bare key was taken: W/A/S/D pan the view in 2D and 3D,
|
|
10
|
+
* R flips or rotates the selection and the armed placement, and E operates the
|
|
11
|
+
* selected door, window or cabinet. Pan stands down for those Shift chords
|
|
12
|
+
* (`claimsKeyboardPan`), and R/E still work bare as before.
|
|
13
|
+
*/
|
|
14
|
+
export type ToolShortcutId =
|
|
15
|
+
| 'select'
|
|
16
|
+
| 'delete'
|
|
17
|
+
| 'wall'
|
|
18
|
+
| 'item'
|
|
19
|
+
| 'zone'
|
|
20
|
+
| 'measurement'
|
|
21
|
+
| 'paint'
|
|
22
|
+
| 'terrain'
|
|
23
|
+
| 'door'
|
|
24
|
+
| 'window'
|
|
25
|
+
| 'stair'
|
|
26
|
+
| 'roof'
|
|
27
|
+
|
|
28
|
+
type ArmedByChord = {
|
|
29
|
+
/** `KeyboardEvent.code`: the physical key, as pan reads it. */
|
|
30
|
+
code: string
|
|
31
|
+
shift: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Keys as `ShortcutToken` names them. */
|
|
35
|
+
export const TOOL_SHORTCUT_KEYS: Record<ToolShortcutId, readonly string[]> = {
|
|
36
|
+
select: ['V'],
|
|
37
|
+
delete: ['X'],
|
|
38
|
+
wall: ['B'],
|
|
39
|
+
item: ['F'],
|
|
40
|
+
zone: ['Z'],
|
|
41
|
+
measurement: ['M'],
|
|
42
|
+
paint: ['P'],
|
|
43
|
+
terrain: ['G'],
|
|
44
|
+
door: ['Shift', 'D'],
|
|
45
|
+
window: ['Shift', 'W'],
|
|
46
|
+
stair: ['Shift', 'S'],
|
|
47
|
+
roof: ['Shift', 'R'],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The tools this module arms itself; the older keys stay in `use-keyboard`. */
|
|
51
|
+
const CHORDS: Partial<Record<ToolShortcutId, ArmedByChord>> = {
|
|
52
|
+
door: { code: 'KeyD', shift: true },
|
|
53
|
+
window: { code: 'KeyW', shift: true },
|
|
54
|
+
stair: { code: 'KeyS', shift: true },
|
|
55
|
+
roof: { code: 'KeyR', shift: true },
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A tool's shortcut as a tooltip prints it: `Shift+D`, `B`. Null for a tool without one. */
|
|
59
|
+
export function toolShortcutLabel(tool: string): string | null {
|
|
60
|
+
const keys = TOOL_SHORTCUT_KEYS[tool as ToolShortcutId]
|
|
61
|
+
return keys ? keys.join('+') : null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A tool's tooltip with its shortcut: `Door (Shift+D)`; the bare label when it has none. */
|
|
65
|
+
export function withToolShortcut(label: string, tool: string | undefined): string {
|
|
66
|
+
const shortcut = tool ? toolShortcutLabel(tool) : null
|
|
67
|
+
return shortcut ? `${label} (${shortcut})` : label
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Gestures a held Shift modifies (free move, free rotate, force place): a Shift
|
|
71
|
+
// chord there is part of the gesture, never a tool switch.
|
|
72
|
+
const SHIFT_GESTURE_SCOPES = new Set([
|
|
73
|
+
'room-divide',
|
|
74
|
+
'placing',
|
|
75
|
+
'moving',
|
|
76
|
+
'handle-drag',
|
|
77
|
+
'mesh-editing',
|
|
78
|
+
'reshaping',
|
|
79
|
+
'box-select',
|
|
80
|
+
'painting',
|
|
81
|
+
'sculpting',
|
|
82
|
+
'pointing',
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The tool a key press arms, or null: the physical key and Shift, with no other
|
|
87
|
+
* modifier and no repeat. A Shift chord is off while a gesture holds Shift, so
|
|
88
|
+
* Shift+WASD keeps panning there.
|
|
89
|
+
*/
|
|
90
|
+
export function toolShortcutForKey(
|
|
91
|
+
event: Pick<KeyboardEvent, 'code' | 'shiftKey' | 'metaKey' | 'ctrlKey' | 'altKey' | 'repeat'>,
|
|
92
|
+
): ToolShortcutId | null {
|
|
93
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.repeat) return null
|
|
94
|
+
const entry = Object.entries(CHORDS).find(
|
|
95
|
+
([, chord]) => chord.code === event.code && chord.shift === event.shiftKey,
|
|
96
|
+
)
|
|
97
|
+
if (!entry) return null
|
|
98
|
+
if (event.shiftKey && SHIFT_GESTURE_SCOPES.has(useInteractionScope.getState().scope.kind))
|
|
99
|
+
return null
|
|
100
|
+
return entry[0] as ToolShortcutId
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Whether WASD pan must leave this key to a tool shortcut (Shift+W/S/D). */
|
|
104
|
+
export function claimsKeyboardPan(
|
|
105
|
+
event: Pick<KeyboardEvent, 'code' | 'shiftKey' | 'metaKey' | 'ctrlKey' | 'altKey' | 'repeat'>,
|
|
106
|
+
): boolean {
|
|
107
|
+
// A held key repeats: once claimed, the repeats stay claimed too. (Fields one by
|
|
108
|
+
// one: a DOM event's are getters a spread would drop.)
|
|
109
|
+
return (
|
|
110
|
+
event.shiftKey &&
|
|
111
|
+
toolShortcutForKey({
|
|
112
|
+
code: event.code,
|
|
113
|
+
shiftKey: event.shiftKey,
|
|
114
|
+
metaKey: event.metaKey,
|
|
115
|
+
ctrlKey: event.ctrlKey,
|
|
116
|
+
altKey: event.altKey,
|
|
117
|
+
repeat: false,
|
|
118
|
+
}) !== null
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Arm the tool `id` names from the keyboard, as its Build panel tile does, with the panel showing. */
|
|
123
|
+
export function armToolShortcut(id: ToolShortcutId): void {
|
|
124
|
+
const editor = useEditor.getState()
|
|
125
|
+
editor.setPhase('building')
|
|
126
|
+
openSidebarPanel(['build'])
|
|
127
|
+
// A raw tool, as a tile click arms it: no preset carried over from earlier.
|
|
128
|
+
editor.setToolDefaults(id, null)
|
|
129
|
+
editor.armToolMode({ mode: 'build', tool: id })
|
|
130
|
+
}
|
|
@@ -38,6 +38,10 @@ import {
|
|
|
38
38
|
viewerSidebarSections,
|
|
39
39
|
} from './viewer-selection'
|
|
40
40
|
|
|
41
|
+
const english = {
|
|
42
|
+
_: (descriptor: { id: string; message?: string }) => descriptor.message ?? descriptor.id,
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
const buildingId = 'building_viewer'
|
|
42
46
|
const levelId = 'level_viewer'
|
|
43
47
|
const upperId = 'level_viewer_upper'
|
|
@@ -251,10 +255,10 @@ describe('viewer rooms by name', () => {
|
|
|
251
255
|
|
|
252
256
|
test('the hover pill names what a click would select', () => {
|
|
253
257
|
const { nodes, kitchen } = scene()
|
|
254
|
-
expect(viewerHoverLabel(sofa.id, nodes)).toBe('Sofa')
|
|
255
|
-
expect(viewerHoverLabel(kitchen, nodes)).toBe('Kitchen')
|
|
256
|
-
expect(viewerHoverLabel('wall_shared', nodes)).toBe('Wall')
|
|
257
|
-
expect(viewerHoverLabel(null, nodes)).toBeNull()
|
|
258
|
+
expect(viewerHoverLabel(sofa.id, nodes, english)).toBe('Sofa')
|
|
259
|
+
expect(viewerHoverLabel(kitchen, nodes, english)).toBe('Kitchen')
|
|
260
|
+
expect(viewerHoverLabel('wall_shared', nodes, english)).toBe('Wall')
|
|
261
|
+
expect(viewerHoverLabel(null, nodes, english)).toBeNull()
|
|
258
262
|
})
|
|
259
263
|
})
|
|
260
264
|
|
|
@@ -262,7 +266,7 @@ describe('viewer breadcrumb', () => {
|
|
|
262
266
|
test('an item reads floor › room › item; the room is shown, not selected', () => {
|
|
263
267
|
const { nodes, kitchen } = scene()
|
|
264
268
|
const selection = { buildingId, levelId, zoneId: null, selectedIds: [sofa.id] }
|
|
265
|
-
const crumbs = viewerBreadcrumb(selection as ViewerSelection, null, nodes, (id) =>
|
|
269
|
+
const crumbs = viewerBreadcrumb(selection as ViewerSelection, null, nodes, english, (id) =>
|
|
266
270
|
id === sofa.id ? kitchen : null,
|
|
267
271
|
)
|
|
268
272
|
expect(crumbs.map((crumb) => [crumb.kind, crumb.label])).toEqual([
|
|
@@ -277,18 +281,20 @@ describe('viewer breadcrumb', () => {
|
|
|
277
281
|
const unit = UnitNode.parse({ id: 'unit_a', name: 'Apartment A', parentId: buildingId })
|
|
278
282
|
const { nodes, living } = scene([unit])
|
|
279
283
|
const room = { buildingId, levelId, zoneId: living, selectedIds: [] } as ViewerSelection
|
|
280
|
-
expect(viewerBreadcrumb(room, null, nodes).map((crumb) => crumb.label)).toEqual([
|
|
284
|
+
expect(viewerBreadcrumb(room, null, nodes, english).map((crumb) => crumb.label)).toEqual([
|
|
281
285
|
'House',
|
|
282
286
|
'Ground floor',
|
|
283
287
|
'Living',
|
|
284
288
|
])
|
|
285
289
|
const floor = { ...room, zoneId: null }
|
|
286
|
-
expect(viewerBreadcrumb(floor, 'unit_a', nodes).map((crumb) => crumb.label)).toEqual([
|
|
290
|
+
expect(viewerBreadcrumb(floor, 'unit_a', nodes, english).map((crumb) => crumb.label)).toEqual([
|
|
287
291
|
'House',
|
|
288
292
|
'Ground floor',
|
|
289
293
|
'Apartment A',
|
|
290
294
|
])
|
|
291
|
-
expect(viewerBreadcrumb(nothing, null, nodes).map((crumb) => crumb.kind)).toEqual([
|
|
295
|
+
expect(viewerBreadcrumb(nothing, null, nodes, english).map((crumb) => crumb.kind)).toEqual([
|
|
296
|
+
'building',
|
|
297
|
+
])
|
|
292
298
|
})
|
|
293
299
|
})
|
|
294
300
|
|
|
@@ -305,25 +311,21 @@ describe('viewer sidebar', () => {
|
|
|
305
311
|
],
|
|
306
312
|
})
|
|
307
313
|
const plain = scene([terrace])
|
|
308
|
-
expect(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
])
|
|
316
|
-
expect(viewerSidebarSections(upperId, plain.nodes)).toEqual([])
|
|
314
|
+
expect(
|
|
315
|
+
viewerSidebarSections(levelId, plain.nodes, english).map((section) => section.kind),
|
|
316
|
+
).toEqual(['rooms', 'zones'])
|
|
317
|
+
expect(
|
|
318
|
+
viewerSidebarSections(levelId, plain.nodes, english)[0]!.rows.map((row) => row.label),
|
|
319
|
+
).toEqual(['Kitchen', 'Living'])
|
|
320
|
+
expect(viewerSidebarSections(upperId, plain.nodes, english)).toEqual([])
|
|
317
321
|
|
|
318
322
|
const unit = UnitNode.parse({ id: 'unit_a', name: 'Apartment A', parentId: buildingId })
|
|
319
323
|
const withUnit = scene([unit, terrace])
|
|
320
324
|
withUnit.nodes.unit_a = { ...unit, members: [withUnit.kitchen as ZoneNode['id']] }
|
|
321
|
-
expect(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
])
|
|
326
|
-
expect(viewerSidebarSections(upperId, withUnit.nodes)).toEqual([])
|
|
325
|
+
expect(
|
|
326
|
+
viewerSidebarSections(levelId, withUnit.nodes, english).map((section) => section.kind),
|
|
327
|
+
).toEqual(['units', 'rooms', 'zones'])
|
|
328
|
+
expect(viewerSidebarSections(upperId, withUnit.nodes, english)).toEqual([])
|
|
327
329
|
})
|
|
328
330
|
})
|
|
329
331
|
|
|
@@ -546,7 +548,9 @@ test('baked and parametric picks agree for room faces, mezzanine plates, doors,
|
|
|
546
548
|
const baked = resolveGlbPick([{ object: foreign, point: new Vector3() }], identity)
|
|
547
549
|
expect(resolveViewerClick(nothing, baked, nodes)).toEqual(nothing)
|
|
548
550
|
expect(
|
|
549
|
-
viewerBreadcrumb(nothing, null, nodes).some(
|
|
551
|
+
viewerBreadcrumb(nothing, null, nodes, english).some(
|
|
552
|
+
(crumb) => crumb.label === 'Unpublished name',
|
|
553
|
+
),
|
|
550
554
|
).toBe(false)
|
|
551
555
|
})
|
|
552
556
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type LevelNode,
|
|
12
12
|
type NodeEvent,
|
|
13
13
|
resolveSelectionProxyId,
|
|
14
|
+
roomFloorElevation,
|
|
14
15
|
sceneRegistry,
|
|
15
16
|
type UnitNode,
|
|
16
17
|
type ZoneNode,
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
} from 'three'
|
|
30
31
|
import { useBelowGrade } from './below-grade'
|
|
31
32
|
import { nodeDisplayLabel } from './node-display-label'
|
|
32
|
-
import {
|
|
33
|
+
import { type DescriptorTranslator, displayRoomName } from './room-name-messages'
|
|
33
34
|
import { RoomSelectionIndex, type RoomSelectionRecord, resolveRoomHit } from './room-selection'
|
|
34
35
|
import type { SelectionModifierKeys } from './selection-routing'
|
|
35
36
|
|
|
@@ -455,8 +456,16 @@ export type ViewerCrumb = {
|
|
|
455
456
|
label: string
|
|
456
457
|
}
|
|
457
458
|
|
|
458
|
-
|
|
459
|
-
|
|
459
|
+
/**
|
|
460
|
+
* The viewer chrome's name for a zone, in the editor's UI language (the scene
|
|
461
|
+
* keeps its stored name). `i18n` is the caller's scoped `usePascalI18n()`.
|
|
462
|
+
*/
|
|
463
|
+
export function zoneLabel(zone: ZoneNode, i18n: DescriptorTranslator): string {
|
|
464
|
+
const name = zone.name?.trim()
|
|
465
|
+
if (name) return displayRoomName(name, i18n)
|
|
466
|
+
return zone.spaceRole === 'room'
|
|
467
|
+
? i18n._({ id: 'editor.roomLabel.room', message: 'Room' })
|
|
468
|
+
: i18n._({ id: 'editor.roomLabel.zone', message: 'Zone' })
|
|
460
469
|
}
|
|
461
470
|
|
|
462
471
|
/**
|
|
@@ -468,6 +477,7 @@ export function viewerBreadcrumb(
|
|
|
468
477
|
selection: ViewerSelection,
|
|
469
478
|
focusedUnitId: string | null,
|
|
470
479
|
nodes: ViewerNodes,
|
|
480
|
+
i18n: DescriptorTranslator,
|
|
471
481
|
roomOf: (nodeId: string, levelId: string) => string | null = () => null,
|
|
472
482
|
): ViewerCrumb[] {
|
|
473
483
|
const crumbs: ViewerCrumb[] = []
|
|
@@ -486,7 +496,8 @@ export function viewerBreadcrumb(
|
|
|
486
496
|
if (unit?.type === 'unit') crumbs.push({ kind: 'unit', id: unit.id, label: unit.name })
|
|
487
497
|
const roomId = selection.zoneId ?? (element ? roomOf(element.id, level.id) : null)
|
|
488
498
|
const room = roomId ? nodes[roomId] : undefined
|
|
489
|
-
if (room?.type === 'zone')
|
|
499
|
+
if (room?.type === 'zone')
|
|
500
|
+
crumbs.push({ kind: 'room', id: room.id, label: zoneLabel(room, i18n) })
|
|
490
501
|
if (element) crumbs.push({ kind: 'node', id: element.id, label: nodeDisplayLabel(element) })
|
|
491
502
|
return crumbs
|
|
492
503
|
}
|
|
@@ -531,14 +542,18 @@ export type ViewerSidebarSection = { kind: 'units' | 'rooms' | 'zones'; rows: Vi
|
|
|
531
542
|
* What sits under the current floor: its units, its rooms, its other zones.
|
|
532
543
|
* An empty section is left out.
|
|
533
544
|
*/
|
|
534
|
-
export function viewerSidebarSections(
|
|
545
|
+
export function viewerSidebarSections(
|
|
546
|
+
levelId: string,
|
|
547
|
+
nodes: ViewerNodes,
|
|
548
|
+
i18n: DescriptorTranslator,
|
|
549
|
+
): ViewerSidebarSection[] {
|
|
535
550
|
const zones = Object.values(nodes).filter(
|
|
536
551
|
(node): node is ZoneNode =>
|
|
537
552
|
node.type === 'zone' && node.parentId === levelId && node.visible !== false,
|
|
538
553
|
)
|
|
539
554
|
const zoneRow = (zone: ZoneNode): ViewerSidebarRow => ({
|
|
540
555
|
id: zone.id,
|
|
541
|
-
label: zoneLabel(zone),
|
|
556
|
+
label: zoneLabel(zone, i18n),
|
|
542
557
|
color: zone.color,
|
|
543
558
|
areaM2: area([{ outer: zone.polygon, holes: zone.holes ?? [] }]),
|
|
544
559
|
})
|