@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
package/src/lib/hud-title.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type IconRef,
|
|
3
|
+
type MessageDescriptor,
|
|
4
|
+
nodeRegistry,
|
|
5
|
+
type TerrainVerb,
|
|
6
|
+
} from '@pascal-app/core'
|
|
7
|
+
import { nodeLabelDescriptor } from '../i18n/node-messages'
|
|
2
8
|
import type { ContinuationMode } from './continuation'
|
|
3
9
|
import { getWallDrawVariantInfo, wallDrawVariantOf } from './wall-draw-variant'
|
|
4
10
|
|
|
5
11
|
/** The contextual helper panel's header: what is in hand, and its key. */
|
|
6
12
|
export type HudTitle = {
|
|
7
|
-
|
|
13
|
+
/** May use `{kind}`, filled with the translated, lower-cased name of `kind`. */
|
|
14
|
+
label: MessageDescriptor
|
|
15
|
+
/** The node kind whose display name the label refers to. */
|
|
16
|
+
kind?: { kind: string; english: string }
|
|
8
17
|
icon?: IconRef
|
|
9
18
|
shortcut?: string
|
|
10
19
|
}
|
|
@@ -18,54 +27,63 @@ const TOOL_SHORTCUTS: Record<string, string> = {
|
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
export const SELECT_HUD_TITLE: HudTitle = {
|
|
21
|
-
label: 'Select',
|
|
30
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.select', message: 'Select' },
|
|
22
31
|
icon: url('/icons/select.webp'),
|
|
23
32
|
shortcut: 'V',
|
|
24
33
|
}
|
|
25
|
-
export const PAINT_HUD_TITLE: HudTitle = {
|
|
34
|
+
export const PAINT_HUD_TITLE: HudTitle = {
|
|
35
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.paint', message: 'Paint' },
|
|
36
|
+
icon: url('/icons/paint.webp'),
|
|
37
|
+
}
|
|
26
38
|
export const ERASE_HUD_TITLE: HudTitle = {
|
|
27
|
-
label: 'Erase',
|
|
39
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.erase', message: 'Erase' },
|
|
28
40
|
icon: { kind: 'iconify', name: 'lucide:eraser' },
|
|
29
41
|
}
|
|
30
42
|
export const PICK_MATERIAL_HUD_TITLE: HudTitle = {
|
|
31
|
-
label: 'Pick material',
|
|
43
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.pickMaterial', message: 'Pick material' },
|
|
32
44
|
icon: { kind: 'iconify', name: 'lucide:pipette' },
|
|
33
45
|
}
|
|
34
|
-
export const ROTATE_HUD_TITLE: HudTitle = {
|
|
46
|
+
export const ROTATE_HUD_TITLE: HudTitle = {
|
|
47
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.rotate', message: 'Rotate' },
|
|
48
|
+
icon: url('/icons/rotate.webp'),
|
|
49
|
+
}
|
|
35
50
|
export const MOVE_SELECTION_HUD_TITLE: HudTitle = {
|
|
36
|
-
label: 'Move selection',
|
|
51
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.moveSelection', message: 'Move selection' },
|
|
37
52
|
icon: url('/icons/pan.webp'),
|
|
38
53
|
}
|
|
39
|
-
export const MOVE_ROOM_HUD_TITLE: HudTitle = {
|
|
54
|
+
export const MOVE_ROOM_HUD_TITLE: HudTitle = {
|
|
55
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.moveRoom', message: 'Move room' },
|
|
56
|
+
icon: url('/icons/room.webp'),
|
|
57
|
+
}
|
|
40
58
|
export const RESIZE_HUD_TITLE: HudTitle = {
|
|
41
|
-
label: 'Resize',
|
|
59
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.resize', message: 'Resize' },
|
|
42
60
|
icon: url('/icons/resize.webp'),
|
|
43
61
|
}
|
|
44
62
|
export const DIVIDE_ROOM_HUD_TITLE: HudTitle = {
|
|
45
|
-
label: 'Divide room',
|
|
63
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.divideRoom', message: 'Divide room' },
|
|
46
64
|
icon: url('/icons/divide-room.webp'),
|
|
47
65
|
}
|
|
48
66
|
export const MEZZANINE_HUD_TITLE: HudTitle = {
|
|
49
|
-
label: 'Mezzanine',
|
|
67
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.mezzanine', message: 'Mezzanine' },
|
|
50
68
|
icon: url('/icons/mezzanine.webp'),
|
|
51
69
|
}
|
|
52
70
|
export const OPENING_HUD_TITLE: HudTitle = {
|
|
53
|
-
label: 'Cut opening',
|
|
71
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.cutOpening', message: 'Cut opening' },
|
|
54
72
|
icon: { kind: 'iconify', name: 'lucide:square-dashed' },
|
|
55
73
|
}
|
|
56
74
|
export const TERRACE_HUD_TITLE: HudTitle = {
|
|
57
|
-
label: 'Terrace',
|
|
75
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.terrace', message: 'Terrace' },
|
|
58
76
|
icon: url('/icons/floor.webp'),
|
|
59
77
|
}
|
|
60
78
|
export const SMART_MEASURE_HUD_TITLE: HudTitle = {
|
|
61
|
-
label: 'Smart measure',
|
|
79
|
+
label: /* i18n */ { id: 'editor.lib.hudTitle.smartMeasure', message: 'Smart measure' },
|
|
62
80
|
icon: url('/icons/measure.webp'),
|
|
63
81
|
shortcut: 'M',
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
const PAINT_REGION_LABELS = {
|
|
67
|
-
rectangle: 'Paint a rectangle',
|
|
68
|
-
polygon: 'Paint a shape',
|
|
85
|
+
rectangle: /* i18n */ { id: 'editor.lib.hudTitle.paintARectangle', message: 'Paint a rectangle' },
|
|
86
|
+
polygon: /* i18n */ { id: 'editor.lib.hudTitle.paintAShape', message: 'Paint a shape' },
|
|
69
87
|
} as const
|
|
70
88
|
|
|
71
89
|
export function paintRegionHudTitle(mode: keyof typeof PAINT_REGION_LABELS): HudTitle {
|
|
@@ -75,22 +93,28 @@ export function paintRegionHudTitle(mode: keyof typeof PAINT_REGION_LABELS): Hud
|
|
|
75
93
|
export function terrainHudTitle(verb: TerrainVerb): HudTitle {
|
|
76
94
|
const label =
|
|
77
95
|
verb === 'raise'
|
|
78
|
-
? 'Raise terrain'
|
|
96
|
+
? /* i18n */ { id: 'editor.lib.hudTitle.raiseTerrain', message: 'Raise terrain' }
|
|
79
97
|
: verb === 'lower'
|
|
80
|
-
? 'Lower terrain'
|
|
98
|
+
? /* i18n */ { id: 'editor.lib.hudTitle.lowerTerrain', message: 'Lower terrain' }
|
|
81
99
|
: verb === 'flatten'
|
|
82
|
-
? 'Level terrain'
|
|
83
|
-
: 'Smooth terrain'
|
|
100
|
+
? /* i18n */ { id: 'editor.lib.hudTitle.levelTerrain', message: 'Level terrain' }
|
|
101
|
+
: /* i18n */ { id: 'editor.lib.hudTitle.smoothTerrain', message: 'Smooth terrain' }
|
|
84
102
|
return { label, icon: url(`/icons/terrain-${verb}.webp`) }
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
const KIND_VERBS = {
|
|
106
|
+
edit: /* i18n */ { id: 'editor.lib.hudTitle.editKind', message: 'Edit {kind}' },
|
|
107
|
+
move: /* i18n */ { id: 'editor.lib.hudTitle.moveKind', message: 'Move {kind}' },
|
|
108
|
+
split: /* i18n */ { id: 'editor.lib.hudTitle.splitKind', message: 'Split {kind}' },
|
|
109
|
+
} as const
|
|
110
|
+
|
|
87
111
|
/** A kind's registry name and icon, optionally behind a verb ("Move door"). */
|
|
88
|
-
export function nodeKindHudTitle(kind: string, verb?:
|
|
112
|
+
export function nodeKindHudTitle(kind: string, verb?: keyof typeof KIND_VERBS): HudTitle | null {
|
|
89
113
|
const presentation = nodeRegistry.get(kind)?.presentation
|
|
90
114
|
if (!presentation) return null
|
|
91
|
-
const name = presentation.label
|
|
92
115
|
return {
|
|
93
|
-
label: verb ?
|
|
116
|
+
label: verb ? KIND_VERBS[verb] : nodeLabelDescriptor(kind, presentation.label),
|
|
117
|
+
kind: verb ? { kind, english: presentation.label } : undefined,
|
|
94
118
|
icon: presentation.icon,
|
|
95
119
|
shortcut: verb ? undefined : TOOL_SHORTCUTS[kind],
|
|
96
120
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { type AnyNode, sceneRegistry } from '@pascal-app/core'
|
|
2
|
+
import { type AnyNode, DoorNode, LevelNode, sceneRegistry, WallNode } from '@pascal-app/core'
|
|
3
3
|
import * as THREE from 'three'
|
|
4
4
|
import { collectIfcMeshes, exportPreparedSceneToIfc, ifcFileName } from './ifc-export'
|
|
5
5
|
|
|
@@ -172,3 +172,43 @@ test('material groups retain their surface owner and semantic subpart', () => {
|
|
|
172
172
|
geometry.dispose()
|
|
173
173
|
material.dispose()
|
|
174
174
|
})
|
|
175
|
+
|
|
176
|
+
for (const rotationY of [0, Math.PI]) {
|
|
177
|
+
test(`IFC opening includes fixed lights and preserves leaf placement facing ${rotationY}`, () => {
|
|
178
|
+
const level = LevelNode.parse({ id: 'level_entry' })
|
|
179
|
+
const wall = WallNode.parse({
|
|
180
|
+
id: 'wall_entry',
|
|
181
|
+
parentId: level.id,
|
|
182
|
+
start: [0, 0],
|
|
183
|
+
end: [5, 0],
|
|
184
|
+
height: 3,
|
|
185
|
+
thickness: 0.2,
|
|
186
|
+
})
|
|
187
|
+
const door = DoorNode.parse({
|
|
188
|
+
id: 'door_entry',
|
|
189
|
+
parentId: wall.id,
|
|
190
|
+
wallId: wall.id,
|
|
191
|
+
position: [2.5, 1.05, 0],
|
|
192
|
+
rotation: [0, rotationY, 0],
|
|
193
|
+
width: 1,
|
|
194
|
+
height: 2.1,
|
|
195
|
+
sidelightLeft: 0.3,
|
|
196
|
+
sidelightRight: 0.5,
|
|
197
|
+
transomHeight: 0.4,
|
|
198
|
+
})
|
|
199
|
+
const { data } = exportPreparedSceneToIfc(
|
|
200
|
+
new THREE.Group(),
|
|
201
|
+
{ [level.id]: level, [wall.id]: wall, [door.id]: door },
|
|
202
|
+
{},
|
|
203
|
+
)
|
|
204
|
+
expect(data).toMatch(/IFCRECTANGLEPROFILEDEF\([^\n]*,1\.8,0\.4\)/)
|
|
205
|
+
expect(data).toMatch(/IFCEXTRUDEDAREASOLID\([^\n]*,2\.5\)/)
|
|
206
|
+
expect(data).toMatch(/IFCDOOR\([^\n]*,2\.1,1\.,\.DOOR\./)
|
|
207
|
+
expect(data).toContain(
|
|
208
|
+
rotationY === 0 ? 'IFCCARTESIANPOINT((2.6,0.,0.))' : 'IFCCARTESIANPOINT((2.4,0.,0.))',
|
|
209
|
+
)
|
|
210
|
+
expect(data).toContain(
|
|
211
|
+
rotationY === 0 ? 'IFCCARTESIANPOINT((-0.1,0.,0.))' : 'IFCCARTESIANPOINT((0.1,0.,0.))',
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
type InspectorCardMode,
|
|
4
|
+
openExtension,
|
|
4
5
|
resolveActiveExtension,
|
|
5
6
|
toggleCard,
|
|
6
|
-
toggleExtension,
|
|
7
7
|
} from './inspector-card-mode'
|
|
8
8
|
|
|
9
9
|
const collapsed: InspectorCardMode = { collapsed: true, activeExtensionId: null }
|
|
@@ -29,27 +29,6 @@ describe('toggleCard (chevron / header press)', () => {
|
|
|
29
29
|
})
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
describe('toggleExtension (header icon press)', () => {
|
|
33
|
-
test('folded card opens straight into extension-only mode', () => {
|
|
34
|
-
expect(toggleExtension(collapsed, 'bones:eng')).toEqual(engineering)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('regular expanded card swaps to extension-only mode', () => {
|
|
38
|
-
expect(toggleExtension(regular, 'bones:eng')).toEqual(engineering)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('pressing the ACTIVE extension icon again returns to regular mode', () => {
|
|
42
|
-
expect(toggleExtension(engineering, 'bones:eng')).toEqual(regular)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test('pressing another extension icon switches extension modes directly', () => {
|
|
46
|
-
expect(toggleExtension(engineering, 'other:ext')).toEqual({
|
|
47
|
-
collapsed: false,
|
|
48
|
-
activeExtensionId: 'other:ext',
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
32
|
describe('resolveActiveExtension', () => {
|
|
54
33
|
const bones = { id: 'bones:eng' }
|
|
55
34
|
const other = { id: 'other:ext' }
|
|
@@ -71,20 +50,13 @@ describe('resolveActiveExtension', () => {
|
|
|
71
50
|
})
|
|
72
51
|
|
|
73
52
|
describe('full user flows (QA script)', () => {
|
|
74
|
-
test('chevron → regular only; fold;
|
|
53
|
+
test('chevron → regular only; fold; extension action → extension only; chevron → regular', () => {
|
|
75
54
|
let mode = collapsed
|
|
76
55
|
mode = toggleCard(mode) // chevron
|
|
77
56
|
expect(mode).toEqual(regular)
|
|
78
57
|
mode = toggleCard(mode) // fold
|
|
79
58
|
expect(mode).toEqual(collapsed)
|
|
80
|
-
mode =
|
|
81
|
-
expect(mode).toEqual(engineering)
|
|
82
|
-
mode = toggleExtension(mode, 'bones:eng') // bones icon again
|
|
83
|
-
expect(mode).toEqual(regular)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('extension mode exits via the chevron too', () => {
|
|
87
|
-
let mode = toggleExtension(collapsed, 'bones:eng')
|
|
59
|
+
mode = openExtension('bones:eng') // the action-menu button
|
|
88
60
|
expect(mode).toEqual(engineering)
|
|
89
61
|
mode = toggleCard(mode) // chevron
|
|
90
62
|
expect(mode).toEqual(regular)
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* - chevron / header press ({@link toggleCard}): collapsed → regular,
|
|
15
15
|
* regular → collapsed, extension → regular (exit extension mode first,
|
|
16
16
|
* stay expanded);
|
|
17
|
-
* - extension
|
|
18
|
-
* extension's mode from anywhere (expanding a folded card);
|
|
19
|
-
*
|
|
17
|
+
* - an extension's action-menu button ({@link openExtension}): enters that
|
|
18
|
+
* extension's mode from anywhere (expanding a folded card); pressed while
|
|
19
|
+
* that extension is already showing, it keeps it showing.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
export interface InspectorCardMode {
|
|
@@ -36,13 +36,8 @@ export function toggleCard(mode: InspectorCardMode): InspectorCardMode {
|
|
|
36
36
|
return { collapsed: true, activeExtensionId: null }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
export function
|
|
41
|
-
// The active extension's icon pressed again → back to the regular controls.
|
|
42
|
-
if (!mode.collapsed && mode.activeExtensionId === extensionId) {
|
|
43
|
-
return { collapsed: false, activeExtensionId: null }
|
|
44
|
-
}
|
|
45
|
-
// Anywhere else (folded, regular, another extension) → this extension only.
|
|
39
|
+
/** An extension's action-menu button: opens the card on that extension, or keeps it there. */
|
|
40
|
+
export function openExtension(extensionId: string): InspectorCardMode {
|
|
46
41
|
return { collapsed: false, activeExtensionId: extensionId }
|
|
47
42
|
}
|
|
48
43
|
|
|
@@ -42,3 +42,16 @@ test('without usable storage the panel opens folded and expanding still works',
|
|
|
42
42
|
useInspectorExpanded.getState().setExpanded(true)
|
|
43
43
|
expect(useInspectorExpanded.getState().expanded).toBe(true)
|
|
44
44
|
})
|
|
45
|
+
|
|
46
|
+
test('an extension opened from the action menu expands the card; folding it forgets the extension', async () => {
|
|
47
|
+
stubStorage({ getItem: () => null, setItem: () => {} })
|
|
48
|
+
const { useInspectorExpanded } = await load()
|
|
49
|
+
useInspectorExpanded.getState().applyMode({ collapsed: false, activeExtensionId: 'bones:eng' })
|
|
50
|
+
expect(useInspectorExpanded.getState()).toMatchObject({
|
|
51
|
+
expanded: true,
|
|
52
|
+
activeExtensionId: 'bones:eng',
|
|
53
|
+
})
|
|
54
|
+
useInspectorExpanded.getState().setExpanded(false)
|
|
55
|
+
useInspectorExpanded.getState().setExpanded(true)
|
|
56
|
+
expect(useInspectorExpanded.getState().activeExtensionId).toBeNull()
|
|
57
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { create } from 'zustand'
|
|
2
|
+
import type { InspectorCardMode } from './inspector-card-mode'
|
|
2
3
|
|
|
3
4
|
// One choice for every right-side inspector panel (room, wall, item…): opened
|
|
4
5
|
// expanded or folded to its header, kept per viewer until they fold it again.
|
|
@@ -26,11 +27,22 @@ function writeExpanded(expanded: boolean) {
|
|
|
26
27
|
|
|
27
28
|
export const useInspectorExpanded = create<{
|
|
28
29
|
expanded: boolean
|
|
30
|
+
/**
|
|
31
|
+
* The plugin inspector extension filling the expanded card (see `inspector-card-mode.ts`),
|
|
32
|
+
* shared so the action menu can open it. Not remembered: folding the card forgets it.
|
|
33
|
+
*/
|
|
34
|
+
activeExtensionId: string | null
|
|
29
35
|
setExpanded: (expanded: boolean) => void
|
|
36
|
+
applyMode: (mode: InspectorCardMode) => void
|
|
30
37
|
}>((set) => ({
|
|
31
38
|
expanded: readExpanded(),
|
|
39
|
+
activeExtensionId: null,
|
|
32
40
|
setExpanded: (expanded) => {
|
|
33
41
|
writeExpanded(expanded)
|
|
34
|
-
set({ expanded })
|
|
42
|
+
set(expanded ? { expanded } : { expanded, activeExtensionId: null })
|
|
43
|
+
},
|
|
44
|
+
applyMode: ({ collapsed, activeExtensionId }) => {
|
|
45
|
+
writeExpanded(!collapsed)
|
|
46
|
+
set({ expanded: !collapsed, activeExtensionId: collapsed ? null : activeExtensionId })
|
|
35
47
|
},
|
|
36
48
|
}))
|
|
@@ -111,6 +111,13 @@ test('modifier chords stay shortcuts, and typing in a field never pans', () => {
|
|
|
111
111
|
expect(acceptsKeyboardPan(key({ target: new Input() }))).toBe(false)
|
|
112
112
|
})
|
|
113
113
|
|
|
114
|
+
test('Shift with W, S or D arms a tool instead of panning; Shift with A still pans', () => {
|
|
115
|
+
for (const code of ['KeyW', 'KeyS', 'KeyD'])
|
|
116
|
+
expect(acceptsKeyboardPan(key({ code, shiftKey: true }))).toBe(false)
|
|
117
|
+
expect(acceptsKeyboardPan(key({ code: 'KeyA', shiftKey: true }))).toBe(true)
|
|
118
|
+
expect(acceptsKeyboardPan(key({ code: 'KeyD' }))).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
114
121
|
test('cruise speed scales with the visible width within fixed bounds', () => {
|
|
115
122
|
expect(keyboardPanSpeed(0)).toBe(0.5)
|
|
116
123
|
expect(keyboardPanSpeed(20)).toBeCloseTo(6)
|
package/src/lib/keyboard-pan.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// move the same way. Keys match by physical position (`event.code`): the
|
|
3
3
|
// cluster stays under the left hand on any layout (Z/Q/S/D on AZERTY).
|
|
4
4
|
|
|
5
|
+
import { claimsKeyboardPan } from './tool-shortcuts'
|
|
6
|
+
|
|
5
7
|
export type KeyboardPanState = {
|
|
6
8
|
forward: boolean
|
|
7
9
|
backward: boolean
|
|
@@ -79,10 +81,15 @@ export function clearKeyboardPanKeys(state: KeyboardPanState) {
|
|
|
79
81
|
state.right = false
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Pan keys are ignored with a modifier held (shortcuts), for a Shift chord that
|
|
86
|
+
* arms a tool (Shift+W window, Shift+S stairs, Shift+D door), and while typing.
|
|
87
|
+
*/
|
|
83
88
|
export function acceptsKeyboardPan(event: KeyboardEvent) {
|
|
84
89
|
return (
|
|
85
|
-
!(event.metaKey || event.ctrlKey || event.altKey) &&
|
|
90
|
+
!(event.metaKey || event.ctrlKey || event.altKey) &&
|
|
91
|
+
!claimsKeyboardPan(event) &&
|
|
92
|
+
!isEditableKeyboardTarget(event.target)
|
|
86
93
|
)
|
|
87
94
|
}
|
|
88
95
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { ROOF_BUILD_UP_PRESETS, roofBuildUpDepths } from '@pascal-app/core'
|
|
3
|
+
import { formatLayerDepth, formatLayerThickness } from './layer-thickness'
|
|
4
|
+
|
|
5
|
+
const IN = 0.0254
|
|
6
|
+
|
|
7
|
+
// The roof layers read DECK 10-3/16" beside COVERING 0.54": a covering of shingle and underlayment
|
|
8
|
+
// is no exact 32nd, so it fell to decimals. Summed depths read as fractions, to the nearest 1/16".
|
|
9
|
+
test('summed depths read as fractions to the 1/16", like the deck beside them', () => {
|
|
10
|
+
expect(formatLayerDepth(0.54 * IN, 'imperial')).toBe('9/16"')
|
|
11
|
+
for (const preset of ROOF_BUILD_UP_PRESETS) {
|
|
12
|
+
const { deck, covering } = roofBuildUpDepths(preset.buildUp)
|
|
13
|
+
for (const depth of [deck, covering])
|
|
14
|
+
expect(formatLayerDepth(depth, 'imperial')).toMatch(/^(\d+|\d+\/\d+|\d+-\d+\/\d+)"$/)
|
|
15
|
+
}
|
|
16
|
+
expect(formatLayerDepth(0.0137, 'metric')).toBe('13.7 mm')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('a typed thickness still reads back as typed', () => {
|
|
20
|
+
expect(formatLayerThickness((7 / 16) * IN, 'imperial')).toBe('7/16"')
|
|
21
|
+
expect(formatLayerThickness(0.0111, 'imperial')).toBe('0.437"')
|
|
22
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { LinearUnit } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
const METRES_PER_INCH = 0.0254
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Imperial-friendly thickness input. Accepts `5/8`, `5/8"`, `7/16 in`,
|
|
7
|
+
* `1-1/4"`, `1 1/2"`, `12mm`, `0.012m`. A bare number means INCHES in imperial
|
|
8
|
+
* display and MILLIMETRES in metric — layers are millimetre-scale and
|
|
9
|
+
* typing `0.0127` for half-inch board is nobody's idea of usable.
|
|
10
|
+
*/
|
|
11
|
+
export function parseLayerThickness(raw: string, unit: LinearUnit): number | null {
|
|
12
|
+
const text = raw.trim().toLowerCase().replace(/["”]/g, ' in ')
|
|
13
|
+
if (!text) return null
|
|
14
|
+
const explicit = /(mm|cm|m|in|ft|')\s*$/.exec(text)
|
|
15
|
+
const suffix = explicit?.[1]
|
|
16
|
+
const body = (suffix ? text.slice(0, explicit?.index) : text).trim()
|
|
17
|
+
|
|
18
|
+
// `1-1/4` / `1 1/2` / `5/8` / `0.4375`
|
|
19
|
+
const mixed = /^(\d+(?:\.\d+)?)[\s-]+(\d+)\s*\/\s*(\d+)$/.exec(body)
|
|
20
|
+
const fraction = /^(\d+)\s*\/\s*(\d+)$/.exec(body)
|
|
21
|
+
let value: number
|
|
22
|
+
if (mixed) {
|
|
23
|
+
value = Number(mixed[1]) + Number(mixed[2]) / Number(mixed[3])
|
|
24
|
+
} else if (fraction) {
|
|
25
|
+
value = Number(fraction[1]) / Number(fraction[2])
|
|
26
|
+
} else {
|
|
27
|
+
value = Number(body)
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isFinite(value) || value < 0) return null
|
|
30
|
+
|
|
31
|
+
switch (suffix) {
|
|
32
|
+
case 'mm':
|
|
33
|
+
return value / 1000
|
|
34
|
+
case 'cm':
|
|
35
|
+
return value / 100
|
|
36
|
+
case 'm':
|
|
37
|
+
return value
|
|
38
|
+
case 'in':
|
|
39
|
+
return value * METRES_PER_INCH
|
|
40
|
+
case 'ft':
|
|
41
|
+
case "'":
|
|
42
|
+
return value * 12 * METRES_PER_INCH
|
|
43
|
+
default:
|
|
44
|
+
return unit === 'imperial' ? value * METRES_PER_INCH : value / 1000
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* An exact 32nd of an inch as its fraction, so 7/16 in (0.0111125 m) reads back as `7/16"`; any
|
|
50
|
+
* other value as decimal inches (0.0111 m is `0.437"`).
|
|
51
|
+
*/
|
|
52
|
+
export function formatLayerThickness(metres: number, unit: LinearUnit): string {
|
|
53
|
+
if (unit !== 'imperial') return `${Math.round(metres * 1000 * 10) / 10} mm`
|
|
54
|
+
const inchValue = metres / METRES_PER_INCH
|
|
55
|
+
const parts = Math.round(inchValue * 32)
|
|
56
|
+
if (Math.abs(inchValue * 32 - parts) > 1e-6) return `${Math.round(inchValue * 1000) / 1000}"`
|
|
57
|
+
const whole = Math.floor(parts / 32)
|
|
58
|
+
let numerator = parts % 32
|
|
59
|
+
let denominator = 32
|
|
60
|
+
while (numerator % 2 === 0 && numerator > 0) {
|
|
61
|
+
numerator /= 2
|
|
62
|
+
denominator /= 2
|
|
63
|
+
}
|
|
64
|
+
if (numerator === 0) return `${whole}"`
|
|
65
|
+
return whole > 0 ? `${whole}-${numerator}/${denominator}"` : `${numerator}/${denominator}"`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A summed depth to read, not type back (a plane's deck or covering, a ceiling's thickness): in
|
|
70
|
+
* imperial to the nearest 1/16" as a fraction, as the lumber and panels it adds up read, so a
|
|
71
|
+
* covering of 0.54" reads 9/16" beside a 10-3/16" deck.
|
|
72
|
+
*/
|
|
73
|
+
export function formatLayerDepth(metres: number, unit: LinearUnit): string {
|
|
74
|
+
if (unit !== 'imperial') return formatLayerThickness(metres, unit)
|
|
75
|
+
return formatLayerThickness(
|
|
76
|
+
(Math.round((metres / METRES_PER_INCH) * 16) / 16) * METRES_PER_INCH,
|
|
77
|
+
unit,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type AnyNode,
|
|
4
4
|
type AnyNodeId,
|
|
5
5
|
BuildingNode,
|
|
6
|
+
ElevatorNode,
|
|
6
7
|
LevelNode,
|
|
7
8
|
RoofNode,
|
|
8
9
|
useScene,
|
|
@@ -27,10 +28,14 @@ beforeEach(() => {
|
|
|
27
28
|
metadata: { role: 'roof' },
|
|
28
29
|
})
|
|
29
30
|
const roof = RoofNode.parse({ id: 'roof_main', parentId: roofLevel.id })
|
|
30
|
-
const
|
|
31
|
+
const elevator = ElevatorNode.parse({ id: 'elevator_lift', parentId: BUILDING })
|
|
32
|
+
const building = BuildingNode.parse({
|
|
33
|
+
id: BUILDING,
|
|
34
|
+
children: [GROUND, roofLevel.id, elevator.id],
|
|
35
|
+
})
|
|
31
36
|
useScene.setState({
|
|
32
37
|
nodes: Object.fromEntries(
|
|
33
|
-
[building, ground, wall, roofLevel, roof].map((node) => [node.id, node as AnyNode]),
|
|
38
|
+
[building, ground, wall, roofLevel, roof, elevator].map((node) => [node.id, node as AnyNode]),
|
|
34
39
|
),
|
|
35
40
|
rootNodeIds: [BUILDING],
|
|
36
41
|
} as never)
|
|
@@ -54,4 +59,15 @@ describe('selectPlacedNode', () => {
|
|
|
54
59
|
selectedIds: ['wall_a'],
|
|
55
60
|
})
|
|
56
61
|
})
|
|
62
|
+
|
|
63
|
+
test('a building-parented node keeps the floor in view and adopts its building', () => {
|
|
64
|
+
useViewer.getState().resetSelection()
|
|
65
|
+
useViewer.getState().setSelection({ levelId: GROUND })
|
|
66
|
+
selectPlacedNode('elevator_lift' as AnyNodeId)
|
|
67
|
+
expect(useViewer.getState().selection).toMatchObject({
|
|
68
|
+
buildingId: BUILDING,
|
|
69
|
+
levelId: GROUND,
|
|
70
|
+
selectedIds: ['elevator_lift'],
|
|
71
|
+
})
|
|
72
|
+
})
|
|
57
73
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
|
|
1
|
+
import type { AnyNode, AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
|
|
2
2
|
import { findLevelAncestorId, useScene } from '@pascal-app/core'
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
4
|
|
|
@@ -30,17 +30,40 @@ export function deleteLevelWithFallbackSelection(levelId: AnyNodeId) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function findBuildingAncestorId(
|
|
34
|
+
nodeId: AnyNodeId,
|
|
35
|
+
nodes: Record<string, AnyNode>,
|
|
36
|
+
): BuildingNode['id'] | null {
|
|
37
|
+
let current: AnyNode | undefined = nodes[nodeId]
|
|
38
|
+
for (let guard = 0; current && guard < 16; guard++) {
|
|
39
|
+
if (current.type === 'building') return current.id
|
|
40
|
+
current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/**
|
|
34
46
|
* Selects a node a tool just made, on its own level when that is not the one in view: a roof drawn
|
|
35
47
|
* over a storey's walls lands on the level above, which the editor hides while the storey is
|
|
36
|
-
* selected, so selecting it there alone would leave it out of sight.
|
|
48
|
+
* selected, so selecting it there alone would leave it out of sight. A node parented to the
|
|
49
|
+
* building itself (an elevator) has no level, so the floor in view stays active when it belongs
|
|
50
|
+
* to that building.
|
|
37
51
|
*/
|
|
38
52
|
export function selectPlacedNode(nodeId: AnyNodeId) {
|
|
39
|
-
const
|
|
53
|
+
const { nodes } = useScene.getState()
|
|
54
|
+
const levelId = findLevelAncestorId(nodeId, nodes) as LevelNode['id'] | null
|
|
55
|
+
const buildingId = findBuildingAncestorId(nodeId, nodes)
|
|
40
56
|
const { selection, setSelection } = useViewer.getState()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
const updates: Partial<typeof selection> = { selectedIds: [nodeId] }
|
|
58
|
+
if (buildingId && buildingId !== selection.buildingId) {
|
|
59
|
+
updates.buildingId = buildingId
|
|
60
|
+
updates.levelId =
|
|
61
|
+
levelId ??
|
|
62
|
+
(selection.levelId && findBuildingAncestorId(selection.levelId, nodes) === buildingId
|
|
63
|
+
? selection.levelId
|
|
64
|
+
: null)
|
|
65
|
+
} else if (levelId && levelId !== selection.levelId) {
|
|
66
|
+
updates.levelId = levelId
|
|
67
|
+
}
|
|
68
|
+
setSelection(updates)
|
|
46
69
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createZoneDivisionContext,
|
|
7
7
|
generateId,
|
|
8
8
|
nodeRegistry,
|
|
9
|
+
roomFloorElevation,
|
|
9
10
|
type SlabNode,
|
|
10
11
|
useLiveNodeOverrides,
|
|
11
12
|
useScene,
|
|
@@ -29,7 +30,6 @@ import { roomRelativeFloorHeight, stepRoomFloorElevation } from './room-construc
|
|
|
29
30
|
import {
|
|
30
31
|
commitRoomElevation,
|
|
31
32
|
mezzanineEdgeHandles,
|
|
32
|
-
roomFloorElevation,
|
|
33
33
|
roomPushHandles,
|
|
34
34
|
runFloorplanMezzanineEdge,
|
|
35
35
|
runFloorplanRoomPush,
|