@pascal-app/editor 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -15
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +110 -42
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +77 -68
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +31 -11
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +458 -140
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +23 -1
- package/src/components/editor/index.tsx +150 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +2 -3
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +4 -3
- package/src/components/editor/room-handles-2d.tsx +158 -7
- package/src/components/editor/room-handles.tsx +244 -54
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +33 -17
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +15 -7
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +47 -19
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
- package/src/components/ui/helpers/helper-manager.tsx +136 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
- package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +11 -10
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +9 -5
- package/src/components/ui/panels/panel-wrapper.tsx +63 -127
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +465 -66
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +22 -13
- package/src/components/ui/panels/room-panel.tsx +207 -100
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +62 -5
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15849 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +39331 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +39331 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +39331 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +39331 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +224 -45
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +101 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -2
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +15 -6
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +24 -6
- package/src/lib/measurements.ts +1 -203
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +28 -17
- package/src/lib/room-built-on.ts +21 -17
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +141 -1
- package/src/lib/room-handle-drag.ts +554 -58
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +38 -1
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +11 -5
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +21 -6
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +17 -6
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +44 -0
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +53 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-snap-indicator.ts +15 -2
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -100
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
emitter,
|
|
6
|
+
formatFixed,
|
|
7
|
+
type GridEvent,
|
|
8
|
+
sceneRegistry,
|
|
9
|
+
usePascalI18n,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
4
12
|
import {
|
|
5
13
|
CursorSphere,
|
|
6
14
|
clearPlacementSurface,
|
|
@@ -11,17 +19,19 @@ import {
|
|
|
11
19
|
isGridSnapActive,
|
|
12
20
|
isMagneticSnapActive,
|
|
13
21
|
isTypingTarget,
|
|
22
|
+
isViewVisible,
|
|
14
23
|
markToolCancelConsumed,
|
|
15
24
|
publishPlacementSurface,
|
|
16
25
|
triggerSFX,
|
|
17
26
|
useEditor,
|
|
18
27
|
useInteractionScope,
|
|
19
28
|
} from '@pascal-app/editor'
|
|
20
|
-
import { Html } from '@react-three/drei'
|
|
21
29
|
import { useThree } from '@react-three/fiber'
|
|
22
30
|
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react'
|
|
23
31
|
import { createPortal } from 'react-dom'
|
|
24
32
|
import { type Group, Vector3 } from 'three'
|
|
33
|
+
import { messageWithSlot } from '../../../i18n/message-slot'
|
|
34
|
+
import { Html } from '../../editor/localized-html'
|
|
25
35
|
import type { RunSurfaceBounds, RunSurfaceTarget } from './distribution-run-contract'
|
|
26
36
|
import { clearDrawAlignment } from './draw-alignment'
|
|
27
37
|
import type { RunBodyHit, ScenePort } from './ports'
|
|
@@ -218,6 +228,11 @@ export type DistributionRunToolConfig = {
|
|
|
218
228
|
active: boolean
|
|
219
229
|
levelId: AnyNodeId | null
|
|
220
230
|
toolName: string
|
|
231
|
+
routingMode?: 'surface' | 'horizontal' | 'vertical'
|
|
232
|
+
/** Level-local centreline elevation, or the target height for a vertical leg. */
|
|
233
|
+
elevation?: number
|
|
234
|
+
targetElevation?: number
|
|
235
|
+
connectCrossings?: boolean
|
|
221
236
|
initialStart?: RunPoint | null
|
|
222
237
|
initialConnection?: RunConnection | null
|
|
223
238
|
getPorts: () => ScenePort[]
|
|
@@ -516,6 +531,7 @@ function surfacePointFromEvent(
|
|
|
516
531
|
|
|
517
532
|
export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
518
533
|
const { camera, gl } = useThree()
|
|
534
|
+
const planVisible = useEditor((state) => isViewVisible(state, '2d'))
|
|
519
535
|
const initialStartRef = useRef<RunPoint | null>(
|
|
520
536
|
config.initialStart ? [...config.initialStart] : null,
|
|
521
537
|
)
|
|
@@ -526,6 +542,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
526
542
|
const [cursor, setCursor] = useState<RunPoint | null>(initialStartRef.current)
|
|
527
543
|
const [snapTarget, setSnapTarget] = useState<RunPoint | null>(null)
|
|
528
544
|
const [snapScreen, setSnapScreen] = useState<PortScreenPoint | null>(null)
|
|
545
|
+
const [cursorScreen, setCursorScreen] = useState<PortScreenPoint | null>(null)
|
|
529
546
|
const [endConnection, setEndConnection] = useState<RunConnection>({
|
|
530
547
|
port: null,
|
|
531
548
|
body: null,
|
|
@@ -584,6 +601,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
584
601
|
const typed = Number.parseFloat(lengthInputRef.current)
|
|
585
602
|
if (
|
|
586
603
|
!currentStart ||
|
|
604
|
+
configRef.current.routingMode === 'vertical' ||
|
|
587
605
|
!Number.isFinite(typed) ||
|
|
588
606
|
typed <= 0 ||
|
|
589
607
|
resolved.port ||
|
|
@@ -609,28 +627,47 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
609
627
|
}
|
|
610
628
|
}
|
|
611
629
|
|
|
630
|
+
// A vertical route rises from its start to the target elevation wherever the
|
|
631
|
+
// pointer is, so it resolves without a pointer event.
|
|
632
|
+
const verticalTarget = (currentStart: RunPoint): ResolvedRunPoint => ({
|
|
633
|
+
point: [
|
|
634
|
+
currentStart[0],
|
|
635
|
+
configRef.current.targetElevation ?? currentStart[1],
|
|
636
|
+
currentStart[2],
|
|
637
|
+
],
|
|
638
|
+
frame: createRunSurfaceFrame(currentStart),
|
|
639
|
+
surfaceTarget: null,
|
|
640
|
+
snapped: null,
|
|
641
|
+
directionMode: 'vertical',
|
|
642
|
+
port: null,
|
|
643
|
+
body: null,
|
|
644
|
+
})
|
|
645
|
+
|
|
612
646
|
const resolvePoint = (event: GridEvent): ResolvedRunPoint => {
|
|
613
647
|
const adapter = configRef.current
|
|
614
648
|
const hit = surfacePointFromEvent(event, adapter.levelId)
|
|
615
649
|
const currentStart = startRef.current
|
|
616
650
|
const previous = lastResolvedRef.current
|
|
617
|
-
const
|
|
651
|
+
const explicitRoute = adapter.routingMode && adapter.routingMode !== 'surface'
|
|
652
|
+
const lockedWallTarget = currentStart && !explicitRoute ? lockedWallTargetRef.current : null
|
|
618
653
|
const lockedWallHit =
|
|
619
654
|
lockedWallTarget && isSameRunWallSurface(lockedWallTarget, event.surfaceHit)
|
|
620
|
-
const working =
|
|
621
|
-
?
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
:
|
|
633
|
-
|
|
655
|
+
const working = explicitRoute
|
|
656
|
+
? createRunSurfaceFrame([0, currentStart?.[1] ?? adapter.elevation ?? 0, 0])
|
|
657
|
+
: lockedWallTarget
|
|
658
|
+
? {
|
|
659
|
+
...lockedWallTarget.frame,
|
|
660
|
+
origin: lockedWallTarget.frame.origin.map(
|
|
661
|
+
(value, index) =>
|
|
662
|
+
value +
|
|
663
|
+
lockedWallTarget.frame.normal[index]! *
|
|
664
|
+
(adapter.surfaceClearance?.(lockedWallTarget) ?? 0),
|
|
665
|
+
) as RunPoint,
|
|
666
|
+
}
|
|
667
|
+
: !event.surfaceHit && previous?.surfaceTarget?.kind === 'wall' && currentStart
|
|
668
|
+
? createRunSurfaceFrame(currentStart, UP)
|
|
669
|
+
: (previous?.frame ?? (currentStart ? createRunSurfaceFrame(currentStart) : null))
|
|
670
|
+
const hasSurface = !explicitRoute && (!!event.surfaceHit || !working || !event.localRay)
|
|
634
671
|
const target = lockedWallTarget ?? (hasSurface ? hit.target : null)
|
|
635
672
|
const hitForCursor = lockedWallTarget
|
|
636
673
|
? lockedWallHit
|
|
@@ -643,16 +680,25 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
643
680
|
hit: hitForCursor,
|
|
644
681
|
working,
|
|
645
682
|
ray: event.localRay,
|
|
646
|
-
fallback: previous?.point ?? currentStart ?? hit.point,
|
|
683
|
+
fallback: explicitRoute ? [...hit.point] : (previous?.point ?? currentStart ?? hit.point),
|
|
647
684
|
clearance: adapter.surfaceClearance?.(target) ?? 0,
|
|
648
685
|
})
|
|
686
|
+
const routeElevation = currentStart?.[1] ?? adapter.elevation ?? 0
|
|
687
|
+
if (explicitRoute) {
|
|
688
|
+
resolved.point[1] = routeElevation
|
|
689
|
+
if (currentStart && adapter.routingMode === 'vertical') return verticalTarget(currentStart)
|
|
690
|
+
}
|
|
649
691
|
const bypass = event.nativeEvent?.altKey === true
|
|
650
692
|
const gridStep = !bypass && isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
|
|
651
693
|
const angleLocked = !bypass && (isAngleSnapActive() || isGridSnapActive())
|
|
652
694
|
let point = currentStart
|
|
653
695
|
? resolved.point
|
|
654
696
|
: snapRunPointToSurface(resolved.point, resolved.frame, gridStep)
|
|
655
|
-
const
|
|
697
|
+
const selectedDirection = forcedDirectionRef.current
|
|
698
|
+
const forcedDirection =
|
|
699
|
+
explicitRoute && selectedDirection && Math.abs(selectedDirection[1]) > 1e-6
|
|
700
|
+
? null
|
|
701
|
+
: selectedDirection
|
|
656
702
|
if (currentStart && forcedDirection) {
|
|
657
703
|
point = projectRunToDirection(currentStart, point, forcedDirection)
|
|
658
704
|
}
|
|
@@ -665,7 +711,11 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
665
711
|
if (currentStart && gridStep > 0) {
|
|
666
712
|
point = snapRunLength(currentStart, point, gridStep)
|
|
667
713
|
}
|
|
668
|
-
const
|
|
714
|
+
const drawingTarget: RunSurfaceTarget | null =
|
|
715
|
+
explicitRoute && adapter.levelId
|
|
716
|
+
? { kind: 'surface', levelId: adapter.levelId, frame: resolved.frame }
|
|
717
|
+
: target
|
|
718
|
+
const sample = { frame: resolved.frame, surfaceTarget: drawingTarget }
|
|
669
719
|
const native = (event.nativeEvent ?? {}) as {
|
|
670
720
|
clientX?: number
|
|
671
721
|
clientY?: number
|
|
@@ -705,11 +755,22 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
705
755
|
const screen = projectConnection(candidate)
|
|
706
756
|
return !!screen && Math.hypot(screen.x - pointer[0], screen.y - pointer[1]) <= 12
|
|
707
757
|
}
|
|
758
|
+
// An explicit route may only connect at the height its free end reaches above that
|
|
759
|
+
// plan point, which includes the adapter's fall.
|
|
760
|
+
const routeHeightAt = (candidate: readonly number[]): number => {
|
|
761
|
+
const level: RunPoint = [candidate[0]!, routeElevation, candidate[2]!]
|
|
762
|
+
return currentStart
|
|
763
|
+
? (adapter.resolveFreeEnd?.(currentStart, level, startConnectionRef.current) ?? level)[1]
|
|
764
|
+
: routeElevation
|
|
765
|
+
}
|
|
766
|
+
const onRoute = (candidate: readonly number[]) =>
|
|
767
|
+
!explicitRoute || Math.abs(candidate[1]! - routeHeightAt(candidate)) < 1e-4
|
|
708
768
|
const resolveConnection = (candidate: RunPoint): ResolvedRunPoint | null => {
|
|
709
769
|
if (bypass || !(isGridSnapActive() || isMagneticSnapActive() || isAngleSnapActive()))
|
|
710
770
|
return null
|
|
711
771
|
const source = startConnectionRef.current.port
|
|
712
|
-
const
|
|
772
|
+
const ports = adapter.getPorts().filter((port) => onRoute(port.position))
|
|
773
|
+
const hit = findScreenPort(ports, pointer, projectConnection, source)
|
|
713
774
|
if (hit) {
|
|
714
775
|
return {
|
|
715
776
|
frame: createRunSurfaceFrame([...hit.port.position]),
|
|
@@ -722,8 +783,19 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
722
783
|
body: null,
|
|
723
784
|
}
|
|
724
785
|
}
|
|
725
|
-
const body =
|
|
726
|
-
|
|
786
|
+
const body =
|
|
787
|
+
adapter.connectCrossings === false
|
|
788
|
+
? null
|
|
789
|
+
: adapter.findBody(
|
|
790
|
+
explicitRoute ? [candidate[0], routeHeightAt(candidate), candidate[2]] : candidate,
|
|
791
|
+
drawingTarget,
|
|
792
|
+
)
|
|
793
|
+
if (
|
|
794
|
+
body &&
|
|
795
|
+
onRoute(body.point) &&
|
|
796
|
+
acceptsConnection(body.point) &&
|
|
797
|
+
body.nodeId !== source?.nodeId
|
|
798
|
+
) {
|
|
727
799
|
return {
|
|
728
800
|
...sample,
|
|
729
801
|
point: body.point,
|
|
@@ -739,7 +811,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
739
811
|
if (connection) return connection
|
|
740
812
|
// Rank displayed directions before a surface projection discards height.
|
|
741
813
|
if (currentStart && event.localRay && !bypass && (forcedDirection || angleLocked)) {
|
|
742
|
-
const source = startConnectionRef.current.port?.direction ?? null
|
|
814
|
+
const source = explicitRoute ? null : (startConnectionRef.current.port?.direction ?? null)
|
|
743
815
|
const candidates = forcedDirection
|
|
744
816
|
? [forcedDirection]
|
|
745
817
|
: source
|
|
@@ -795,7 +867,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
795
867
|
forcedDirection ||
|
|
796
868
|
(Math.abs(directionHit.direction[1]) > 1e-6 && target?.kind === 'floor')
|
|
797
869
|
? null
|
|
798
|
-
:
|
|
870
|
+
: drawingTarget,
|
|
799
871
|
snapped: null,
|
|
800
872
|
directionMode: 'angle',
|
|
801
873
|
port: null,
|
|
@@ -943,7 +1015,12 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
943
1015
|
setAltActive(false)
|
|
944
1016
|
}
|
|
945
1017
|
|
|
1018
|
+
const updateCursorScreen = (event: GridEvent) => {
|
|
1019
|
+
const pointer = getGridEventScreenProjection(event)?.pointer
|
|
1020
|
+
setCursorScreen(pointer ? { x: pointer[0], y: pointer[1], depth: 0 } : null)
|
|
1021
|
+
}
|
|
946
1022
|
const onMove = (event: RunPointerEvent) => {
|
|
1023
|
+
updateCursorScreen(event)
|
|
947
1024
|
lastPointerRef.current = event
|
|
948
1025
|
const clientY = (event.nativeEvent as { clientY?: number } | undefined)?.clientY
|
|
949
1026
|
if (typeof clientY === 'number') lastClientYRef.current = clientY
|
|
@@ -965,6 +1042,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
965
1042
|
}
|
|
966
1043
|
|
|
967
1044
|
const onClick = (event: GridEvent) => {
|
|
1045
|
+
updateCursorScreen(event)
|
|
968
1046
|
event.nativeEvent?.stopPropagation?.()
|
|
969
1047
|
const currentStart = startRef.current
|
|
970
1048
|
if (altAnchorRef.current && currentStart) {
|
|
@@ -985,8 +1063,10 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
985
1063
|
port: resolved.port,
|
|
986
1064
|
body: resolved.port ? null : resolved.body,
|
|
987
1065
|
}
|
|
988
|
-
if (
|
|
989
|
-
|
|
1066
|
+
if (!configRef.current.routingMode || configRef.current.routingMode === 'surface') {
|
|
1067
|
+
if (shouldLockRunWallSurface(resolved.surfaceTarget, connection)) {
|
|
1068
|
+
lockedWallTargetRef.current = resolved.surfaceTarget
|
|
1069
|
+
}
|
|
990
1070
|
}
|
|
991
1071
|
triggerSFX('sfx:grid-snap')
|
|
992
1072
|
startConnectionRef.current = connection
|
|
@@ -997,6 +1077,10 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
997
1077
|
setSnapTarget(resolved.snapped)
|
|
998
1078
|
setSnapScreen(resolved.snapScreen ?? null)
|
|
999
1079
|
setEndConnection({ port: null, body: null })
|
|
1080
|
+
// A vertical route ends at the target, not under the pointer: show that
|
|
1081
|
+
// riser now so the next click or Enter places what the preview shows.
|
|
1082
|
+
if (configRef.current.routingMode === 'vertical')
|
|
1083
|
+
updateCursor(verticalTarget(resolved.point))
|
|
1000
1084
|
return
|
|
1001
1085
|
}
|
|
1002
1086
|
const typedResolved = applyTypedLength(resolved)
|
|
@@ -1028,6 +1112,11 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
1028
1112
|
setAltActive(true)
|
|
1029
1113
|
return
|
|
1030
1114
|
}
|
|
1115
|
+
if (
|
|
1116
|
+
configRef.current.routingMode === 'vertical' &&
|
|
1117
|
+
(event.key === 'Tab' || /^[0-9.,]$/.test(event.key))
|
|
1118
|
+
)
|
|
1119
|
+
return
|
|
1031
1120
|
if (
|
|
1032
1121
|
event.key === 'Tab' &&
|
|
1033
1122
|
startRef.current &&
|
|
@@ -1037,7 +1126,10 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
1037
1126
|
) {
|
|
1038
1127
|
event.preventDefault()
|
|
1039
1128
|
event.stopImmediatePropagation()
|
|
1040
|
-
const source =
|
|
1129
|
+
const source =
|
|
1130
|
+
configRef.current.routingMode === 'horizontal'
|
|
1131
|
+
? null
|
|
1132
|
+
: startConnectionRef.current.port?.direction
|
|
1041
1133
|
const candidates = source
|
|
1042
1134
|
? run3DDirectionCandidates(source)
|
|
1043
1135
|
: runHorizontalDirectionCandidates(null)
|
|
@@ -1109,7 +1201,10 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
1109
1201
|
}
|
|
1110
1202
|
|
|
1111
1203
|
refreshCursorRef.current = () => {
|
|
1112
|
-
|
|
1204
|
+
const currentStart = startRef.current
|
|
1205
|
+
if (currentStart && configRef.current.routingMode === 'vertical')
|
|
1206
|
+
updateCursor(verticalTarget(currentStart))
|
|
1207
|
+
else if (lastPointerRef.current)
|
|
1113
1208
|
updateCursor(applyTypedLength(resolvePoint(lastPointerRef.current)))
|
|
1114
1209
|
else if (lastResolvedRef.current) updateCursor(applyTypedLength(lastResolvedRef.current))
|
|
1115
1210
|
}
|
|
@@ -1143,18 +1238,39 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
1143
1238
|
}
|
|
1144
1239
|
}, [camera, gl, config.active, updateLengthInput])
|
|
1145
1240
|
|
|
1241
|
+
// The resolver reads config through a ref; changing these controls must refresh a stationary cursor.
|
|
1242
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: configuration changes invalidate the cursor
|
|
1243
|
+
useEffect(() => {
|
|
1244
|
+
forcedDirectionRef.current = null
|
|
1245
|
+
if (config.routingMode !== 'surface') lockedWallTargetRef.current = null
|
|
1246
|
+
refreshCursor()
|
|
1247
|
+
}, [
|
|
1248
|
+
config.routingMode,
|
|
1249
|
+
config.elevation,
|
|
1250
|
+
config.targetElevation,
|
|
1251
|
+
config.connectCrossings,
|
|
1252
|
+
refreshCursor,
|
|
1253
|
+
])
|
|
1254
|
+
|
|
1146
1255
|
return {
|
|
1147
1256
|
refreshCursor,
|
|
1257
|
+
routingMode: config.routingMode,
|
|
1148
1258
|
start,
|
|
1149
1259
|
cursor,
|
|
1150
1260
|
snapTarget,
|
|
1151
1261
|
snapScreen,
|
|
1262
|
+
cursorScreen: planVisible ? cursorScreen : null,
|
|
1152
1263
|
altActive,
|
|
1153
1264
|
directionMode,
|
|
1154
1265
|
lengthInput,
|
|
1155
1266
|
validationMessage,
|
|
1156
1267
|
onLengthInputChange: updateLengthInput,
|
|
1157
1268
|
onDirectionSelect: (direction: RunPoint) => {
|
|
1269
|
+
if (
|
|
1270
|
+
configRef.current.routingMode === 'vertical' ||
|
|
1271
|
+
(configRef.current.routingMode === 'horizontal' && Math.abs(direction[1]) > 1e-6)
|
|
1272
|
+
)
|
|
1273
|
+
return
|
|
1158
1274
|
forcedDirectionRef.current = direction
|
|
1159
1275
|
setDirectionMode('angle')
|
|
1160
1276
|
refreshCursorRef.current()
|
|
@@ -1175,6 +1291,7 @@ export function DistributionRunCursor({
|
|
|
1175
1291
|
extraParts = [],
|
|
1176
1292
|
cursorRef,
|
|
1177
1293
|
directionMode,
|
|
1294
|
+
routingMode,
|
|
1178
1295
|
startDirection,
|
|
1179
1296
|
lengthInput,
|
|
1180
1297
|
validationMessage,
|
|
@@ -1190,6 +1307,7 @@ export function DistributionRunCursor({
|
|
|
1190
1307
|
extraParts?: DimensionPillPart[]
|
|
1191
1308
|
cursorRef?: RefObject<Group | null>
|
|
1192
1309
|
directionMode: RunDirectionMode
|
|
1310
|
+
routingMode?: 'surface' | 'horizontal' | 'vertical'
|
|
1193
1311
|
startDirection?: readonly [number, number, number] | null
|
|
1194
1312
|
lengthInput?: string
|
|
1195
1313
|
validationMessage?: string | null
|
|
@@ -1197,10 +1315,11 @@ export function DistributionRunCursor({
|
|
|
1197
1315
|
onLengthInputChange?: (value: string) => void
|
|
1198
1316
|
onDirectionSelect?: (direction: RunPoint) => void
|
|
1199
1317
|
}) {
|
|
1318
|
+
const i18n = usePascalI18n()
|
|
1200
1319
|
const lengthFieldRef = useRef<HTMLInputElement>(null)
|
|
1201
1320
|
useEffect(() => {
|
|
1202
|
-
if (start) lengthFieldRef.current?.focus()
|
|
1203
|
-
}, [start])
|
|
1321
|
+
if (start && routingMode !== 'vertical') lengthFieldRef.current?.focus()
|
|
1322
|
+
}, [start, routingMode])
|
|
1204
1323
|
if (!cursor) return null
|
|
1205
1324
|
const parts: DimensionPillPart[] = [
|
|
1206
1325
|
...(start
|
|
@@ -1261,9 +1380,9 @@ export function DistributionRunCursor({
|
|
|
1261
1380
|
cursor={cursor}
|
|
1262
1381
|
mode={directionMode}
|
|
1263
1382
|
snapped={!!snapTarget}
|
|
1264
|
-
sourceDirection={startDirection ?? null}
|
|
1383
|
+
sourceDirection={routingMode === 'horizontal' ? null : (startDirection ?? null)}
|
|
1265
1384
|
start={start}
|
|
1266
|
-
onDirectionSelect={onDirectionSelect}
|
|
1385
|
+
onDirectionSelect={routingMode === 'vertical' ? undefined : onDirectionSelect}
|
|
1267
1386
|
/>
|
|
1268
1387
|
)}
|
|
1269
1388
|
<CursorSphere
|
|
@@ -1282,32 +1401,55 @@ export function DistributionRunCursor({
|
|
|
1282
1401
|
>
|
|
1283
1402
|
<div className="flex flex-col items-center gap-1">
|
|
1284
1403
|
<DimensionPill parts={parts} primary={primary} unit={unit} />
|
|
1285
|
-
{start ? (
|
|
1404
|
+
{start && routingMode === 'vertical' ? (
|
|
1405
|
+
<div className="rounded-full border border-border/60 bg-background/90 px-3 py-1 text-xs shadow-sm">
|
|
1406
|
+
{i18n._({
|
|
1407
|
+
id: 'editor.tools.distribution.distributionRunTool.targetElevation',
|
|
1408
|
+
message: 'Target elevation: {elevation} m · click or Enter to place',
|
|
1409
|
+
values: { elevation: formatFixed(cursor[1], 2, i18n.getLocale()) },
|
|
1410
|
+
})}
|
|
1411
|
+
</div>
|
|
1412
|
+
) : start ? (
|
|
1286
1413
|
<label className="rounded-full border border-border/60 bg-background/90 px-3 py-1 text-[11px] tabular-nums text-muted-foreground shadow-sm backdrop-blur">
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1414
|
+
{messageWithSlot(
|
|
1415
|
+
i18n,
|
|
1416
|
+
/* i18n */ {
|
|
1417
|
+
id: 'editor.tools.distribution.distributionRunTool.lengthField',
|
|
1418
|
+
message: 'Length: {field} m',
|
|
1419
|
+
},
|
|
1420
|
+
'field',
|
|
1421
|
+
<input
|
|
1422
|
+
className="w-20 bg-transparent text-center text-foreground outline-none"
|
|
1423
|
+
data-run-length-input
|
|
1424
|
+
inputMode="decimal"
|
|
1425
|
+
onKeyDown={(event) => {
|
|
1426
|
+
if (
|
|
1427
|
+
event.key === 'Backspace' ||
|
|
1428
|
+
event.key === 'Delete' ||
|
|
1429
|
+
event.key === 'ArrowLeft' ||
|
|
1430
|
+
event.key === 'ArrowRight'
|
|
1431
|
+
) {
|
|
1432
|
+
event.stopPropagation()
|
|
1433
|
+
}
|
|
1434
|
+
}}
|
|
1435
|
+
onChange={(event) => onLengthInputChange?.(event.target.value)}
|
|
1436
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
1437
|
+
placeholder={i18n._({
|
|
1438
|
+
id: 'editor.tools.distribution.distributionRunTool.typeALength',
|
|
1439
|
+
message: 'type a length',
|
|
1440
|
+
})}
|
|
1441
|
+
ref={lengthFieldRef}
|
|
1442
|
+
style={{ pointerEvents: 'auto' }}
|
|
1443
|
+
type="text"
|
|
1444
|
+
value={lengthInput ?? ''}
|
|
1445
|
+
/>,
|
|
1446
|
+
)}
|
|
1447
|
+
<span className="ml-2">
|
|
1448
|
+
{i18n._({
|
|
1449
|
+
id: 'editor.tools.distribution.distributionRunTool.tabDirection',
|
|
1450
|
+
message: 'Tab: direction',
|
|
1451
|
+
})}
|
|
1452
|
+
</span>
|
|
1311
1453
|
</label>
|
|
1312
1454
|
) : null}
|
|
1313
1455
|
{validationMessage ? (
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type { AnyNodeId,
|
|
1
|
+
import type { AnyNodeId, SpawnNode } from '@pascal-app/core'
|
|
2
2
|
import { createSceneApi, nodeRegistry, useScene } from '@pascal-app/core'
|
|
3
3
|
import { Suspense, useMemo } from 'react'
|
|
4
4
|
import { useMovingNode } from '../../../store/use-interaction-scope'
|
|
5
|
-
import { MoveElevatorTool } from '../elevator/move-elevator-tool'
|
|
6
5
|
import { MoveRegistryNodeTool } from '../registry/move-registry-node-tool'
|
|
7
6
|
import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
|
|
8
7
|
|
|
@@ -20,8 +19,6 @@ import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
|
|
|
20
19
|
* 2. `MoveRegistryNodeTool` — generic translate-on-XZ for kinds that only
|
|
21
20
|
* declare `capabilities.movable` (shelf, spawn, duct-terminal,
|
|
22
21
|
* hvac-equipment, …).
|
|
23
|
-
* 3. `elevator` is the lone remaining legacy arm — its bespoke cab/shaft
|
|
24
|
-
* mover hasn't been ported to a kind-owned affordance yet.
|
|
25
22
|
*/
|
|
26
23
|
export const MoveTool: React.FC<{
|
|
27
24
|
onNodeMoved?: (nodeId: AnyNodeId) => void
|
|
@@ -47,7 +44,5 @@ export const MoveTool: React.FC<{
|
|
|
47
44
|
return <MoveRegistryNodeTool node={movingNode} />
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
if (movingNode.type === 'elevator')
|
|
51
|
-
return <MoveElevatorTool node={movingNode as ElevatorNode} onCommitted={onNodeMoved} />
|
|
52
47
|
return null
|
|
53
48
|
}
|
|
@@ -22,12 +22,12 @@ import {
|
|
|
22
22
|
WallNode,
|
|
23
23
|
} from '@pascal-app/core'
|
|
24
24
|
import { useViewer } from '@pascal-app/viewer'
|
|
25
|
-
import { Html } from '@react-three/drei'
|
|
26
25
|
import { act, create } from '@react-three/test-renderer'
|
|
27
26
|
import { Children, Component, cloneElement, isValidElement, type ReactNode } from 'react'
|
|
28
27
|
import { Group } from 'three'
|
|
29
28
|
import useEditor from '../../../store/use-editor'
|
|
30
29
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
30
|
+
import { Html } from '../../editor/localized-html'
|
|
31
31
|
import { useDraftNode } from './use-draft-node'
|
|
32
32
|
import { usePlacementCoordinator } from './use-placement-coordinator'
|
|
33
33
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
BlockNode,
|
|
4
|
+
BuildingNode,
|
|
4
5
|
type CeilingEvent,
|
|
5
6
|
type CeilingNode,
|
|
6
7
|
type GridEvent,
|
|
@@ -11,7 +12,11 @@ import {
|
|
|
11
12
|
type WallEvent,
|
|
12
13
|
type WallNode,
|
|
13
14
|
} from '@pascal-app/core'
|
|
15
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
14
16
|
import { BufferGeometry, Mesh, MeshBasicMaterial, type Object3D, Vector3 } from 'three'
|
|
17
|
+
import { GROUP_MOVE_DRAG_LABEL } from '../../../lib/contextual-help'
|
|
18
|
+
import useEditor from '../../../store/use-editor'
|
|
19
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
15
20
|
import {
|
|
16
21
|
ceilingStrategy,
|
|
17
22
|
faceHostStrategy,
|
|
@@ -498,6 +503,64 @@ describe('floorStrategy.move', () => {
|
|
|
498
503
|
expect(result.cursorPosition[1]).toBe(0)
|
|
499
504
|
expect(result.nodeUpdate?.position?.[1]).toBe(0)
|
|
500
505
|
})
|
|
506
|
+
|
|
507
|
+
describe('in grid mode', () => {
|
|
508
|
+
const originalSelection = useViewer.getState().selection
|
|
509
|
+
|
|
510
|
+
beforeEach(() => {
|
|
511
|
+
// The group-move scope resolves to the item snap context without the
|
|
512
|
+
// node registry; grid mode at a 0.5 m step.
|
|
513
|
+
useInteractionScope
|
|
514
|
+
.getState()
|
|
515
|
+
.begin({ kind: 'handle-drag', nodeId: 'item_test', handle: GROUP_MOVE_DRAG_LABEL })
|
|
516
|
+
useEditor.getState().setSnappingMode('item', 'grid')
|
|
517
|
+
useEditor.getState().setGridSnapStep(0.5)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
afterEach(() => {
|
|
521
|
+
useInteractionScope.getState().end()
|
|
522
|
+
useEditor.getState().setSnappingMode('item', 'lines')
|
|
523
|
+
useViewer.setState({ selection: originalSelection } as never)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
// A 0.5 m plant centres half a cell off the lattice, its edges on it.
|
|
527
|
+
test("inside a turned building it lands on the building's lattice, square to the house", () => {
|
|
528
|
+
const yaw = (17 * Math.PI) / 180
|
|
529
|
+
const building = BuildingNode.parse({ position: [12.37, 0, -8.91], rotation: [0, yaw, 0] })
|
|
530
|
+
useScene.setState((state) => ({
|
|
531
|
+
...state,
|
|
532
|
+
nodes: { ...state.nodes, [building.id]: building },
|
|
533
|
+
}))
|
|
534
|
+
useViewer.setState({
|
|
535
|
+
selection: { buildingId: building.id, levelId: null, zoneId: null, selectedIds: [] },
|
|
536
|
+
} as never)
|
|
537
|
+
const local: [number, number] = [1.37, 2.61]
|
|
538
|
+
const cos = Math.cos(yaw)
|
|
539
|
+
const sin = Math.sin(yaw)
|
|
540
|
+
const event: GridEvent = {
|
|
541
|
+
position: [
|
|
542
|
+
12.37 + local[0] * cos + local[1] * sin,
|
|
543
|
+
0,
|
|
544
|
+
-8.91 - local[0] * sin + local[1] * cos,
|
|
545
|
+
],
|
|
546
|
+
localPosition: [local[0], 0, local[1]],
|
|
547
|
+
nativeEvent: {} as GridEvent['nativeEvent'],
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const result = floorStrategy.move(floorItemContext(), event)
|
|
551
|
+
expect(result?.nodeUpdate?.position?.[0]).toBeCloseTo(1.25, 9)
|
|
552
|
+
expect(result?.nodeUpdate?.position?.[2]).toBeCloseTo(2.75, 9)
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
test('outside any building it keeps the world grid', () => {
|
|
556
|
+
useViewer.setState({
|
|
557
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
558
|
+
} as never)
|
|
559
|
+
const result = floorStrategy.move(floorItemContext(), makeGridEvent(5.37, 0, -3.12))
|
|
560
|
+
expect(result?.nodeUpdate?.position?.[0]).toBeCloseTo(5.25, 9)
|
|
561
|
+
expect(result?.nodeUpdate?.position?.[2]).toBeCloseTo(-3.25, 9)
|
|
562
|
+
})
|
|
563
|
+
})
|
|
501
564
|
})
|
|
502
565
|
|
|
503
566
|
describe('ceilingStrategy.move', () => {
|
|
@@ -27,13 +27,12 @@ import {
|
|
|
27
27
|
resolveSurfacePlacement,
|
|
28
28
|
roofFacePointToSegment,
|
|
29
29
|
sceneRegistry,
|
|
30
|
-
snapLocalXZInWorld,
|
|
31
30
|
useScene,
|
|
32
31
|
wouldCreateHostingCycle,
|
|
33
32
|
} from '@pascal-app/core'
|
|
34
33
|
import { Euler, Quaternion, Vector3 } from 'three'
|
|
35
34
|
import { hasRoofFaceChildOverlap, resolveRoofWallHit } from '../../../lib/roof-wall-hit'
|
|
36
|
-
import {
|
|
35
|
+
import { snapPlanPointToActiveGrid } from '../../../lib/world-grid-snap'
|
|
37
36
|
import { itemEventToSurfaceHit, surfaceWorldNormalY } from '../shared/surface-hit'
|
|
38
37
|
import {
|
|
39
38
|
calculateItemRotation,
|
|
@@ -73,17 +72,14 @@ export const floorStrategy = {
|
|
|
73
72
|
const [dimX, , dimZ] = dims
|
|
74
73
|
const rotY = ctx.draftItem?.rotation?.[1] ?? 0
|
|
75
74
|
const swapDims = Math.abs(Math.sin(rotY)) > 0.9
|
|
76
|
-
// Snap on the
|
|
77
|
-
// item edges
|
|
78
|
-
//
|
|
79
|
-
// for storage. Without this, a rotated building drags placement off
|
|
80
|
-
// the world grid.
|
|
75
|
+
// Snap on the lattice the grid is drawn on: a turned building's own
|
|
76
|
+
// lattice inside it (item edges square to the house), the world grid
|
|
77
|
+
// outside any building.
|
|
81
78
|
// Snapping is governed by the active mode (snapToGrid returns raw in Off /
|
|
82
79
|
// non-grid modes); Alt is force-place only and never bypasses snapping here.
|
|
83
80
|
const dimensions = [swapDims ? dimZ : dimX, swapDims ? dimX : dimZ] as const
|
|
84
|
-
const [x, z] =
|
|
81
|
+
const [x, z] = snapPlanPointToActiveGrid(
|
|
85
82
|
[event.localPosition[0], event.localPosition[2]],
|
|
86
|
-
getActiveBuildingPose(),
|
|
87
83
|
(value, axis) => snapToGrid(value, dimensions[axis]),
|
|
88
84
|
)
|
|
89
85
|
const y = ctx.gridPosition.y
|