@pascal-app/editor 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -15
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +110 -42
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +77 -68
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +31 -11
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +458 -140
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +23 -1
- package/src/components/editor/index.tsx +150 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +2 -3
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +4 -3
- package/src/components/editor/room-handles-2d.tsx +158 -7
- package/src/components/editor/room-handles.tsx +244 -54
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +33 -17
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +15 -7
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +47 -19
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
- package/src/components/ui/helpers/helper-manager.tsx +136 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
- package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +11 -10
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +9 -5
- package/src/components/ui/panels/panel-wrapper.tsx +63 -127
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +465 -66
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +22 -13
- package/src/components/ui/panels/room-panel.tsx +207 -100
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +62 -5
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15849 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +39331 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +39331 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +39331 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +39331 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +224 -45
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +101 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -2
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +15 -6
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +24 -6
- package/src/lib/measurements.ts +1 -203
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +28 -17
- package/src/lib/room-built-on.ts +21 -17
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +141 -1
- package/src/lib/room-handle-drag.ts +554 -58
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +38 -1
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +11 -5
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +21 -6
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +17 -6
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +44 -0
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +53 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-snap-indicator.ts +15 -2
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -100
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
calculateLevelMiters,
|
|
7
|
+
getLevelAbove,
|
|
8
|
+
getLevelBelow,
|
|
9
|
+
getWallPlanFootprint,
|
|
10
|
+
type LevelNode,
|
|
11
|
+
type Point2D,
|
|
12
|
+
servesAsRoofLevel,
|
|
13
|
+
useScene,
|
|
14
|
+
type WallNode,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
import { memo, useMemo } from 'react'
|
|
17
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
18
|
+
import useEditor from '../../store/use-editor'
|
|
19
|
+
import { useLevelWallCopyOffer } from '../ui/level-wall-copy-prompt'
|
|
20
|
+
|
|
21
|
+
/** Walls below read blue, walls above magenta: told apart at a glance and from the active walls. */
|
|
22
|
+
export const ADJACENT_WALL_COLORS = { below: '#2563eb', above: '#c026d3' } as const
|
|
23
|
+
|
|
24
|
+
export type AdjacentWallOutline = { id: string; points: string }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Each visible wall of `levelId` as the outline of its full thickness (both faces, joined at the
|
|
28
|
+
* corners as the level draws them), in plan metres.
|
|
29
|
+
*/
|
|
30
|
+
export function adjacentWallOutlines(
|
|
31
|
+
nodes: Record<string, AnyNode>,
|
|
32
|
+
levelId: string | null | undefined,
|
|
33
|
+
): AdjacentWallOutline[] {
|
|
34
|
+
if (!levelId) return []
|
|
35
|
+
const walls = Object.values(nodes).filter(
|
|
36
|
+
(node): node is WallNode =>
|
|
37
|
+
node.type === 'wall' && node.parentId === levelId && node.visible !== false,
|
|
38
|
+
)
|
|
39
|
+
if (!walls.length) return []
|
|
40
|
+
const miters = calculateLevelMiters(walls)
|
|
41
|
+
return walls.flatMap((wall) => {
|
|
42
|
+
const polygon: Point2D[] = getWallPlanFootprint(wall, miters)
|
|
43
|
+
return polygon.length >= 3
|
|
44
|
+
? [{ id: wall.id, points: polygon.map((point) => `${point.x},${point.y}`).join(' ') }]
|
|
45
|
+
: []
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The levels directly below and above `levelId` in its building's stack (by ordinal, as the
|
|
51
|
+
* storey stack orders them): on a roof level, "below" is the top storey, the walls the roof bears
|
|
52
|
+
* on. Reads only the building's levels, so a plan selector can call it on every scene change.
|
|
53
|
+
*/
|
|
54
|
+
export function adjacentLevelIds(
|
|
55
|
+
nodes: Record<string, AnyNode>,
|
|
56
|
+
levelId: string | null | undefined,
|
|
57
|
+
): { below: string | null; above: string | null } {
|
|
58
|
+
const level = levelId ? nodes[levelId] : undefined
|
|
59
|
+
if (level?.type !== 'level') return { below: null, above: null }
|
|
60
|
+
const building = level.parentId ? nodes[level.parentId] : undefined
|
|
61
|
+
if (building?.type !== 'building') {
|
|
62
|
+
const graph = nodes as Record<AnyNodeId, AnyNode>
|
|
63
|
+
return {
|
|
64
|
+
below: getLevelBelow(level.id, graph)?.id ?? null,
|
|
65
|
+
above: getLevelAbove(level.id, graph)?.id ?? null,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let below: LevelNode | null = null
|
|
69
|
+
let above: LevelNode | null = null
|
|
70
|
+
for (const id of building.children) {
|
|
71
|
+
const other = nodes[id]
|
|
72
|
+
if (other?.type !== 'level' || other.id === level.id) continue
|
|
73
|
+
if (other.level < level.level && (!below || other.level > below.level)) below = other
|
|
74
|
+
if (other.level > level.level && (!above || other.level < above.level)) above = other
|
|
75
|
+
}
|
|
76
|
+
return { below: below?.id ?? null, above: above?.id ?? null }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The adjacent levels whose walls the plan traces: each only while it is shown. */
|
|
80
|
+
export function adjacentWallLevels(
|
|
81
|
+
nodes: Record<string, AnyNode>,
|
|
82
|
+
levelId: string | null | undefined,
|
|
83
|
+
show: { below: boolean; above: boolean },
|
|
84
|
+
): { below: string | null; above: string | null } {
|
|
85
|
+
const levels = adjacentLevelIds(nodes, levelId)
|
|
86
|
+
return { below: show.below ? levels.below : null, above: show.above ? levels.above : null }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The "Walls below" setting that applies on `levelId`: the roof plan's, on until turned off, on a
|
|
91
|
+
* roof level; the floor plans', off until turned on, on a storey.
|
|
92
|
+
*/
|
|
93
|
+
export function useWallsBelowSetting(levelId: string | null | undefined): {
|
|
94
|
+
onRoofLevel: boolean
|
|
95
|
+
shown: boolean
|
|
96
|
+
setShown: (show: boolean) => void
|
|
97
|
+
} {
|
|
98
|
+
const onRoofLevel = useScene((state) => {
|
|
99
|
+
const level = levelId ? state.nodes[levelId as AnyNodeId] : undefined
|
|
100
|
+
return level?.type === 'level' && servesAsRoofLevel(state.nodes, level)
|
|
101
|
+
})
|
|
102
|
+
const shown = useEditor((state) =>
|
|
103
|
+
onRoofLevel ? state.showRoofWallsBelow : state.showWallsBelow,
|
|
104
|
+
)
|
|
105
|
+
const setRoof = useEditor((state) => state.setShowRoofWallsBelow)
|
|
106
|
+
const setFloor = useEditor((state) => state.setShowWallsBelow)
|
|
107
|
+
return { onRoofLevel, shown, setShown: onRoofLevel ? setRoof : setFloor }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The level's own walls, read through its children rather than the whole scene. */
|
|
111
|
+
function useLevelWalls(levelId: string | null) {
|
|
112
|
+
return useScene(
|
|
113
|
+
useShallow((state) => {
|
|
114
|
+
const level = levelId ? state.nodes[levelId as AnyNodeId] : undefined
|
|
115
|
+
if (level?.type !== 'level') return [] as WallNode[]
|
|
116
|
+
return level.children.flatMap((id) => {
|
|
117
|
+
const node = state.nodes[id as AnyNodeId]
|
|
118
|
+
return node?.type === 'wall' ? [node] : []
|
|
119
|
+
})
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Outlines({ color, outlines }: { color: string; outlines: AdjacentWallOutline[] }) {
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
{outlines.map((outline) => (
|
|
128
|
+
<polygon
|
|
129
|
+
fill={color}
|
|
130
|
+
fillOpacity={0.05}
|
|
131
|
+
key={outline.id}
|
|
132
|
+
points={outline.points}
|
|
133
|
+
stroke={color}
|
|
134
|
+
strokeDasharray="7 5"
|
|
135
|
+
strokeLinejoin="miter"
|
|
136
|
+
strokeOpacity={0.9}
|
|
137
|
+
strokeWidth={1.4}
|
|
138
|
+
vectorEffect="non-scaling-stroke"
|
|
139
|
+
/>
|
|
140
|
+
))}
|
|
141
|
+
</>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* "Walls below / above": dashed outlines of the walls on the neighbouring levels, drawn over the
|
|
147
|
+
* active level's plan so second-storey walls line up with the ones under them and a roof outline
|
|
148
|
+
* can be checked against the walls it covers — while its toggle is on, or while a new level's
|
|
149
|
+
* "Copy exterior walls?" offer is open. Display only: never hit-tested, never snapped to.
|
|
150
|
+
*/
|
|
151
|
+
export const FloorplanAdjacentWallsLayer = memo(function FloorplanAdjacentWallsLayer({
|
|
152
|
+
levelId,
|
|
153
|
+
}: {
|
|
154
|
+
levelId: string | null | undefined
|
|
155
|
+
}) {
|
|
156
|
+
const offer = useLevelWallCopyOffer((state) =>
|
|
157
|
+
state.offer && state.offer.levelId === levelId ? state.offer.direction : null,
|
|
158
|
+
)
|
|
159
|
+
const showBelow = useWallsBelowSetting(levelId).shown || offer === 'below'
|
|
160
|
+
const showAbove = useEditor((state) => state.showWallsAbove) || offer === 'above'
|
|
161
|
+
const levels = useScene(
|
|
162
|
+
useShallow((state) =>
|
|
163
|
+
adjacentWallLevels(state.nodes as Record<string, AnyNode>, levelId, {
|
|
164
|
+
below: showBelow,
|
|
165
|
+
above: showAbove,
|
|
166
|
+
}),
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
const belowWalls = useLevelWalls(levels.below)
|
|
170
|
+
const aboveWalls = useLevelWalls(levels.above)
|
|
171
|
+
const below = useMemo(
|
|
172
|
+
() => adjacentWallOutlines(Object.fromEntries(belowWalls.map((w) => [w.id, w])), levels.below),
|
|
173
|
+
[belowWalls, levels.below],
|
|
174
|
+
)
|
|
175
|
+
const above = useMemo(
|
|
176
|
+
() => adjacentWallOutlines(Object.fromEntries(aboveWalls.map((w) => [w.id, w])), levels.above),
|
|
177
|
+
[aboveWalls, levels.above],
|
|
178
|
+
)
|
|
179
|
+
if (!(below.length || above.length)) return null
|
|
180
|
+
return (
|
|
181
|
+
<g data-adjacent-walls pointerEvents="none">
|
|
182
|
+
<Outlines color={ADJACENT_WALL_COLORS.below} outlines={below} />
|
|
183
|
+
<Outlines color={ADJACENT_WALL_COLORS.above} outlines={above} />
|
|
184
|
+
</g>
|
|
185
|
+
)
|
|
186
|
+
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
4
|
import { memo } from 'react'
|
|
5
|
-
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
5
|
+
import useAlignmentGuides, { type DraftGuideLine } from '../../store/use-alignment-guides'
|
|
6
6
|
import { formatMeasurement } from '../editor/measurement-pill'
|
|
7
7
|
import { useFloorplanRender } from './floorplan-render-context'
|
|
8
8
|
|
|
@@ -24,16 +24,29 @@ import { useFloorplanRender } from './floorplan-render-context'
|
|
|
24
24
|
*
|
|
25
25
|
* Stroke widths and handle radii are scaled by `unitsPerPixel` so they
|
|
26
26
|
* stay a constant size on screen no matter the zoom.
|
|
27
|
+
*
|
|
28
|
+
* A wall pulled from a wall end (and the run it continues) publishes its
|
|
29
|
+
* projection guides beside them (`useAlignmentGuides.draft`): dashed lines —
|
|
30
|
+
* the extension of the wall it comes from in amber, lines through other walls'
|
|
31
|
+
* ends in indigo, lines through the run's start in emerald — and a ringed ✕
|
|
32
|
+
* where two of them cross, matching the 3D layer.
|
|
27
33
|
*/
|
|
28
|
-
export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuideLayer(
|
|
34
|
+
export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuideLayer({
|
|
35
|
+
unitsPerPixel,
|
|
36
|
+
}: {
|
|
37
|
+
/** The plan's live scale, which keeps a wall draft's guides screen-sized. */
|
|
38
|
+
unitsPerPixel?: number
|
|
39
|
+
}) {
|
|
29
40
|
const guides = useAlignmentGuides((s) => s.guides)
|
|
41
|
+
const draft = useAlignmentGuides((s) => s.draft)
|
|
30
42
|
const unit = useViewer((s) => s.unit)
|
|
31
43
|
const metricNotation = useViewer((s) => s.metricNotation)
|
|
32
44
|
const ctx = useFloorplanRender()
|
|
33
45
|
|
|
34
|
-
if (guides.length === 0) return null
|
|
46
|
+
if (guides.length === 0 && !draft) return null
|
|
35
47
|
|
|
36
48
|
const upp = ctx?.unitsPerPixel ?? 0.01
|
|
49
|
+
const draftUpp = unitsPerPixel ?? upp
|
|
37
50
|
const sceneRot = ctx?.sceneRotationDeg ?? 0
|
|
38
51
|
|
|
39
52
|
// Pixel-budgeted sizes converted to world meters so visuals stay
|
|
@@ -50,6 +63,47 @@ export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuid
|
|
|
50
63
|
|
|
51
64
|
return (
|
|
52
65
|
<g pointerEvents="none">
|
|
66
|
+
{draft?.lines.map((line, i) => (
|
|
67
|
+
<line
|
|
68
|
+
key={`draft-${i}`}
|
|
69
|
+
stroke={DRAFT_GUIDE_COLORS[line.kind]}
|
|
70
|
+
strokeDasharray={`${6 * draftUpp} ${4 * draftUpp}`}
|
|
71
|
+
strokeLinecap="round"
|
|
72
|
+
strokeWidth={1.5 * draftUpp}
|
|
73
|
+
x1={line.from[0]}
|
|
74
|
+
x2={line.to[0]}
|
|
75
|
+
y1={line.from[1]}
|
|
76
|
+
y2={line.to[1]}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
{draft?.intersections.map((point, i) => (
|
|
80
|
+
<g
|
|
81
|
+
key={`draft-x-${i}`}
|
|
82
|
+
stroke={DRAFT_GUIDE_COLORS.alignment}
|
|
83
|
+
strokeLinecap="round"
|
|
84
|
+
strokeWidth={1.5 * draftUpp}
|
|
85
|
+
>
|
|
86
|
+
<circle
|
|
87
|
+
cx={point[0]}
|
|
88
|
+
cy={point[1]}
|
|
89
|
+
fill="#ffffff"
|
|
90
|
+
fillOpacity={0.85}
|
|
91
|
+
r={5.5 * draftUpp}
|
|
92
|
+
/>
|
|
93
|
+
<line
|
|
94
|
+
x1={point[0] - 3 * draftUpp}
|
|
95
|
+
x2={point[0] + 3 * draftUpp}
|
|
96
|
+
y1={point[1] - 3 * draftUpp}
|
|
97
|
+
y2={point[1] + 3 * draftUpp}
|
|
98
|
+
/>
|
|
99
|
+
<line
|
|
100
|
+
x1={point[0] - 3 * draftUpp}
|
|
101
|
+
x2={point[0] + 3 * draftUpp}
|
|
102
|
+
y1={point[1] + 3 * draftUpp}
|
|
103
|
+
y2={point[1] - 3 * draftUpp}
|
|
104
|
+
/>
|
|
105
|
+
</g>
|
|
106
|
+
))}
|
|
53
107
|
{guides.map((guide, i) => {
|
|
54
108
|
const { from, to, axis } = guide
|
|
55
109
|
const midX = (from.x + to.x) / 2
|
|
@@ -107,6 +161,12 @@ export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuid
|
|
|
107
161
|
)
|
|
108
162
|
})
|
|
109
163
|
|
|
164
|
+
const DRAFT_GUIDE_COLORS: Record<DraftGuideLine['kind'], string> = {
|
|
165
|
+
extension: '#f59e0b', // amber-500
|
|
166
|
+
alignment: '#6366f1', // indigo-500 — the snap marker's colour
|
|
167
|
+
'run-start': '#10b981', // emerald-500 — the pull grip's colour
|
|
168
|
+
}
|
|
169
|
+
|
|
110
170
|
function XCap({
|
|
111
171
|
color,
|
|
112
172
|
size,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DEFAULT_ANGLE_STEP,
|
|
9
9
|
type FloorplanGeometry,
|
|
10
10
|
type FloorplanPalette,
|
|
11
|
+
formatLinearMeasurement,
|
|
11
12
|
pauseSceneHistory,
|
|
12
13
|
pauseSpaceDetection,
|
|
13
14
|
resumeSceneHistory,
|
|
@@ -23,7 +24,8 @@ import { GROUP_MOVE_DRAG_LABEL, GROUP_ROTATE_DRAG_LABEL } from '../../lib/contex
|
|
|
23
24
|
import { applyFloorplanAlignment } from '../../lib/floorplan/apply-alignment'
|
|
24
25
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
25
26
|
import { isHistoryShortcut } from '../../lib/history'
|
|
26
|
-
import {
|
|
27
|
+
import type { WallGroupPush } from '../../lib/room-handle-drag'
|
|
28
|
+
import { useRoomHandleDrag } from '../../lib/room-handle-drag'
|
|
27
29
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
28
30
|
import { isTypingTarget } from '../../lib/typing-target'
|
|
29
31
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
@@ -36,6 +38,7 @@ import useEditor, {
|
|
|
36
38
|
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
37
39
|
import useInteractionScope, { useMovingNode } from '../../store/use-interaction-scope'
|
|
38
40
|
import {
|
|
41
|
+
beginGroupWallPush,
|
|
39
42
|
classifyParticipant,
|
|
40
43
|
collectParticipants,
|
|
41
44
|
computeGroupPlanBox,
|
|
@@ -120,6 +123,8 @@ export function startFloorplanGroupMove(
|
|
|
120
123
|
rotation: number
|
|
121
124
|
restCenter: Vec2
|
|
122
125
|
lastDelta: Vec2 | null
|
|
126
|
+
/** Walls alone: each moves as its push arrow does instead of the rigid slide. */
|
|
127
|
+
push: WallGroupPush | null
|
|
123
128
|
}
|
|
124
129
|
let session: Session | null = null
|
|
125
130
|
|
|
@@ -185,6 +190,7 @@ export function startFloorplanGroupMove(
|
|
|
185
190
|
rotation: 0,
|
|
186
191
|
restCenter,
|
|
187
192
|
lastDelta: null,
|
|
193
|
+
push: beginGroupWallPush(starts, levelId),
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
196
|
|
|
@@ -201,8 +207,14 @@ export function startFloorplanGroupMove(
|
|
|
201
207
|
let dz = snap ? Math.round((plan[1] - startPlan[1]) / step) * step : plan[1] - startPlan[1]
|
|
202
208
|
|
|
203
209
|
// Figma-style alignment layered on top: guides display in every mode
|
|
204
|
-
// except Off; the magnetic pull applies only in 'lines' mode.
|
|
205
|
-
|
|
210
|
+
// except Off; the magnetic pull applies only in 'lines' mode. Pushed walls
|
|
211
|
+
// don't move as one footprint, so they have no box to align.
|
|
212
|
+
if (
|
|
213
|
+
!s.push &&
|
|
214
|
+
isAlignmentGuideActive() &&
|
|
215
|
+
s.candidates.length > 0 &&
|
|
216
|
+
s.restAnchors.length > 0
|
|
217
|
+
) {
|
|
206
218
|
const proposed: [number, number] = [startPlan[0] + dx, startPlan[1] + dz]
|
|
207
219
|
const aligned = applyFloorplanAlignment(
|
|
208
220
|
proposed,
|
|
@@ -225,6 +237,10 @@ export function startFloorplanGroupMove(
|
|
|
225
237
|
sfxEmitter.emit('sfx:grid-snap')
|
|
226
238
|
s.lastDelta = [dx, dz]
|
|
227
239
|
}
|
|
240
|
+
if (s.push) {
|
|
241
|
+
s.push.preview([dx, dz])
|
|
242
|
+
return
|
|
243
|
+
}
|
|
228
244
|
|
|
229
245
|
const entries = translateGroupPatches(s.starts, s.links, dx, dz)
|
|
230
246
|
const patchById = new Map(entries)
|
|
@@ -252,6 +268,8 @@ export function startFloorplanGroupMove(
|
|
|
252
268
|
// current delta — the carried group turns exactly like the idle keyboard
|
|
253
269
|
// rotate, and the commit stays a single updateNodes.
|
|
254
270
|
const rotateSession = (s: Session, direction: 1 | -1) => {
|
|
271
|
+
// Pushed walls keep their bearing: there is nothing to turn.
|
|
272
|
+
if (s.push) return
|
|
255
273
|
const pivot = { x: s.restCenter[0], z: s.restCenter[1] }
|
|
256
274
|
const delta = -direction * (Math.PI / 4)
|
|
257
275
|
const rotated = rotateGroupSnapshots(s.starts, s.links, pivot, delta)
|
|
@@ -271,6 +289,7 @@ export function startFloorplanGroupMove(
|
|
|
271
289
|
}
|
|
272
290
|
|
|
273
291
|
const clearLivePreviews = (s: Session) => {
|
|
292
|
+
s.push?.cancel()
|
|
274
293
|
const overrides = useLiveNodeOverrides.getState()
|
|
275
294
|
const liveTransforms = useLiveTransforms.getState()
|
|
276
295
|
for (const id of s.affectedIds) {
|
|
@@ -330,6 +349,13 @@ export function startFloorplanGroupMove(
|
|
|
330
349
|
// doesn't clear the multi-selection the drag is preserving.
|
|
331
350
|
swallowNextClick()
|
|
332
351
|
sfxEmitter.emit('sfx:item-place')
|
|
352
|
+
if (session.push) {
|
|
353
|
+
resumeSceneHistory(useScene)
|
|
354
|
+
session.push.commit()
|
|
355
|
+
session = null
|
|
356
|
+
teardown()
|
|
357
|
+
return
|
|
358
|
+
}
|
|
333
359
|
const overrides = useLiveNodeOverrides.getState()
|
|
334
360
|
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = []
|
|
335
361
|
for (const id of session.affectedIds) {
|
|
@@ -632,6 +658,8 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
632
658
|
const delta = useFloorplanGroupDrag((s) => s.delta)
|
|
633
659
|
const liveRotation = useFloorplanGroupDrag((s) => s.rotation)
|
|
634
660
|
const movingNode = useMovingNode()
|
|
661
|
+
// Pushed walls don't move as one box: the room widths they show take its place.
|
|
662
|
+
const wallPush = useRoomHandleDrag((s) => s.drag?.kind === 'wall-group-push')
|
|
635
663
|
const mode = useEditor((s) => s.mode)
|
|
636
664
|
const floorplanMode = useFloorplanMode((s) => s.mode)
|
|
637
665
|
const sceneRotationDeg = useFloorplanSceneRotation()
|
|
@@ -673,7 +701,7 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
673
701
|
}
|
|
674
702
|
}, [selectedIds, levelId, nodes, meshEpoch])
|
|
675
703
|
|
|
676
|
-
if (!box || movingNode || mode === 'delete') return null
|
|
704
|
+
if (!box || movingNode || wallPush || mode === 'delete') return null
|
|
677
705
|
|
|
678
706
|
const pad = 6 * unitsPerPixel
|
|
679
707
|
const stroke = palette?.selectedStroke ?? '#3b82f6'
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
+
formatMeasurementInputValue,
|
|
3
4
|
isProjectedFloorplanAxisPointVerified,
|
|
4
5
|
resolveFloorplanMeasurementAxisSnap,
|
|
5
6
|
resolveProjectedFloorplanSnap,
|
|
6
7
|
} from './floorplan-measurement-tool-layer'
|
|
7
8
|
|
|
9
|
+
test('extrusion inputs write values back with the scoped decimal separator', () => {
|
|
10
|
+
expect(formatMeasurementInputValue(2.75, 'fr')).toBe('2,75')
|
|
11
|
+
expect(formatMeasurementInputValue(2.75, 'en')).toBe('2.75')
|
|
12
|
+
})
|
|
13
|
+
|
|
8
14
|
describe('resolveFloorplanMeasurementAxisSnap', () => {
|
|
9
15
|
test('snaps to the closest X or Z axis inside the screen-space threshold', () => {
|
|
10
16
|
expect(resolveFloorplanMeasurementAxisSnap([3, 0, 2], [1, 0, 1], 4, 9)).toEqual({
|
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
+
buildMeasurementAngleArcPoints,
|
|
6
7
|
closestMeasurementFeatureBinding,
|
|
7
8
|
collectAlignmentAnchors,
|
|
8
9
|
emitter,
|
|
10
|
+
formatAngleRadians,
|
|
11
|
+
formatAreaLabel,
|
|
12
|
+
formatLinearMeasurement,
|
|
13
|
+
formatNumber,
|
|
14
|
+
formatVolumeLabel,
|
|
9
15
|
type GeometryContext,
|
|
16
|
+
getLinearUnitLabel,
|
|
10
17
|
type MeasurementFeatureAnchor,
|
|
11
18
|
type MeasurementSnapKind,
|
|
12
19
|
measurementAngle,
|
|
@@ -15,22 +22,18 @@ import {
|
|
|
15
22
|
measurementFeatureLength,
|
|
16
23
|
measurementPerimeter,
|
|
17
24
|
measurementPrismVolume,
|
|
25
|
+
metersToLinearUnit,
|
|
18
26
|
nodeRegistry,
|
|
27
|
+
parseLinearMeasurementInput,
|
|
28
|
+
useLocale,
|
|
19
29
|
useScene,
|
|
20
30
|
} from '@pascal-app/core'
|
|
21
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
22
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
23
|
-
import { measurementPolygonLabelAnchor } from '../../lib/measurement-label'
|
|
24
31
|
import {
|
|
25
|
-
buildMeasurementAngleArcPoints,
|
|
26
|
-
formatAreaLabel,
|
|
27
|
-
formatLinearMeasurement,
|
|
28
|
-
formatVolumeLabel,
|
|
29
|
-
getLinearUnitLabel,
|
|
30
|
-
linearUnitToMeters,
|
|
31
32
|
MEASUREMENT_ACTIVE_COLOR,
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
measurementPolygonLabelAnchor,
|
|
34
|
+
useViewer,
|
|
35
|
+
} from '@pascal-app/viewer'
|
|
36
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
34
37
|
import {
|
|
35
38
|
clearSurfacePlanSnapFeedback,
|
|
36
39
|
resolveSurfacePlanPointSnap,
|
|
@@ -43,7 +46,6 @@ import {
|
|
|
43
46
|
measurementPolygonMidpoints,
|
|
44
47
|
useMeasurementDraft,
|
|
45
48
|
} from '../../store/use-measurement-draft'
|
|
46
|
-
import { formatAngleRadians } from '../tools/shared/segment-angle'
|
|
47
49
|
import { FloorplanQuickMeasureLayer } from './floorplan-quick-measure-layer'
|
|
48
50
|
import { useFloorplanRender } from './floorplan-render-context'
|
|
49
51
|
import { resolveFloorplanLabelAngle } from './renderers/floorplan-label-angle'
|
|
@@ -77,6 +79,10 @@ export type ProjectedFloorplanSnap = {
|
|
|
77
79
|
const MAX_REGISTERED_SNAP_ELEMENTS = 512
|
|
78
80
|
const MAX_REGISTERED_SNAP_SEGMENTS = 4096
|
|
79
81
|
const MAX_SAMPLED_PATH_SEGMENTS = 48
|
|
82
|
+
|
|
83
|
+
export function formatMeasurementInputValue(value: number, locale: string): string {
|
|
84
|
+
return formatNumber(value, { locale, max: 3 })
|
|
85
|
+
}
|
|
80
86
|
const VERTEX_SNAP_DISTANCE_PX = 16
|
|
81
87
|
const EDGE_SNAP_DISTANCE_PX = 10
|
|
82
88
|
const SEMANTIC_FEATURE_SNAP_DISTANCE = 0.2
|
|
@@ -636,17 +642,22 @@ function FloorplanExtrusionControl({
|
|
|
636
642
|
unitsPerPixel: number
|
|
637
643
|
}) {
|
|
638
644
|
const unit = useViewer((state) => state.unit)
|
|
645
|
+
const locale = useLocale()
|
|
639
646
|
const extrusionHeight = useMeasurementDraft((state) => state.extrusionHeight)
|
|
640
647
|
const points = useMeasurementDraft((state) => state.points)
|
|
641
648
|
const baseNormal = useMeasurementDraft((state) => state.baseNormal)
|
|
642
649
|
const [value, setValue] = useState(() =>
|
|
643
|
-
extrusionHeight === 0
|
|
650
|
+
extrusionHeight === 0
|
|
651
|
+
? ''
|
|
652
|
+
: formatMeasurementInputValue(metersToLinearUnit(extrusionHeight, unit), locale),
|
|
644
653
|
)
|
|
645
654
|
|
|
646
655
|
useEffect(() => {
|
|
647
656
|
const height = useMeasurementDraft.getState().extrusionHeight
|
|
648
|
-
setValue(
|
|
649
|
-
|
|
657
|
+
setValue(
|
|
658
|
+
height === 0 ? '' : formatMeasurementInputValue(metersToLinearUnit(height, unit), locale),
|
|
659
|
+
)
|
|
660
|
+
}, [locale, unit])
|
|
650
661
|
|
|
651
662
|
const width = 260 * unitsPerPixel
|
|
652
663
|
const height = 72 * unitsPerPixel
|
|
@@ -654,12 +665,12 @@ function FloorplanExtrusionControl({
|
|
|
654
665
|
const y = center[2] - height / 2
|
|
655
666
|
|
|
656
667
|
const commit = () => {
|
|
657
|
-
const
|
|
658
|
-
if (
|
|
668
|
+
const meters = parseLinearMeasurementInput(value, unit, locale)
|
|
669
|
+
if (meters === null) {
|
|
659
670
|
useMeasurementDraft.getState().setExtrusionHeight('2d', 0)
|
|
660
671
|
return
|
|
661
672
|
}
|
|
662
|
-
useMeasurementDraft.getState().setExtrusionHeight('2d',
|
|
673
|
+
useMeasurementDraft.getState().setExtrusionHeight('2d', meters)
|
|
663
674
|
const draft = useMeasurementDraft.getState()
|
|
664
675
|
if (draft.finishExtrusion('2d')) commitMeasurementDraft('2d')
|
|
665
676
|
}
|
|
@@ -710,11 +721,9 @@ function FloorplanExtrusionControl({
|
|
|
710
721
|
onChange={(event) => {
|
|
711
722
|
const next = event.target.value
|
|
712
723
|
setValue(next)
|
|
713
|
-
const
|
|
714
|
-
if (
|
|
715
|
-
useMeasurementDraft
|
|
716
|
-
.getState()
|
|
717
|
-
.setExtrusionHeight('2d', linearUnitToMeters(numericValue, unit))
|
|
724
|
+
const meters = parseLinearMeasurementInput(next, unit, locale)
|
|
725
|
+
if (meters !== null) {
|
|
726
|
+
useMeasurementDraft.getState().setExtrusionHeight('2d', meters)
|
|
718
727
|
} else {
|
|
719
728
|
useMeasurementDraft.getState().setExtrusionHeight('2d', 0)
|
|
720
729
|
}
|
|
@@ -728,7 +737,7 @@ function FloorplanExtrusionControl({
|
|
|
728
737
|
}}
|
|
729
738
|
placeholder="0"
|
|
730
739
|
step="0.1"
|
|
731
|
-
type="
|
|
740
|
+
type="text"
|
|
732
741
|
value={value}
|
|
733
742
|
/>
|
|
734
743
|
<span className="pointer-events-none absolute inset-y-0 right-2 flex items-center text-muted-foreground text-xs">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import type { OpenWallEnd } from '@pascal-app/core'
|
|
4
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import type { AnyNodeId, OpenWallEnd } from '@pascal-app/core'
|
|
4
|
+
import { MEASUREMENT_DANGLING_COLOR, useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { memo, useEffect, useState } from 'react'
|
|
6
6
|
import { createPortal } from 'react-dom'
|
|
7
7
|
import {
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
useOpenWallEndsSuppressed,
|
|
11
11
|
} from '../../hooks/use-open-wall-ends'
|
|
12
12
|
import { openWallEndKey, openWallEndLabel } from '../../lib/floorplan/open-wall-ends'
|
|
13
|
-
import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
|
|
14
13
|
import useEditor from '../../store/use-editor'
|
|
15
14
|
import { isViewVisible, VIEW_2D } from '../../store/view-layout'
|
|
16
15
|
import { JoinWallsPill } from '../editor/join-walls-pill'
|
|
@@ -31,7 +30,8 @@ function sceneScreenPoint(point: [number, number]): { left: number; top: number
|
|
|
31
30
|
* Shows where walls look joined but are not, so a room can't close: a red dot
|
|
32
31
|
* on each open end, a dashed line to the wall it nearly meets with what is
|
|
33
32
|
* wrong ("4 cm gap"), and — while drawing — the walls that bound no room dimmed. Hover or click a
|
|
34
|
-
* dot for "Join walls", which applies core's join planner as one undo step
|
|
33
|
+
* dot for "Join walls", which applies core's join planner as one undo step; a
|
|
34
|
+
* click on a dot also selects its wall.
|
|
35
35
|
*
|
|
36
36
|
* On while walls or rooms are being drawn (the wall tool's variants, Divide);
|
|
37
37
|
* otherwise only when a wall that bounds no room has a near miss.
|
|
@@ -76,7 +76,14 @@ const ActiveOpenWallEndsLayer = memo(function ActiveOpenWallEndsLayer() {
|
|
|
76
76
|
label={openWallEndLabel(end, unit)}
|
|
77
77
|
onHoverEnd={focus.releaseHover}
|
|
78
78
|
onHoverStart={() => focus.keepHover(key)}
|
|
79
|
-
onPin={() =>
|
|
79
|
+
onPin={() => {
|
|
80
|
+
focus.togglePin(key)
|
|
81
|
+
// The dot can cover all of a wall a few centimetres long: its
|
|
82
|
+
// click selects that wall too, so it can be fixed or deleted.
|
|
83
|
+
if (!drawing && useEditor.getState().mode === 'select') {
|
|
84
|
+
useViewer.getState().setSelection({ selectedIds: [end.wallId as AnyNodeId] })
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
80
87
|
sceneRotationDeg={sceneRotationDeg}
|
|
81
88
|
unitsPerPixel={unitsPerPixel}
|
|
82
89
|
/>
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
collectionIdsOf,
|
|
8
8
|
createSceneApi,
|
|
9
9
|
getWallMidpointHandlePoint,
|
|
10
|
+
isNodeDuplicable,
|
|
10
11
|
type NodeQuickAction,
|
|
11
12
|
nodeRegistry,
|
|
12
13
|
runAsSingleSceneHistoryStep,
|
|
@@ -255,7 +256,7 @@ export function FloorplanRegistryActionMenu() {
|
|
|
255
256
|
const canMove =
|
|
256
257
|
!registryMoveDisabled(node) &&
|
|
257
258
|
(!!def.capabilities.movable || !!def.floorplanMoveTarget || !!def.affordanceTools?.move)
|
|
258
|
-
const canDuplicate =
|
|
259
|
+
const canDuplicate = isNodeDuplicable(node)
|
|
259
260
|
const canDelete = def.capabilities.deletable !== false
|
|
260
261
|
const canAddHole = node.type === 'slab' || node.type === 'ceiling'
|
|
261
262
|
|