@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
|
@@ -388,7 +388,13 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
388
388
|
endTransientMarkers()
|
|
389
389
|
})
|
|
390
390
|
sfxEmitter.emit('sfx:item-place')
|
|
391
|
-
|
|
391
|
+
// The moved node stays selected, as after a 3D move: the joined walls and
|
|
392
|
+
// rooms a wall move reshapes are part of its write, not a pick.
|
|
393
|
+
useViewer.getState().setSelection({
|
|
394
|
+
selectedIds: useScene.getState().nodes[movingNode.id]
|
|
395
|
+
? [movingNode.id]
|
|
396
|
+
: initialAffectedIds,
|
|
397
|
+
})
|
|
392
398
|
return
|
|
393
399
|
}
|
|
394
400
|
|
|
@@ -844,14 +850,25 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
844
850
|
}
|
|
845
851
|
boxEl.setAttribute('transform', transform)
|
|
846
852
|
const oldY = originalPosition[1]
|
|
853
|
+
const position = nodeRegistry.get(movingNode.type)?.capabilities?.movable?.resolvePosition?.({
|
|
854
|
+
node: useScene.getState().nodes[movingNode.id] ?? movingNode,
|
|
855
|
+
position: [finalX, oldY, finalZ],
|
|
856
|
+
nodes: useScene.getState().nodes,
|
|
857
|
+
}) ?? [finalX, oldY, finalZ]
|
|
858
|
+
if (def?.capabilities?.movable?.resolvePosition) {
|
|
859
|
+
useLiveTransforms.getState().set(movingNode.id, {
|
|
860
|
+
position: position as [number, number, number],
|
|
861
|
+
rotation: currentRotation,
|
|
862
|
+
})
|
|
863
|
+
}
|
|
847
864
|
lastPositionValid = movableValidityConfig?.isValidPosition
|
|
848
865
|
? movableValidityConfig.isValidPosition({
|
|
849
866
|
node: {
|
|
850
867
|
...movingNode,
|
|
851
|
-
position
|
|
868
|
+
position,
|
|
852
869
|
rotation: currentRotation,
|
|
853
870
|
} as AnyNode,
|
|
854
|
-
position: [
|
|
871
|
+
position: position as [number, number, number],
|
|
855
872
|
rotation: currentRotation,
|
|
856
873
|
levelId:
|
|
857
874
|
(useViewer.getState().selection.levelId as AnyNodeId | null) ??
|
|
@@ -879,6 +896,11 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
879
896
|
(storedRotation as [number?, number?, number?])[2] ?? 0,
|
|
880
897
|
]
|
|
881
898
|
: snapped.rotation
|
|
899
|
+
const position = nodeRegistry.get(movingNode.type)?.capabilities?.movable?.resolvePosition?.({
|
|
900
|
+
node: useScene.getState().nodes[movingNode.id] ?? movingNode,
|
|
901
|
+
position: [sx, oldY, sz],
|
|
902
|
+
nodes: useScene.getState().nodes,
|
|
903
|
+
}) ?? [sx, oldY, sz]
|
|
882
904
|
const rotationPatch = 'rotation' in movingNode ? { rotation } : {}
|
|
883
905
|
const atomicPreview =
|
|
884
906
|
isFreshPlacement &&
|
|
@@ -931,7 +953,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
931
953
|
const committedId = commitFreshPlacementSubtree(
|
|
932
954
|
movingNode.id as AnyNodeId,
|
|
933
955
|
{
|
|
934
|
-
position
|
|
956
|
+
position,
|
|
935
957
|
...rotationPatch,
|
|
936
958
|
metadata: stripPlacementMetadataFlags((movingNode as { metadata?: unknown }).metadata),
|
|
937
959
|
visible: true,
|
|
@@ -947,7 +969,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
947
969
|
useScene.getState().updateNode(
|
|
948
970
|
movingNode.id as AnyNodeId,
|
|
949
971
|
{
|
|
950
|
-
position
|
|
972
|
+
position,
|
|
951
973
|
...rotationPatch,
|
|
952
974
|
} as Partial<AnyNode>,
|
|
953
975
|
)
|
|
@@ -999,6 +1021,8 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
999
1021
|
for (const relatedEntry of relatedEntries) {
|
|
1000
1022
|
relatedEntry.style.visibility = ''
|
|
1001
1023
|
}
|
|
1024
|
+
if (def?.capabilities?.movable?.resolvePosition)
|
|
1025
|
+
useLiveTransforms.getState().clear(movingNode.id)
|
|
1002
1026
|
boxEl.remove()
|
|
1003
1027
|
useAlignmentGuides.getState().clear()
|
|
1004
1028
|
}
|
|
@@ -13,33 +13,92 @@ import { useFloorplanRender } from './floorplan-render-context'
|
|
|
13
13
|
*
|
|
14
14
|
* endpoint (corner) → square midpoint → triangle
|
|
15
15
|
* intersection → ✕ cross wall body (edge) → circle
|
|
16
|
+
* perpendicular → ⊥ corner projection guide → diamond
|
|
17
|
+
* grid length → small +
|
|
16
18
|
*
|
|
17
19
|
* Indigo to match the 3D beacon, except the corner (endpoint) square which is
|
|
18
|
-
* green to match the alignment guides.
|
|
19
|
-
*
|
|
20
|
+
* green to match the alignment guides. A point that carries a `label` (a wall
|
|
21
|
+
* pulled from a wall end) also names what it caught in a pill beside the
|
|
22
|
+
* glyph. Sizes are pixel-budgeted via `unitsPerPixel` so the marker stays a
|
|
20
23
|
* constant size on screen at any zoom. Mounted inside the `data-floorplan-scene`
|
|
21
24
|
* group so coordinates are world meters (XZ) 1:1, like the alignment guides.
|
|
22
25
|
*/
|
|
23
26
|
const COLOR = '#6366f1' // indigo-500 — matches the 3D beacon
|
|
24
27
|
const ENDPOINT_COLOR = '#22c55e' // green-500 — corner (endpoint) snap accent
|
|
25
28
|
|
|
26
|
-
export const FloorplanSnapBeaconLayer = memo(function FloorplanSnapBeaconLayer(
|
|
29
|
+
export const FloorplanSnapBeaconLayer = memo(function FloorplanSnapBeaconLayer({
|
|
30
|
+
unitsPerPixel,
|
|
31
|
+
}: {
|
|
32
|
+
/** The plan's live scale, which keeps a labelled marker screen-sized. */
|
|
33
|
+
unitsPerPixel?: number
|
|
34
|
+
}) {
|
|
27
35
|
const point = useWallSnapIndicator((s) => s.point)
|
|
28
36
|
const ctx = useFloorplanRender()
|
|
29
37
|
|
|
30
38
|
if (!point) return null
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
// A labelled point (a wall pulled from a wall end) is sized on screen.
|
|
41
|
+
const upp = (point.label ? unitsPerPixel : undefined) ?? ctx?.unitsPerPixel ?? 0.01
|
|
33
42
|
const m = 6 * upp // base half-size of the glyph in world meters
|
|
34
43
|
const stroke = 1.5 * upp
|
|
35
44
|
|
|
36
45
|
return (
|
|
37
46
|
<g pointerEvents="none">
|
|
38
47
|
<SnapMarker color={COLOR} kind={point.kind} m={m} stroke={stroke} x={point.x} z={point.z} />
|
|
48
|
+
{point.label ? (
|
|
49
|
+
<SnapLabel
|
|
50
|
+
color={point.kind === 'endpoint' ? ENDPOINT_COLOR : COLOR}
|
|
51
|
+
sceneRotationDeg={ctx?.sceneRotationDeg ?? 0}
|
|
52
|
+
text={point.label}
|
|
53
|
+
upp={upp}
|
|
54
|
+
x={point.x}
|
|
55
|
+
z={point.z}
|
|
56
|
+
/>
|
|
57
|
+
) : null}
|
|
39
58
|
</g>
|
|
40
59
|
)
|
|
41
60
|
})
|
|
42
61
|
|
|
62
|
+
/** What the snap caught, in a pill down and to the right of the glyph, kept upright. */
|
|
63
|
+
function SnapLabel({
|
|
64
|
+
color,
|
|
65
|
+
sceneRotationDeg,
|
|
66
|
+
text,
|
|
67
|
+
upp,
|
|
68
|
+
x,
|
|
69
|
+
z,
|
|
70
|
+
}: {
|
|
71
|
+
color: string
|
|
72
|
+
sceneRotationDeg: number
|
|
73
|
+
text: string
|
|
74
|
+
upp: number
|
|
75
|
+
x: number
|
|
76
|
+
z: number
|
|
77
|
+
}) {
|
|
78
|
+
const fontSize = 10.5 * upp
|
|
79
|
+
const padX = 5 * upp
|
|
80
|
+
const width = text.length * fontSize * 0.56 + padX * 2
|
|
81
|
+
const height = fontSize + 6 * upp
|
|
82
|
+
return (
|
|
83
|
+
<g
|
|
84
|
+
transform={`translate(${x} ${z}) rotate(${-sceneRotationDeg}) translate(${12 * upp} ${16 * upp})`}
|
|
85
|
+
>
|
|
86
|
+
<rect fill={color} height={height} rx={3 * upp} width={width} x={0} y={-height / 2} />
|
|
87
|
+
<text
|
|
88
|
+
dominantBaseline="middle"
|
|
89
|
+
fill="#ffffff"
|
|
90
|
+
fontFamily="-apple-system, system-ui, sans-serif"
|
|
91
|
+
fontSize={fontSize}
|
|
92
|
+
fontWeight={600}
|
|
93
|
+
x={padX}
|
|
94
|
+
y={0}
|
|
95
|
+
>
|
|
96
|
+
{text}
|
|
97
|
+
</text>
|
|
98
|
+
</g>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
43
102
|
function SnapMarker({
|
|
44
103
|
color,
|
|
45
104
|
kind,
|
|
@@ -72,6 +131,39 @@ function SnapMarker({
|
|
|
72
131
|
</g>
|
|
73
132
|
)
|
|
74
133
|
}
|
|
134
|
+
if (kind === 'perpendicular') {
|
|
135
|
+
// ⊥ — a base line with an upright from its middle, and the square corner.
|
|
136
|
+
const c = m * 1.3
|
|
137
|
+
return (
|
|
138
|
+
<g fill="none" stroke={color} strokeLinecap="round" strokeWidth={stroke * 1.6}>
|
|
139
|
+
<line x1={x - c} x2={x + c} y1={z} y2={z} />
|
|
140
|
+
<line x1={x} x2={x} y1={z} y2={z - c * 1.4} />
|
|
141
|
+
<polyline
|
|
142
|
+
points={`${x + c * 0.5},${z} ${x + c * 0.5},${z - c * 0.5} ${x},${z - c * 0.5}`}
|
|
143
|
+
/>
|
|
144
|
+
</g>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
if (kind === 'alignment') {
|
|
148
|
+
const c = m * 1.2
|
|
149
|
+
return (
|
|
150
|
+
<polygon
|
|
151
|
+
fill="#ffffff"
|
|
152
|
+
points={`${x},${z - c} ${x + c},${z} ${x},${z + c} ${x - c},${z}`}
|
|
153
|
+
stroke={color}
|
|
154
|
+
strokeWidth={stroke * 1.4}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
if (kind === 'grid') {
|
|
159
|
+
const c = m * 0.9
|
|
160
|
+
return (
|
|
161
|
+
<g stroke={color} strokeLinecap="round" strokeWidth={stroke * 1.4}>
|
|
162
|
+
<line x1={x - c} x2={x + c} y1={z} y2={z} />
|
|
163
|
+
<line x1={x} x2={x} y1={z - c} y2={z + c} />
|
|
164
|
+
</g>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
75
167
|
// 'wall' (edge / along-wall) → circle
|
|
76
168
|
return <circle cx={x} cy={z} fill={color} r={m} />
|
|
77
169
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { AnyNode, AnyNodeId } from '@pascal-app/core'
|
|
4
|
-
import { useScene } from '@pascal-app/core'
|
|
4
|
+
import { useLocale, useScene } from '@pascal-app/core'
|
|
5
5
|
import { useViewer } from '@pascal-app/viewer'
|
|
6
6
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
7
7
|
import { createPortal } from 'react-dom'
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveDimensionDrive,
|
|
12
12
|
} from '../../../lib/floorplan/dimension-drive'
|
|
13
13
|
import { formatScheduleLength } from '../../../lib/floorplan/schedules'
|
|
14
|
+
import { commitRoomPush } from '../../../lib/room-handle-drag'
|
|
14
15
|
import useEditor from '../../../store/use-editor'
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -20,7 +21,8 @@ import useEditor from '../../../store/use-editor'
|
|
|
20
21
|
* plate, pre-filled with the current text. Enter commits, Esc cancels.
|
|
21
22
|
* Committing DRIVES geometry: the anchor the dimension runs toward moves
|
|
22
23
|
* along the dimension direction by the delta, and connected wall junctions
|
|
23
|
-
* follow (`planDimensionDrive`)
|
|
24
|
+
* follow (`planDimensionDrive`); a dimension across a wall slides the whole
|
|
25
|
+
* wall, as its push arrow does (`commitRoomPush`). When nothing bindable is under the far
|
|
24
26
|
* anchor the value is written as `textOverride` on a construction-dimension
|
|
25
27
|
* node instead, and the label renders an "override" badge.
|
|
26
28
|
*
|
|
@@ -60,6 +62,32 @@ export function useDimensionEditAllowed(): boolean {
|
|
|
60
62
|
return useEditor((state) => isDimensionEditAllowed(state, { readOnly }))
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The typeable dimension value under a press, with the node typing it drives:
|
|
67
|
+
* the pressed element's or, when the plan's dashed group box lies over it, the
|
|
68
|
+
* one beneath (a multi-selection's walls keep their dimensions inside it).
|
|
69
|
+
*/
|
|
70
|
+
export function typeableDimensionAt(
|
|
71
|
+
target: EventTarget | null,
|
|
72
|
+
clientX: number,
|
|
73
|
+
clientY: number,
|
|
74
|
+
): { label: Element; ownerNodeId: AnyNodeId } | null {
|
|
75
|
+
const resolve = (element: Element | null | undefined) => {
|
|
76
|
+
const hit = element?.closest?.('[data-floorplan-dimension-hit]')
|
|
77
|
+
const label = hit?.closest('[data-floorplan-annotation-label]')
|
|
78
|
+
const ownerNodeId = hit?.closest('[data-node-id]')?.getAttribute('data-node-id')
|
|
79
|
+
return label && ownerNodeId ? { label, ownerNodeId: ownerNodeId as AnyNodeId } : null
|
|
80
|
+
}
|
|
81
|
+
const element = target as Element | null
|
|
82
|
+
const direct = resolve(element)
|
|
83
|
+
if (direct || !element?.closest?.('[data-group-selection-box]')) return direct
|
|
84
|
+
for (const below of document.elementsFromPoint(clientX, clientY)) {
|
|
85
|
+
const found = resolve(below)
|
|
86
|
+
if (found) return found
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
63
91
|
type EditTarget = {
|
|
64
92
|
ownerNodeId: AnyNodeId
|
|
65
93
|
text: string
|
|
@@ -78,6 +106,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
|
|
|
78
106
|
const [error, setError] = useState<string | null>(null)
|
|
79
107
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
80
108
|
const unit = useViewer((state) => state.unit)
|
|
109
|
+
const locale = useLocale()
|
|
81
110
|
|
|
82
111
|
useEffect(() => {
|
|
83
112
|
let down: { x: number; y: number } | null = null
|
|
@@ -92,13 +121,9 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
|
|
|
92
121
|
return
|
|
93
122
|
}
|
|
94
123
|
if (!isDimensionEditAllowed(useEditor.getState(), useScene.getState())) return
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const label = hit.closest('[data-floorplan-annotation-label]')
|
|
99
|
-
const owner = hit.closest('[data-node-id]')
|
|
100
|
-
const ownerNodeId = owner?.getAttribute('data-node-id')
|
|
101
|
-
if (!label || !ownerNodeId) return
|
|
124
|
+
const found = typeableDimensionAt(event.target, event.clientX, event.clientY)
|
|
125
|
+
if (!found) return
|
|
126
|
+
const { label, ownerNodeId } = found
|
|
102
127
|
|
|
103
128
|
const value = number(label.getAttribute('data-floorplan-dimension-value'))
|
|
104
129
|
const witnessStart = point(
|
|
@@ -116,7 +141,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
|
|
|
116
141
|
const box = label.getBoundingClientRect()
|
|
117
142
|
const text = label.getAttribute('data-floorplan-dimension-text') ?? ''
|
|
118
143
|
setTarget({
|
|
119
|
-
ownerNodeId
|
|
144
|
+
ownerNodeId,
|
|
120
145
|
text,
|
|
121
146
|
value,
|
|
122
147
|
witnessStart,
|
|
@@ -154,7 +179,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
|
|
|
154
179
|
|
|
155
180
|
const commit = useCallback(() => {
|
|
156
181
|
if (!target) return
|
|
157
|
-
const nextLength = parseDimensionInput(draft, unit)
|
|
182
|
+
const nextLength = parseDimensionInput(draft, unit, locale)
|
|
158
183
|
if (nextLength === null) {
|
|
159
184
|
setError(`Cannot read "${draft.trim()}"`)
|
|
160
185
|
return
|
|
@@ -166,7 +191,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
|
|
|
166
191
|
}
|
|
167
192
|
setTarget(null)
|
|
168
193
|
setError(null)
|
|
169
|
-
}, [draft, target, unit])
|
|
194
|
+
}, [draft, locale, target, unit])
|
|
170
195
|
|
|
171
196
|
if (!target) return null
|
|
172
197
|
if (typeof document === 'undefined') return null
|
|
@@ -246,6 +271,21 @@ export function commitDimensionValue(args: {
|
|
|
246
271
|
return { ok: true }
|
|
247
272
|
}
|
|
248
273
|
|
|
274
|
+
if (resolution.target.kind === 'wall-offset') {
|
|
275
|
+
// The wall's own push: the whole wall slides along its normal, connected
|
|
276
|
+
// walls follow through the junction planner, rooms and floors re-form —
|
|
277
|
+
// one undo step, exactly as dragging its side arrow that far.
|
|
278
|
+
const delta = args.nextLength - args.target.value
|
|
279
|
+
if (Math.abs(delta) < 1e-9) return { ok: true }
|
|
280
|
+
return commitRoomPush(
|
|
281
|
+
{ wallId: resolution.target.wallId, t0: 0, t1: 1 },
|
|
282
|
+
resolution.target.direction,
|
|
283
|
+
delta,
|
|
284
|
+
)
|
|
285
|
+
? { ok: true }
|
|
286
|
+
: { ok: false, reason: "Can't move this wall there" }
|
|
287
|
+
}
|
|
288
|
+
|
|
249
289
|
const plan = planDimensionDrive({
|
|
250
290
|
nodes,
|
|
251
291
|
target: resolution.target,
|
|
@@ -19,6 +19,7 @@ export type DraftMeasurement = {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function FloorplanDraftMeasurement({
|
|
22
|
+
lengthHidden = false,
|
|
22
23
|
measurement,
|
|
23
24
|
measurementStroke,
|
|
24
25
|
labelBackground,
|
|
@@ -26,6 +27,8 @@ export function FloorplanDraftMeasurement({
|
|
|
26
27
|
sceneRotationDeg,
|
|
27
28
|
unitsPerPixel,
|
|
28
29
|
}: {
|
|
30
|
+
/** Leave the length plate to something drawn in its place (the typed-entry HUD). */
|
|
31
|
+
lengthHidden?: boolean
|
|
29
32
|
measurement: DraftMeasurement
|
|
30
33
|
measurementStroke: string
|
|
31
34
|
labelBackground: string
|
|
@@ -68,7 +71,10 @@ export function FloorplanDraftMeasurement({
|
|
|
68
71
|
|
|
69
72
|
return (
|
|
70
73
|
<g pointerEvents="none">
|
|
71
|
-
<g
|
|
74
|
+
<g
|
|
75
|
+
display={lengthHidden ? 'none' : undefined}
|
|
76
|
+
transform={`translate(${cx} ${cy}) rotate(${labelAngleDeg})`}
|
|
77
|
+
>
|
|
72
78
|
<rect
|
|
73
79
|
fill={labelBg}
|
|
74
80
|
height={lengthPlateH}
|
|
@@ -4,17 +4,19 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type FloorplanGeometry,
|
|
7
|
+
formatLinearMeasurement,
|
|
7
8
|
type GeometryContext,
|
|
8
9
|
nodeRegistry,
|
|
9
10
|
useScene,
|
|
10
11
|
} from '@pascal-app/core'
|
|
11
12
|
import { useViewer } from '@pascal-app/viewer'
|
|
12
13
|
import { memo, useMemo } from 'react'
|
|
13
|
-
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
14
14
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
15
|
+
import { usePlacementTyping } from '../../../store/use-placement-typing'
|
|
15
16
|
import { useFloorplanRender, useFloorplanSceneRotation } from '../floorplan-render-context'
|
|
16
17
|
import { FloorplanDimensionRenderer } from './floorplan-dimension-renderer'
|
|
17
18
|
import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
|
|
19
|
+
import { FloorplanPlacementTypingHud } from './floorplan-placement-typing-hud'
|
|
18
20
|
|
|
19
21
|
export interface FloorplanNodePreviewProps {
|
|
20
22
|
node: AnyNode
|
|
@@ -139,14 +141,24 @@ export const FloorplanPlacementPreviewLayer = memo(function FloorplanPlacementPr
|
|
|
139
141
|
const dimensions = usePlacementPreview((s) => s.dimensions)
|
|
140
142
|
const activeDimensionId = usePlacementPreview((s) => s.activeDimensionId)
|
|
141
143
|
const dimensionInput = usePlacementPreview((s) => s.dimensionInput)
|
|
144
|
+
const typingProjectedPosition = usePlacementTyping((s) => s.projectedPosition)
|
|
142
145
|
const unit = useViewer((s) => s.unit)
|
|
143
146
|
const metricNotation = useViewer((s) => s.metricNotation)
|
|
144
147
|
const sceneRotationDeg = useFloorplanSceneRotation()
|
|
148
|
+
const unitsPerPixel = useFloorplanRender()?.unitsPerPixel ?? 1
|
|
145
149
|
if (!node) return null
|
|
146
150
|
|
|
147
151
|
return (
|
|
148
152
|
<g data-floorplan-placement-preview>
|
|
149
153
|
<FloorplanNodePreview contextNodes={contextNodes} node={node} parentNode={parentNode} />
|
|
154
|
+
{typingProjectedPosition ? (
|
|
155
|
+
<FloorplanPlacementTypingHud
|
|
156
|
+
sceneRotationDeg={sceneRotationDeg}
|
|
157
|
+
unitsPerPixel={unitsPerPixel}
|
|
158
|
+
x={typingProjectedPosition[0]}
|
|
159
|
+
y={typingProjectedPosition[2]}
|
|
160
|
+
/>
|
|
161
|
+
) : null}
|
|
150
162
|
<g data-floorplan-placement-dimensions>
|
|
151
163
|
{dimensions
|
|
152
164
|
.filter((dimension) => dimension.renderInFloorplan !== false)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
PLACEMENT_TYPING_LENGTH_ANGLE,
|
|
4
|
+
usePlacementTyping,
|
|
5
|
+
} from '../../../store/use-placement-typing'
|
|
6
|
+
import { renderCurrentMarkup } from '../../../test-utils/render-current-markup'
|
|
7
|
+
import { FloorplanPlacementTypingHud } from './floorplan-placement-typing-hud'
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
usePlacementTyping.getState().clear()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('sizes and counter-rotates the HUD from the props it is given', () => {
|
|
14
|
+
usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_LENGTH_ANGLE, activeField: 0 })
|
|
15
|
+
|
|
16
|
+
const markup = renderCurrentMarkup(
|
|
17
|
+
<svg>
|
|
18
|
+
<FloorplanPlacementTypingHud sceneRotationDeg={30} unitsPerPixel={0.02} x={1} y={2} />
|
|
19
|
+
</svg>,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(markup).toContain('transform="translate(1 2) rotate(-30)"')
|
|
23
|
+
expect(markup).toMatch(/<foreignObject[^>]*width="4.72"/)
|
|
24
|
+
expect(markup).toContain('scale(0.02)')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('renders nothing without a typing session', () => {
|
|
28
|
+
const markup = renderCurrentMarkup(
|
|
29
|
+
<svg>
|
|
30
|
+
<FloorplanPlacementTypingHud sceneRotationDeg={0} unitsPerPixel={0.02} x={0} y={0} />
|
|
31
|
+
</svg>,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
expect(markup).not.toContain('foreignObject')
|
|
35
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { usePlacementTyping } from '../../../store/use-placement-typing'
|
|
4
|
+
import { PlacementCoordinateInput } from '../../tools/shared/placement-coordinate-input'
|
|
5
|
+
|
|
6
|
+
const HUD_WIDTH_PX = 236
|
|
7
|
+
const HUD_HEIGHT_PX = 64
|
|
8
|
+
const HUD_LIFT_PX = 14
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The typed-entry HUD placed in the plan at `x`/`y` (level-local metres).
|
|
12
|
+
* HTML inside a `foreignObject` is laid out in CSS pixels equal to SVG user
|
|
13
|
+
* units, and the plan's `<g>` is zoomed and rotated, so the box is scaled by
|
|
14
|
+
* `unitsPerPixel` and counter-rotated to stay screen-sized and upright. Both
|
|
15
|
+
* come from the caller: the wall draft renders outside `FloorplanRenderProvider`.
|
|
16
|
+
* It sits just above its anchor, and only the HUD itself takes the pointer, so
|
|
17
|
+
* clicks on the ghost or the draft under it still land on the plan.
|
|
18
|
+
*/
|
|
19
|
+
export function FloorplanPlacementTypingHud({
|
|
20
|
+
placeholders,
|
|
21
|
+
sceneRotationDeg,
|
|
22
|
+
unitsPerPixel,
|
|
23
|
+
x,
|
|
24
|
+
y,
|
|
25
|
+
}: {
|
|
26
|
+
placeholders?: readonly string[]
|
|
27
|
+
sceneRotationDeg: number
|
|
28
|
+
unitsPerPixel: number
|
|
29
|
+
x: number
|
|
30
|
+
y: number
|
|
31
|
+
}) {
|
|
32
|
+
const isActive = usePlacementTyping((state) => state.isActive)
|
|
33
|
+
const scale = Math.max(unitsPerPixel, 1e-6)
|
|
34
|
+
if (!isActive) return null
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<g transform={`translate(${x} ${y}) rotate(${-sceneRotationDeg})`}>
|
|
38
|
+
<foreignObject
|
|
39
|
+
height={HUD_HEIGHT_PX * scale}
|
|
40
|
+
overflow="visible"
|
|
41
|
+
pointerEvents="none"
|
|
42
|
+
style={{ overflow: 'visible' }}
|
|
43
|
+
width={HUD_WIDTH_PX * scale}
|
|
44
|
+
x={(-HUD_WIDTH_PX / 2) * scale}
|
|
45
|
+
y={-(HUD_HEIGHT_PX + HUD_LIFT_PX) * scale}
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
style={{
|
|
49
|
+
alignItems: 'flex-end',
|
|
50
|
+
display: 'flex',
|
|
51
|
+
height: HUD_HEIGHT_PX,
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
overflow: 'visible',
|
|
54
|
+
pointerEvents: 'none',
|
|
55
|
+
transform: `scale(${scale})`,
|
|
56
|
+
transformOrigin: 'top left',
|
|
57
|
+
width: HUD_WIDTH_PX,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<PlacementCoordinateInput placeholders={placeholders} />
|
|
61
|
+
</div>
|
|
62
|
+
</foreignObject>
|
|
63
|
+
</g>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -11,11 +11,13 @@ import { type AnyNodeDefinition, emitter, nodeRegistry, registerNode } from '@pa
|
|
|
11
11
|
import { createElement } from 'react'
|
|
12
12
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
13
13
|
import { z } from 'zod'
|
|
14
|
+
import { AXIS_MOVE_DRAG_LABEL, PATH_POINT_DRAG_LABEL } from '../../../lib/contextual-help'
|
|
14
15
|
import {
|
|
15
16
|
FLOORPLAN_NODE_EXTENSION_KEY,
|
|
16
17
|
floorplanGeometryMetadata,
|
|
17
18
|
} from '../../../lib/floorplan/floorplan-extension'
|
|
18
19
|
import type { SessionWrites } from '../../../lib/session-writes'
|
|
20
|
+
import { snapContextOf } from '../../../lib/snapping-mode'
|
|
19
21
|
import {
|
|
20
22
|
buildFloorplanEntryGeometry,
|
|
21
23
|
cancelFloorplanAffordanceDrag,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
collectFloorplanLinkedLevelNodes,
|
|
25
27
|
computeAffectedSiblingIds,
|
|
26
28
|
floorplanAffordanceReshapeScope,
|
|
29
|
+
floorplanEntryStripsHandles,
|
|
27
30
|
floorplanEntryYieldsToTool,
|
|
28
31
|
floorplanHandleDoubleClickAffordance,
|
|
29
32
|
InteractiveGeometry,
|
|
@@ -162,7 +165,6 @@ describe('site-scoped floorplan discovery', () => {
|
|
|
162
165
|
siblingEpoch: 0,
|
|
163
166
|
unit: 'metric' as const,
|
|
164
167
|
metricNotation: 'meters' as const,
|
|
165
|
-
wallDimensionReference: 'finished-faces' as const,
|
|
166
168
|
visibilityRootId: site.id,
|
|
167
169
|
}
|
|
168
170
|
const committedSnapshot = structuredClone(overlay)
|
|
@@ -276,6 +278,26 @@ describe('floorplan affordance ownership', () => {
|
|
|
276
278
|
driver: 'floorplan',
|
|
277
279
|
})
|
|
278
280
|
})
|
|
281
|
+
|
|
282
|
+
// With no scope, Select mode reads every snap mode as off, so these drags
|
|
283
|
+
// never snapped to the grid on the plan.
|
|
284
|
+
test('gives the MEP run point and segment drags a handle-drag scope in the run snap context', () => {
|
|
285
|
+
const point = floorplanAffordanceReshapeScope('move-path-point', 'duct_1', { pointIndex: 0 })
|
|
286
|
+
const segment = floorplanAffordanceReshapeScope('move-segment', 'duct_1', { segmentIndex: 0 })
|
|
287
|
+
expect(point).toEqual({ kind: 'handle-drag', nodeId: 'duct_1', handle: PATH_POINT_DRAG_LABEL })
|
|
288
|
+
expect(segment).toEqual({ kind: 'handle-drag', nodeId: 'duct_1', handle: AXIS_MOVE_DRAG_LABEL })
|
|
289
|
+
for (const scope of [point, segment]) {
|
|
290
|
+
expect(
|
|
291
|
+
snapContextOf({
|
|
292
|
+
scope: scope!,
|
|
293
|
+
mode: 'select',
|
|
294
|
+
tool: null,
|
|
295
|
+
profileOf: () => undefined,
|
|
296
|
+
profileOfNode: () => 'structural',
|
|
297
|
+
}),
|
|
298
|
+
).toBe('polygon')
|
|
299
|
+
}
|
|
300
|
+
})
|
|
279
301
|
})
|
|
280
302
|
|
|
281
303
|
function cabinetRun(id: string, children: string[] = [], parentId: string | null = 'level_test') {
|
|
@@ -793,6 +815,17 @@ describe('floorplan entry routing while a tool is active', () => {
|
|
|
793
815
|
expect(floorplanEntryYieldsToTool({ mode: 'select', openingPlacement: false })).toBe(false)
|
|
794
816
|
expect(floorplanEntryYieldsToTool({ mode: 'delete', openingPlacement: false })).toBe(false)
|
|
795
817
|
})
|
|
818
|
+
|
|
819
|
+
// A roof plane selected before arming the roof tool kept its live corner and edge handles: a
|
|
820
|
+
// click near its eave resized it instead of drawing.
|
|
821
|
+
test('a selected node shows no edit handles while a build tool is armed', () => {
|
|
822
|
+
const entry = { multiSelected: false, ceilingWithoutSession: false }
|
|
823
|
+
expect(floorplanEntryStripsHandles({ ...entry, mode: 'select' })).toBe(false)
|
|
824
|
+
expect(floorplanEntryStripsHandles({ ...entry, mode: 'build' })).toBe(true)
|
|
825
|
+
expect(floorplanEntryStripsHandles({ ...entry, mode: 'select', multiSelected: true })).toBe(
|
|
826
|
+
true,
|
|
827
|
+
)
|
|
828
|
+
})
|
|
796
829
|
})
|
|
797
830
|
|
|
798
831
|
describe('isFloorplanHierarchyVisible', () => {
|