@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
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
collectAlignmentAnchors,
|
|
10
10
|
emitter,
|
|
11
11
|
findLevelAncestorId,
|
|
12
|
+
formatLinearMeasurement,
|
|
12
13
|
type GridEvent,
|
|
13
14
|
getScaledDimensions,
|
|
14
15
|
getWallBodyCenterOffset,
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
type WallEvent,
|
|
31
32
|
} from '@pascal-app/core'
|
|
32
33
|
import { useViewer } from '@pascal-app/viewer'
|
|
33
|
-
import { Html } from '@react-three/drei'
|
|
34
34
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
35
35
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
36
36
|
import {
|
|
@@ -50,7 +50,6 @@ import {
|
|
|
50
50
|
publishPlacementSurface,
|
|
51
51
|
} from '../../../lib/active-placement-surface'
|
|
52
52
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
53
|
-
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
54
53
|
import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
|
|
55
54
|
import { createMovementSfxTick } from '../../../lib/sfx/movement-tick'
|
|
56
55
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
@@ -74,6 +73,7 @@ import useEditor, {
|
|
|
74
73
|
import useFacingPose from '../../../store/use-facing-pose'
|
|
75
74
|
import { getMovingNode } from '../../../store/use-interaction-scope'
|
|
76
75
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
76
|
+
import { Html } from '../../editor/localized-html'
|
|
77
77
|
import {
|
|
78
78
|
createItemSurfaceGridDispatch,
|
|
79
79
|
createItemSurfacePointerArbitration,
|
|
@@ -1008,9 +1008,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1008
1008
|
|
|
1009
1009
|
// Floor grab-offset: the item tracks the grabbed point instead of snapping
|
|
1010
1010
|
// its origin under the cursor. The offset is computed in building-local space
|
|
1011
|
-
// (`event.localPosition
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1011
|
+
// (`event.localPosition`, which `floorStrategy.move` snaps), and the corrected
|
|
1012
|
+
// local point is re-projected to a corrected world point so both frames carry
|
|
1013
|
+
// the offset and stay consistent.
|
|
1014
1014
|
const applyFloorGrabOffset = (event: GridEvent): GridEvent => {
|
|
1015
1015
|
if (relativeFloorStart === null) return event
|
|
1016
1016
|
const rawX = event.localPosition[0]
|
|
@@ -1871,6 +1871,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1871
1871
|
position: floorPos,
|
|
1872
1872
|
rotation,
|
|
1873
1873
|
})
|
|
1874
|
+
// The surface moves published a building-space cursor (the host's top, level height
|
|
1875
|
+
// included) as the live transform. Back on the floor the item renders level-local, so
|
|
1876
|
+
// that stale height lifted it by its level's elevation (a bedside left on a 4th-floor bed
|
|
1877
|
+
// floated ~10 m up) until the next floor move: publish the floor pose now.
|
|
1878
|
+
useLiveTransforms.getState().set(draft.id, { position: floorPos, rotation: rotation[1] })
|
|
1874
1879
|
}
|
|
1875
1880
|
|
|
1876
1881
|
revalidate()
|
|
@@ -2717,13 +2722,31 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2717
2722
|
|
|
2718
2723
|
// ---- Subscribe ----
|
|
2719
2724
|
|
|
2725
|
+
// With a level selected, surfaces on other floors don't capture the draft: past the building
|
|
2726
|
+
// edge, through a window or over a stairwell the ray finds a lower floor's furniture, slab or
|
|
2727
|
+
// wall, and adopting it re-parented the item to that floor mid-gesture (and paid a level
|
|
2728
|
+
// change each time). The floor cursor keeps following the selected level instead.
|
|
2729
|
+
const onActiveLevel = (node: AnyNode | undefined) => {
|
|
2730
|
+
const levelId = useViewer.getState().selection.levelId
|
|
2731
|
+
if (!levelId || !node) return true
|
|
2732
|
+
return resolveLevelId(node, useScene.getState().nodes) === levelId
|
|
2733
|
+
}
|
|
2720
2734
|
const specializedKinds = new Set<string>()
|
|
2721
|
-
const
|
|
2735
|
+
const unsubscribeSpecialized: (() => void)[] = []
|
|
2736
|
+
const subscribeSpecialized = <E extends { node?: AnyNode }>(
|
|
2737
|
+
key: string,
|
|
2738
|
+
handler: (event: E) => void,
|
|
2739
|
+
) => {
|
|
2722
2740
|
specializedKinds.add(key.slice(0, key.lastIndexOf(':')))
|
|
2723
|
-
|
|
2741
|
+
const gated = (event: E) => {
|
|
2742
|
+
if (onActiveLevel(event.node)) handler(event)
|
|
2743
|
+
}
|
|
2744
|
+
emitter.on(key as never, gated as never)
|
|
2745
|
+
unsubscribeSpecialized.push(() => emitter.off(key as never, gated as never))
|
|
2724
2746
|
}
|
|
2725
2747
|
const genericRoute = (phase: 'enter' | 'move' | 'click' | 'leave') => (event: NodeEvent) => {
|
|
2726
2748
|
if (specializedKinds.has(event.node.type)) return
|
|
2749
|
+
if (!onActiveLevel(event.node)) return
|
|
2727
2750
|
if (
|
|
2728
2751
|
!surfaceOwnership.allows(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
2729
2752
|
)
|
|
@@ -2820,38 +2843,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2820
2843
|
gridDispatch.cancel()
|
|
2821
2844
|
emitter.off('grid:move', gridDispatch.schedule)
|
|
2822
2845
|
emitter.off('grid:click', onGridClick)
|
|
2823
|
-
|
|
2824
|
-
emitter.off('item:move', onItemMove)
|
|
2825
|
-
emitter.off('item:leave', onItemLeave)
|
|
2826
|
-
emitter.off('item:click', onItemClick)
|
|
2827
|
-
emitter.off('wall:enter', onWallEnter)
|
|
2828
|
-
emitter.off('wall:move', onWallMove)
|
|
2829
|
-
emitter.off('wall:click', onWallClick)
|
|
2830
|
-
emitter.off('wall:leave', onWallLeave)
|
|
2831
|
-
emitter.off('roof:enter', onRoofWallEnter)
|
|
2832
|
-
emitter.off('roof:move', onRoofWallMove)
|
|
2833
|
-
emitter.off('roof:click', onRoofWallClick)
|
|
2834
|
-
emitter.off('roof:leave', onRoofWallLeave)
|
|
2846
|
+
for (const unsubscribe of unsubscribeSpecialized) unsubscribe()
|
|
2835
2847
|
emitter.off('node:enter', onNodeEnter)
|
|
2836
2848
|
emitter.off('node:move', onNodeMove)
|
|
2837
2849
|
emitter.off('node:click', onNodeClick)
|
|
2838
2850
|
emitter.off('node:leave', onNodeLeave)
|
|
2839
|
-
emitter.off('ceiling:enter', onCeilingEnter)
|
|
2840
|
-
emitter.off('ceiling:move', onCeilingMove)
|
|
2841
|
-
emitter.off('ceiling:click', onCeilingClick)
|
|
2842
|
-
emitter.off('ceiling:leave', onCeilingLeave)
|
|
2843
|
-
emitter.off('cabinet:enter', onItemEnter)
|
|
2844
|
-
emitter.off('procedural-item:enter' as never, onItemEnter)
|
|
2845
|
-
emitter.off('cabinet:move', onItemMove)
|
|
2846
|
-
emitter.off('procedural-item:move' as never, onItemMove)
|
|
2847
|
-
emitter.off('cabinet:click', onItemClick)
|
|
2848
|
-
emitter.off('procedural-item:click' as never, onItemClick)
|
|
2849
|
-
emitter.off('cabinet:leave', onItemLeave)
|
|
2850
|
-
emitter.off('procedural-item:leave' as never, onItemLeave)
|
|
2851
|
-
emitter.off('shelf:enter', onShelfEnter)
|
|
2852
|
-
emitter.off('shelf:move', onShelfMove)
|
|
2853
|
-
emitter.off('shelf:click', onShelfClick)
|
|
2854
|
-
emitter.off('shelf:leave', onShelfLeave)
|
|
2855
2851
|
emitter.off('node:click', commitFloorOnSurfaceClick as never)
|
|
2856
2852
|
emitter.off('tool:cancel', onCancel)
|
|
2857
2853
|
window.removeEventListener('keydown', onKeyDown)
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
emitter,
|
|
7
|
+
formatLinearMeasurement,
|
|
7
8
|
sceneRegistry,
|
|
8
9
|
useScene,
|
|
9
10
|
type WallFace,
|
|
@@ -27,7 +28,6 @@ import {
|
|
|
27
28
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
28
29
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
29
30
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
30
|
-
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
31
31
|
import { paintRegionTargets, usePaintRegionMode } from '../../../lib/paint-region-mode'
|
|
32
32
|
import { isTypingTarget } from '../../../lib/typing-target'
|
|
33
33
|
import {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '@pascal-app/core/procedural-items'
|
|
23
23
|
import { Group, Vector3 } from 'three'
|
|
24
24
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
25
|
+
import useEditor from '../../../store/use-editor'
|
|
25
26
|
import { registerHostingTestNode } from '../__fixtures__/hosting'
|
|
26
27
|
import {
|
|
27
28
|
createItemSurfaceGridDispatch,
|
|
@@ -62,6 +63,7 @@ const host = ItemNode.parse({
|
|
|
62
63
|
let restoreRegistry: () => void
|
|
63
64
|
let oldNodes: ReturnType<typeof useScene.getState>['nodes']
|
|
64
65
|
let oldRoots: AnyNodeId[]
|
|
66
|
+
let oldSnapping: ReturnType<typeof useEditor.getState>['snappingModeByContext']
|
|
65
67
|
|
|
66
68
|
beforeEach(() => {
|
|
67
69
|
globalThis.requestAnimationFrame = (callback) => {
|
|
@@ -69,6 +71,14 @@ beforeEach(() => {
|
|
|
69
71
|
return 0
|
|
70
72
|
}
|
|
71
73
|
globalThis.cancelAnimationFrame = () => {}
|
|
74
|
+
// Poses are asserted raw: whatever context a previous suite left active
|
|
75
|
+
// (a tool, a scope, its snapping mode) must not snap them to the grid.
|
|
76
|
+
oldSnapping = useEditor.getState().snappingModeByContext
|
|
77
|
+
useEditor.setState({
|
|
78
|
+
snappingModeByContext: Object.fromEntries(
|
|
79
|
+
Object.keys(oldSnapping).map((context) => [context, 'off']),
|
|
80
|
+
) as typeof oldSnapping,
|
|
81
|
+
})
|
|
72
82
|
restoreRegistry = nodeRegistry._snapshot()
|
|
73
83
|
oldNodes = useScene.getState().nodes
|
|
74
84
|
oldRoots = useScene.getState().rootNodeIds
|
|
@@ -119,6 +129,7 @@ afterEach(() => {
|
|
|
119
129
|
globalThis.requestAnimationFrame = originalRaf
|
|
120
130
|
globalThis.cancelAnimationFrame = originalCancelRaf
|
|
121
131
|
restoreRegistry()
|
|
132
|
+
useEditor.setState({ snappingModeByContext: oldSnapping })
|
|
122
133
|
useLiveTransforms.getState().clear(original.id)
|
|
123
134
|
for (const id of ['level_surface-move', host.id, 'item_second-host'])
|
|
124
135
|
sceneRegistry.nodes.delete(id)
|
|
@@ -107,6 +107,7 @@ test('focus completion: mounted registry mover owns only outside R T and Alt', a
|
|
|
107
107
|
cancelAnimationFrame: { configurable: true, value: (key: number) => frames.delete(key) },
|
|
108
108
|
IS_REACT_ACT_ENVIRONMENT: { configurable: true, value: true },
|
|
109
109
|
})
|
|
110
|
+
let supportY = 0
|
|
110
111
|
core.registerNode({
|
|
111
112
|
kind: node.type,
|
|
112
113
|
schemaVersion: 1,
|
|
@@ -114,7 +115,14 @@ test('focus completion: mounted registry mover owns only outside R T and Alt', a
|
|
|
114
115
|
schema: z.object({ type: z.literal(node.type) }).passthrough(),
|
|
115
116
|
defaults: () => ({}),
|
|
116
117
|
capabilities: {
|
|
117
|
-
movable: {
|
|
118
|
+
movable: {
|
|
119
|
+
isValidPosition: () => false,
|
|
120
|
+
resolvePosition: ({ position }: { position: [number, number, number] }) => [
|
|
121
|
+
position[0],
|
|
122
|
+
supportY,
|
|
123
|
+
position[2],
|
|
124
|
+
],
|
|
125
|
+
},
|
|
118
126
|
floorPlaced: {
|
|
119
127
|
collides: true,
|
|
120
128
|
footprint: (candidate: AnyNode) => ({
|
|
@@ -251,6 +259,39 @@ test('focus completion: mounted registry mover owns only outside R T and Alt', a
|
|
|
251
259
|
expect((await key('t', false)).defaultPrevented).toBe(true)
|
|
252
260
|
expect(mesh.rotation.y).toBeCloseTo(0.25)
|
|
253
261
|
expect(core.useLiveTransforms.getState().transforms.get(id)?.rotation).toBeCloseTo(0.25)
|
|
262
|
+
supportY = 0.75
|
|
263
|
+
await act(async () => {
|
|
264
|
+
core.emitter.emit('grid:move', {
|
|
265
|
+
nativeEvent: {
|
|
266
|
+
target: { closest: () => null },
|
|
267
|
+
} as unknown as import('@pascal-app/core').GridEvent['nativeEvent'],
|
|
268
|
+
position: [7, 0, 3],
|
|
269
|
+
localPosition: [7, 0, 3],
|
|
270
|
+
stopPropagation() {},
|
|
271
|
+
} as import('@pascal-app/core').GridEvent)
|
|
272
|
+
flushFrames()
|
|
273
|
+
})
|
|
274
|
+
expect(mesh.position.y).toBeCloseTo(0.75)
|
|
275
|
+
expect(core.useLiveTransforms.getState().transforms.get(id)?.position?.[1]).toBeCloseTo(0.75)
|
|
276
|
+
expect(
|
|
277
|
+
(core.useScene.getState().nodes[id] as unknown as { position: number[] }).position[1],
|
|
278
|
+
).toBe(0)
|
|
279
|
+
supportY = 1.25
|
|
280
|
+
await key('Alt', false)
|
|
281
|
+
await act(async () => {
|
|
282
|
+
core.emitter.emit('grid:click', {
|
|
283
|
+
nativeEvent: {
|
|
284
|
+
target: { closest: () => null },
|
|
285
|
+
} as unknown as import('@pascal-app/core').GridEvent['nativeEvent'],
|
|
286
|
+
position: [7, 0, 3],
|
|
287
|
+
localPosition: [7, 0, 3],
|
|
288
|
+
stopPropagation() {},
|
|
289
|
+
} as import('@pascal-app/core').GridEvent)
|
|
290
|
+
flushFrames()
|
|
291
|
+
})
|
|
292
|
+
expect(
|
|
293
|
+
(core.useScene.getState().nodes[id] as unknown as { position: number[] }).position[1],
|
|
294
|
+
).toBeCloseTo(1.25)
|
|
254
295
|
} finally {
|
|
255
296
|
if (root)
|
|
256
297
|
await act(async () => {
|
|
@@ -484,6 +484,18 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
484
484
|
return Array.isArray(rotation) ? [rotation[0] ?? 0, y, rotation[2] ?? 0] : y
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
const resolvePosition = (position: [number, number, number]) => {
|
|
488
|
+
const nodes = useScene.getState().nodes
|
|
489
|
+
const current = nodes[node.id] ?? node
|
|
490
|
+
return (
|
|
491
|
+
nodeRegistry.get(current.type)?.capabilities?.movable?.resolvePosition?.({
|
|
492
|
+
node: current,
|
|
493
|
+
position,
|
|
494
|
+
nodes,
|
|
495
|
+
}) ?? position
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
487
499
|
const keepStartingElevation = (position: [number, number, number], yaw: number) => {
|
|
488
500
|
if (!preserveLevelAndElevation) return position
|
|
489
501
|
const rotation = toCommitRotation(yaw)
|
|
@@ -534,14 +546,19 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
534
546
|
// them through `useLiveNodeOverrides` (transient — no history churn;
|
|
535
547
|
// GeometrySystem merges overrides via getEffectiveNode). Each connected
|
|
536
548
|
// node is re-dirtied so its geometry rebuilds against the new override.
|
|
537
|
-
|
|
549
|
+
// False, previewing nothing, when the connected joints cannot follow.
|
|
550
|
+
const previewConnectivity = (
|
|
551
|
+
position: [number, number, number],
|
|
552
|
+
rotationY: number,
|
|
553
|
+
): boolean => {
|
|
538
554
|
const connectivity = connectivityRef.current
|
|
539
|
-
if (!connectivity) return
|
|
555
|
+
if (!connectivity) return true
|
|
540
556
|
const updates = resolveConnectivityUpdates(
|
|
541
557
|
connectivity,
|
|
542
558
|
buildPreviewNode(position, rotationY),
|
|
543
559
|
)
|
|
544
|
-
if (updates
|
|
560
|
+
if (!updates) return false
|
|
561
|
+
if (updates.length === 0) return true
|
|
545
562
|
useLiveNodeOverrides
|
|
546
563
|
.getState()
|
|
547
564
|
.setMany(updates.map((u) => [u.id, u.data as Record<string, unknown>] as const))
|
|
@@ -549,6 +566,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
549
566
|
for (const u of updates) {
|
|
550
567
|
if (useScene.getState().nodes[u.id]) useScene.getState().markDirty(u.id)
|
|
551
568
|
}
|
|
569
|
+
return true
|
|
552
570
|
}
|
|
553
571
|
|
|
554
572
|
const clearConnectivityOverrides = () => {
|
|
@@ -1026,7 +1044,11 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1026
1044
|
},
|
|
1027
1045
|
).position
|
|
1028
1046
|
}
|
|
1029
|
-
position = keepStartingElevation(position, rotationRef.current)
|
|
1047
|
+
position = resolvePosition(keepStartingElevation(position, rotationRef.current))
|
|
1048
|
+
// Carry connected ductwork along (preview only — committed on drop). A
|
|
1049
|
+
// position the connected joints cannot follow holds the last frame,
|
|
1050
|
+
// which is what the drop commits.
|
|
1051
|
+
if (!previewConnectivity(position, rotationRef.current)) return
|
|
1030
1052
|
const visualPosition = getVisualPosition(position)
|
|
1031
1053
|
hasMovedRef.current = true
|
|
1032
1054
|
setCursorPosition(visualPosition)
|
|
@@ -1054,8 +1076,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1054
1076
|
})
|
|
1055
1077
|
syncParentFramePreview(position)
|
|
1056
1078
|
markMovedNodeDirty()
|
|
1057
|
-
// Carry connected ductwork along (preview only — committed on drop).
|
|
1058
|
-
previewConnectivity(position, rotationRef.current)
|
|
1059
1079
|
|
|
1060
1080
|
const soundStep = {
|
|
1061
1081
|
coords: [x, z],
|
|
@@ -1116,7 +1136,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1116
1136
|
// tool — unless Alt (free place) is held to force placement. Other kinds
|
|
1117
1137
|
// carry no validity box (`validRef` stays true), so they're never blocked.
|
|
1118
1138
|
if (!validRef.current && !altRef.current) return
|
|
1119
|
-
const position
|
|
1139
|
+
const position = resolvePosition([...lastCursorRef.current])
|
|
1120
1140
|
|
|
1121
1141
|
const rotation = toCommitRotation(rotationRef.current)
|
|
1122
1142
|
let committedId = node.id as AnyNodeId
|
|
@@ -1171,8 +1191,9 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1171
1191
|
? resolveConnectivityUpdates(
|
|
1172
1192
|
connectivityRef.current,
|
|
1173
1193
|
buildPreviewNode(position, rotationRef.current),
|
|
1174
|
-
)
|
|
1194
|
+
)?.filter((u) => useScene.getState().nodes[u.id])
|
|
1175
1195
|
: []
|
|
1196
|
+
if (!connectivityUpdates) return
|
|
1176
1197
|
const surfaceId = surfaceAttachmentId(effectiveNode)
|
|
1177
1198
|
itemSurfaceMove?.restore()
|
|
1178
1199
|
useScene.temporal.getState().resume()
|
|
@@ -1298,7 +1319,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1298
1319
|
attachmentRotationRef.current,
|
|
1299
1320
|
)
|
|
1300
1321
|
if (nextFreeRotation === null) return
|
|
1301
|
-
sfxEmitter.emit('sfx:item-rotate')
|
|
1302
1322
|
const hostedPose = itemSurfaceMove?.rotate(nextFreeRotation)
|
|
1303
1323
|
let position = hostedPose?.position ?? lastCursorRef.current
|
|
1304
1324
|
if (
|
|
@@ -1319,10 +1339,16 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1319
1339
|
)
|
|
1320
1340
|
position = canonicalPositionFromPlan(planOrigin[0], position[1], planOrigin[2])
|
|
1321
1341
|
}
|
|
1322
|
-
|
|
1342
|
+
const rotationY = hostedPose?.rotationY ?? nextFreeRotation
|
|
1343
|
+
position = keepStartingElevation(position, rotationY)
|
|
1344
|
+
// Rotating the fitting swings its collars — connected ducts follow. A
|
|
1345
|
+
// swing they cannot follow holds the last frame; a surface-hosted node
|
|
1346
|
+
// has already turned with its host, so its drop refuses instead.
|
|
1347
|
+
if (!previewConnectivity(position, rotationY) && !hostedPose) return
|
|
1348
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
1323
1349
|
lastCursorRef.current = position
|
|
1324
|
-
freeRotationRef.current =
|
|
1325
|
-
rotationRef.current =
|
|
1350
|
+
freeRotationRef.current = rotationY
|
|
1351
|
+
rotationRef.current = rotationY
|
|
1326
1352
|
setCursorRotationY(previewRotationY(rotationRef.current))
|
|
1327
1353
|
const visualPosition = getVisualPosition(position)
|
|
1328
1354
|
setCursorPosition(visualPosition)
|
|
@@ -1335,8 +1361,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1335
1361
|
})
|
|
1336
1362
|
syncParentFramePreview(position)
|
|
1337
1363
|
markMovedNodeDirty()
|
|
1338
|
-
// Rotating the fitting swings its collars — connected ducts follow.
|
|
1339
|
-
previewConnectivity(position, rotationRef.current)
|
|
1340
1364
|
// Rotation changes the footprint's collision span — re-check validity.
|
|
1341
1365
|
if (!itemSurfaceMove?.hosted && itemSurfaceMove?.rejection && lastSurfaceEvent)
|
|
1342
1366
|
onItemMove(lastSurfaceEvent)
|
|
@@ -139,4 +139,49 @@ describe('selectable candidates', () => {
|
|
|
139
139
|
})
|
|
140
140
|
expect(collectSelectableCandidateIds().sort()).toEqual(['item_pick', 'wall_pick'])
|
|
141
141
|
})
|
|
142
|
+
|
|
143
|
+
test("a box leaves a room's automatic floor plate and ceiling to the room, keeping what hangs from it", () => {
|
|
144
|
+
const restore = nodeRegistry._snapshot()
|
|
145
|
+
try {
|
|
146
|
+
for (const kind of ['slab', 'ceiling', 'box-select-test-light'] as const)
|
|
147
|
+
if (!nodeRegistry.has(kind))
|
|
148
|
+
registerNode({
|
|
149
|
+
kind,
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
schema: z.object({ type: z.literal(kind) }) as never,
|
|
152
|
+
category: 'structure',
|
|
153
|
+
defaults: () => ({}),
|
|
154
|
+
capabilities: { selectable: {} },
|
|
155
|
+
} as AnyNodeDefinition)
|
|
156
|
+
useScene.setState({
|
|
157
|
+
nodes: {
|
|
158
|
+
level_test: {
|
|
159
|
+
id: 'level_test',
|
|
160
|
+
type: 'level',
|
|
161
|
+
parentId: 'building_test',
|
|
162
|
+
children: ['slab_shared', 'slab_drawn', 'ceiling_auto'],
|
|
163
|
+
},
|
|
164
|
+
slab_shared: {
|
|
165
|
+
id: 'slab_shared',
|
|
166
|
+
type: 'slab',
|
|
167
|
+
parentId: 'level_test',
|
|
168
|
+
boundary: 'auto',
|
|
169
|
+
plateRole: 'base',
|
|
170
|
+
},
|
|
171
|
+
slab_drawn: { id: 'slab_drawn', type: 'slab', parentId: 'level_test' },
|
|
172
|
+
ceiling_auto: {
|
|
173
|
+
id: 'ceiling_auto',
|
|
174
|
+
type: 'ceiling',
|
|
175
|
+
parentId: 'level_test',
|
|
176
|
+
boundary: 'auto',
|
|
177
|
+
children: ['light_hung'],
|
|
178
|
+
},
|
|
179
|
+
light_hung: { id: 'light_hung', type: 'box-select-test-light', parentId: 'ceiling_auto' },
|
|
180
|
+
} as unknown as Record<string, AnyNode>,
|
|
181
|
+
})
|
|
182
|
+
expect(collectSelectableCandidateIds().sort()).toEqual(['light_hung', 'slab_drawn'])
|
|
183
|
+
} finally {
|
|
184
|
+
restore()
|
|
185
|
+
}
|
|
186
|
+
})
|
|
142
187
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
isDerivedNode,
|
|
4
5
|
isRegistrySelectable,
|
|
5
6
|
type LevelNode,
|
|
6
7
|
nodeRegistry,
|
|
@@ -16,6 +17,14 @@ function isVisibleSelectableNode(node: AnyNode): boolean {
|
|
|
16
17
|
return isRegistrySelectable(node.type)
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Derived construction (a room's automatic floor plate or ceiling) belongs to its rooms, so a box
|
|
22
|
+
* never takes it along: a box round a porch would otherwise catch the house's shared floor plate,
|
|
23
|
+
* and deleting the selection took the floor out from under every room on it. A click still selects
|
|
24
|
+
* one on its own.
|
|
25
|
+
*/
|
|
26
|
+
const boxSelectable = (node: AnyNode) => !isDerivedNode(node)
|
|
27
|
+
|
|
19
28
|
export function collectSelectableCandidateIds(): string[] {
|
|
20
29
|
const { levelId } = useViewer.getState().selection
|
|
21
30
|
const { nodes } = useScene.getState()
|
|
@@ -43,7 +52,8 @@ export function collectSelectableCandidateIds(): string[] {
|
|
|
43
52
|
)
|
|
44
53
|
return
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
// A ceiling's hung lights and fans are still the box's; the ceiling is not.
|
|
56
|
+
if (isRegistrySelectable(node.type) && boxSelectable(node)) {
|
|
47
57
|
addNode(node)
|
|
48
58
|
}
|
|
49
59
|
|
|
@@ -70,7 +80,8 @@ export function collectSelectableCandidateIds(): string[] {
|
|
|
70
80
|
|
|
71
81
|
const buildingId = resolveBuildingForLevel(levelId as AnyNodeId, nodes)
|
|
72
82
|
for (const node of Object.values(nodes)) {
|
|
73
|
-
if (!node || node.type === 'level' || !isVisibleSelectableNode(node))
|
|
83
|
+
if (!node || node.type === 'level' || !isVisibleSelectableNode(node) || !boxSelectable(node))
|
|
84
|
+
continue
|
|
74
85
|
|
|
75
86
|
const def = nodeRegistry.get(node.type)
|
|
76
87
|
const isBuildingScoped = def?.floorplanScope === 'building'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type MessageDescriptor, usePascalI18n } from '@pascal-app/core'
|
|
2
2
|
import type { ThreeElements } from '@react-three/fiber'
|
|
3
3
|
import { forwardRef } from 'react'
|
|
4
4
|
import { CircleGeometry, CylinderGeometry, type Group, SphereGeometry } from 'three'
|
|
@@ -8,6 +8,7 @@ import { EDITOR_LAYER } from '../../../lib/constants'
|
|
|
8
8
|
import { wallCursorIcon } from '../../../lib/tool-cursor-icon'
|
|
9
9
|
import useEditor from '../../../store/use-editor'
|
|
10
10
|
import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
|
|
11
|
+
import { Html } from '../../editor/localized-html'
|
|
11
12
|
|
|
12
13
|
interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
13
14
|
color?: string
|
|
@@ -51,6 +52,7 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
51
52
|
},
|
|
52
53
|
ref,
|
|
53
54
|
) {
|
|
55
|
+
const i18n = usePascalI18n()
|
|
54
56
|
const tool = useEditor((s) => s.tool)
|
|
55
57
|
const mode = useEditor((s) => s.mode)
|
|
56
58
|
const catalogCategory = useEditor((s) => s.catalogCategory)
|
|
@@ -62,7 +64,8 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
62
64
|
const wallMode = useEditor((s) => s.continuationByContext.wall)
|
|
63
65
|
|
|
64
66
|
// Find the icon for the current tool — the wall shows its Rooms variant.
|
|
65
|
-
let activeToolConfig: { label: string; iconSrc: string } | null | undefined =
|
|
67
|
+
let activeToolConfig: { label: string | MessageDescriptor; iconSrc: string } | null | undefined =
|
|
68
|
+
null
|
|
66
69
|
if (mode === 'build' && tool) {
|
|
67
70
|
if (tool === 'wall') {
|
|
68
71
|
activeToolConfig = wallCursorIcon(wallMode)
|
|
@@ -162,7 +165,11 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
162
165
|
{tooltipContent || (
|
|
163
166
|
// eslint-disable-next-line @next/next/no-img-element
|
|
164
167
|
<img
|
|
165
|
-
alt={
|
|
168
|
+
alt={
|
|
169
|
+
typeof activeToolConfig!.label === 'string'
|
|
170
|
+
? activeToolConfig!.label
|
|
171
|
+
: i18n._(activeToolConfig!.label)
|
|
172
|
+
}
|
|
166
173
|
src={activeToolConfig!.iconSrc}
|
|
167
174
|
style={{
|
|
168
175
|
width: '100%',
|
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import '../../../three-types'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { formatLinearMeasurement, type LinearUnit, type MetricNotation } from '@pascal-app/core'
|
|
6
6
|
import { useEffect, useMemo } from 'react'
|
|
7
7
|
import { PlaneGeometry } from 'three'
|
|
8
8
|
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
9
9
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
10
10
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
11
|
-
import {
|
|
12
|
-
formatLinearMeasurement,
|
|
13
|
-
type LinearUnit,
|
|
14
|
-
type MetricNotation,
|
|
15
|
-
} from '../../../lib/measurements'
|
|
11
|
+
import { Html } from '../../editor/localized-html'
|
|
16
12
|
import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
|
|
17
13
|
|
|
18
14
|
const VALID_COLOR = 0x22_c5_5e // green-500
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
4
|
+
import { type PlacementTypingField, usePlacementTyping } from '../../../store/use-placement-typing'
|
|
5
|
+
|
|
6
|
+
export type PlacementCoordinateInputProps = {
|
|
7
|
+
/** What each empty field stands for; defaults to the session's `fieldDefaults`. */
|
|
8
|
+
placeholders?: readonly string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The typed-entry HUD, one box per field of the session. It only displays the
|
|
13
|
+
* session: keys reach the active tool's own handler (`routePlacementTypingKey`),
|
|
14
|
+
* so the same HUD can sit in the 3D view and the plan at once without
|
|
15
|
+
* competing for focus. Clicking a field makes it the active one.
|
|
16
|
+
*/
|
|
17
|
+
export function PlacementCoordinateInput({ placeholders }: PlacementCoordinateInputProps) {
|
|
18
|
+
const i18n = usePascalI18n()
|
|
19
|
+
const isActive = usePlacementTyping((state) => state.isActive)
|
|
20
|
+
const labels = usePlacementTyping((state) => state.labels)
|
|
21
|
+
const fields = usePlacementTyping((state) => state.fields)
|
|
22
|
+
const fieldDefaults = usePlacementTyping((state) => state.fieldDefaults)
|
|
23
|
+
const activeField = usePlacementTyping((state) => state.activeField)
|
|
24
|
+
|
|
25
|
+
if (!isActive) return null
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
data-placement-coordinate-entry
|
|
30
|
+
onClick={(event) => event.stopPropagation()}
|
|
31
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
32
|
+
style={{
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
background: 'rgba(15, 23, 42, 0.94)',
|
|
35
|
+
border: '1px solid rgba(148, 163, 184, 0.55)',
|
|
36
|
+
borderRadius: 6,
|
|
37
|
+
boxShadow: '0 4px 14px rgba(15, 23, 42, 0.28)',
|
|
38
|
+
color: '#f8fafc',
|
|
39
|
+
display: 'flex',
|
|
40
|
+
gap: 6,
|
|
41
|
+
padding: '5px 7px',
|
|
42
|
+
pointerEvents: 'auto',
|
|
43
|
+
userSelect: 'none',
|
|
44
|
+
whiteSpace: 'nowrap',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{labels.map((descriptor, index) => {
|
|
48
|
+
const field = index as PlacementTypingField
|
|
49
|
+
const label = i18n._(descriptor)
|
|
50
|
+
const active = field === activeField
|
|
51
|
+
const value = fields[field]
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-placement-coordinate-field={descriptor.id}
|
|
55
|
+
key={descriptor.id}
|
|
56
|
+
onPointerDown={(event) => {
|
|
57
|
+
event.stopPropagation()
|
|
58
|
+
usePlacementTyping.getState().setActiveField(field)
|
|
59
|
+
}}
|
|
60
|
+
style={{
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
display: 'flex',
|
|
63
|
+
flexDirection: 'column',
|
|
64
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
65
|
+
fontSize: 9,
|
|
66
|
+
gap: 2,
|
|
67
|
+
textTransform: 'uppercase',
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<span style={{ color: active ? '#a5b4fc' : '#94a3b8' }}>{label}</span>
|
|
71
|
+
<span
|
|
72
|
+
aria-label={label}
|
|
73
|
+
role="status"
|
|
74
|
+
style={{
|
|
75
|
+
background: active ? '#eef2ff' : '#e2e8f0',
|
|
76
|
+
borderRadius: 3,
|
|
77
|
+
color: value ? '#0f172a' : '#64748b',
|
|
78
|
+
fontSize: 12,
|
|
79
|
+
fontWeight: 600,
|
|
80
|
+
minWidth: 76,
|
|
81
|
+
outline: active ? '2px solid #818cf8' : 'none',
|
|
82
|
+
padding: '3px 5px',
|
|
83
|
+
textAlign: 'right',
|
|
84
|
+
textTransform: 'none',
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{value || placeholders?.[field] || fieldDefaults[field] || '\u00a0'}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
})}
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
-
import { Html } from '@react-three/drei'
|
|
5
4
|
import { useLayoutEffect, useMemo } from 'react'
|
|
6
5
|
import { BufferGeometry, Float32BufferAttribute, Line as ThreeLine } from 'three'
|
|
7
6
|
import { LineBasicNodeMaterial } from 'three/webgpu'
|
|
8
7
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
9
8
|
import type { PlacementPreviewDimension } from '../../../store/use-placement-preview'
|
|
10
9
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
10
|
+
import { Html } from '../../editor/localized-html'
|
|
11
11
|
import { formatMeasurement } from '../../editor/measurement-pill'
|
|
12
12
|
|
|
13
13
|
const DIMENSION_COLOR = 0x63_66_f1
|
|
@@ -83,10 +83,6 @@ function getCurveTangentAtPoint(point: PlanPoint, segment: SegmentAngleLike): Pl
|
|
|
83
83
|
return best.tangent
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function formatAngleRadians(angle: number) {
|
|
87
|
-
return `${Math.round((angle * 180) / Math.PI)}°`
|
|
88
|
-
}
|
|
89
|
-
|
|
90
86
|
export function getAngleBetweenVectors(first: PlanPoint, second: PlanPoint): number | null {
|
|
91
87
|
const firstLength = Math.hypot(first[0], first[1])
|
|
92
88
|
const secondLength = Math.hypot(second[0], second[1])
|