@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
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
type WallNode,
|
|
20
20
|
} from '@pascal-app/core'
|
|
21
21
|
import { useViewer } from '@pascal-app/viewer'
|
|
22
|
-
import { Html } from '@react-three/drei'
|
|
23
22
|
import { createPortal, type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
|
|
24
23
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
25
24
|
import {
|
|
@@ -61,6 +60,7 @@ import useInteractionScope, {
|
|
|
61
60
|
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
62
61
|
import { resolveResizeSnapValue } from './handles/resize-snap'
|
|
63
62
|
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
63
|
+
import { Html } from './localized-html'
|
|
64
64
|
import { MeasurementPill } from './measurement-pill'
|
|
65
65
|
import {
|
|
66
66
|
createArrowHitAreaGeometry,
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
NO_RAYCAST,
|
|
71
71
|
useInvisibleHitAreaMaterial,
|
|
72
72
|
} from './node-arrow-handles'
|
|
73
|
+
import { WallPullGrip } from './wall-pull-grip'
|
|
73
74
|
import { WallPushArrow } from './wall-push-arrow'
|
|
74
75
|
|
|
75
76
|
const HANDLE_OFFSET = 0.27
|
|
@@ -251,6 +252,9 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
251
252
|
/>
|
|
252
253
|
<WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
|
|
253
254
|
<WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
|
|
255
|
+
{/* Past each end, the grip that pulls the next wall out of it. */}
|
|
256
|
+
<WallPullGrip endpoint="start" wall={effectiveWall} />
|
|
257
|
+
<WallPullGrip endpoint="end" wall={effectiveWall} />
|
|
254
258
|
</group>
|
|
255
259
|
<WallBaseElevationHandle
|
|
256
260
|
baseElevation={baseElevation}
|
|
@@ -297,7 +301,7 @@ function StructureThicknessHandle({
|
|
|
297
301
|
const structureHeight =
|
|
298
302
|
node.type === 'fence'
|
|
299
303
|
? (node.height ?? 1.8)
|
|
300
|
-
: getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
|
|
304
|
+
: getWallEffectiveHeightForNodes(node, useScene.getState().nodes, 0.5)
|
|
301
305
|
const outward = new Vector2(frame.normal.x * side, frame.normal.y * side)
|
|
302
306
|
const faceOffset = getWallLocalFaceZ({ ...node, thickness }, side > 0 ? 'a' : 'b') * side + 0.006
|
|
303
307
|
const position: [number, number, number] = [
|
|
@@ -371,7 +375,7 @@ function StructureThicknessHandle({
|
|
|
371
375
|
const initialStructureHeight =
|
|
372
376
|
initialNode.type === 'fence'
|
|
373
377
|
? (initialNode.height ?? 1.8)
|
|
374
|
-
: getWallEffectiveHeightForNodes(initialNode, useScene.getState().nodes)
|
|
378
|
+
: getWallEffectiveHeightForNodes(initialNode, useScene.getState().nodes, 0.5)
|
|
375
379
|
const initialStructureThickness =
|
|
376
380
|
initialNode.type === 'fence'
|
|
377
381
|
? (initialNode.thickness ?? 0.08)
|
|
@@ -574,7 +578,11 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
|
|
|
574
578
|
const corner = endpoint === 'start' ? wall.start : wall.end
|
|
575
579
|
const x = corner[0]
|
|
576
580
|
const z = corner[1]
|
|
577
|
-
const wallHeight = getWallEffectiveHeightForNodes(
|
|
581
|
+
const wallHeight = getWallEffectiveHeightForNodes(
|
|
582
|
+
wall,
|
|
583
|
+
useScene.getState().nodes,
|
|
584
|
+
endpoint === 'start' ? 0 : 1,
|
|
585
|
+
)
|
|
578
586
|
|
|
579
587
|
const dashedGeometry = useMemo(() => buildDashedVerticalGeometry(wallHeight), [wallHeight])
|
|
580
588
|
const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(CORNER_HEX_RADIUS), [])
|
|
@@ -780,7 +788,7 @@ function WallBaseElevationHandle({
|
|
|
780
788
|
levelObject.updateWorldMatrix(true, false)
|
|
781
789
|
const initialBase = getWallBaseElevationForNodes(initialNode, sceneApi.nodes())
|
|
782
790
|
const supportBase = initialBase - (initialNode.supportOffset ?? 0)
|
|
783
|
-
const initialHeight = getWallEffectiveHeightForNodes(initialNode, sceneApi.nodes())
|
|
791
|
+
const initialHeight = getWallEffectiveHeightForNodes(initialNode, sceneApi.nodes(), 0)
|
|
784
792
|
const midpointWorld = new Vector3(midpoint[0], initialBase, midpoint[1]).applyMatrix4(
|
|
785
793
|
levelObject.matrixWorld,
|
|
786
794
|
)
|
|
@@ -916,7 +924,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
916
924
|
const wallAngle = Math.atan2(-dirZ, dirX)
|
|
917
925
|
// `wall` is the override-merged effective wall (see
|
|
918
926
|
// WallMoveSideHandlesForWall), so this height is already live during a drag.
|
|
919
|
-
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
927
|
+
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes, 0.5)
|
|
920
928
|
const handleY = wallHeight + HEIGHT_HANDLE_OFFSET
|
|
921
929
|
|
|
922
930
|
const activateHeightResize = (event: ThreeEvent<PointerEvent>) => {
|
|
@@ -969,7 +977,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
969
977
|
|
|
970
978
|
// Dragging the top makes the wall custom-height; seed from the resolved
|
|
971
979
|
// effective height so a plane-bound wall's drag starts at its real top.
|
|
972
|
-
const initialHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
980
|
+
const initialHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes, 0)
|
|
973
981
|
const wallId = wall.id as AnyNodeId
|
|
974
982
|
let pendingHeight = initialHeight
|
|
975
983
|
let releaseSpatialCapture: (() => void) | null = null
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { nodeRegistry, type WallNode } from '@pascal-app/core'
|
|
4
|
+
import { type ThreeEvent, useThree } from '@react-three/fiber'
|
|
5
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
6
|
+
import { Color, DoubleSide, ExtrudeGeometry, OrthographicCamera, Shape } from 'three'
|
|
7
|
+
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
8
|
+
import { EDITOR_LAYER } from '../../lib/constants'
|
|
9
|
+
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
10
|
+
import { startWallPull, WALL_PULL_GRIP_TITLE, wallPullSeed } from '../../lib/wall-pull'
|
|
11
|
+
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
12
|
+
import {
|
|
13
|
+
createEndpointHitAreaGeometry,
|
|
14
|
+
InvisibleHandleHitArea,
|
|
15
|
+
NO_RAYCAST,
|
|
16
|
+
useInvisibleHitAreaMaterial,
|
|
17
|
+
} from './handles/handle-arrow'
|
|
18
|
+
|
|
19
|
+
const PULL_FILL = '#10b981' // emerald-500 — the 2D grip's colour
|
|
20
|
+
const PULL_HOVER_FILL = '#34d399' // emerald-400
|
|
21
|
+
// Out from the wall end (m, before zoom): clear of the corner picker's hex.
|
|
22
|
+
const PULL_ARROW_OFFSET = 0.2
|
|
23
|
+
const PULL_ARROW_LENGTH = 0.34
|
|
24
|
+
const PULL_ARROW_HALF_WIDTH = 0.11
|
|
25
|
+
const PULL_ARROW_SHAFT_HALF_WIDTH = 0.04
|
|
26
|
+
const PULL_HIT_RADIUS = 0.17
|
|
27
|
+
const PULL_FLOOR_LIFT = 0.02
|
|
28
|
+
|
|
29
|
+
// The grip's tooltip: one plain DOM element beside the pointer, so it never
|
|
30
|
+
// lives in (or unmounts with) the R3F tree.
|
|
31
|
+
let tooltip: HTMLDivElement | null = null
|
|
32
|
+
|
|
33
|
+
function showGripTooltip(clientX: number, clientY: number) {
|
|
34
|
+
if (typeof document === 'undefined') return
|
|
35
|
+
if (!tooltip) {
|
|
36
|
+
tooltip = document.createElement('div')
|
|
37
|
+
tooltip.textContent = WALL_PULL_GRIP_TITLE
|
|
38
|
+
tooltip.setAttribute('role', 'tooltip')
|
|
39
|
+
Object.assign(tooltip.style, {
|
|
40
|
+
position: 'fixed',
|
|
41
|
+
zIndex: '60',
|
|
42
|
+
pointerEvents: 'none',
|
|
43
|
+
whiteSpace: 'nowrap',
|
|
44
|
+
borderRadius: '6px',
|
|
45
|
+
background: 'rgba(24, 24, 27, 0.92)',
|
|
46
|
+
color: '#fff',
|
|
47
|
+
font: '500 11px/1.4 system-ui, -apple-system, sans-serif',
|
|
48
|
+
padding: '4px 8px',
|
|
49
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
tooltip.style.left = `${clientX + 14}px`
|
|
53
|
+
tooltip.style.top = `${clientY + 18}px`
|
|
54
|
+
if (!tooltip.isConnected) document.body.appendChild(tooltip)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hideGripTooltip() {
|
|
58
|
+
tooltip?.remove()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A flat outward arrow (tip along +X), lying in the XZ plane from x = 0. */
|
|
62
|
+
function createPullArrowGeometry() {
|
|
63
|
+
const head = PULL_ARROW_LENGTH * 0.45
|
|
64
|
+
const shape = new Shape()
|
|
65
|
+
shape.moveTo(0, -PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
66
|
+
shape.lineTo(PULL_ARROW_LENGTH - head, -PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
67
|
+
shape.lineTo(PULL_ARROW_LENGTH - head, -PULL_ARROW_HALF_WIDTH)
|
|
68
|
+
shape.lineTo(PULL_ARROW_LENGTH, 0)
|
|
69
|
+
shape.lineTo(PULL_ARROW_LENGTH - head, PULL_ARROW_HALF_WIDTH)
|
|
70
|
+
shape.lineTo(PULL_ARROW_LENGTH - head, PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
71
|
+
shape.lineTo(0, PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
72
|
+
shape.lineTo(0, -PULL_ARROW_SHAFT_HALF_WIDTH)
|
|
73
|
+
const geometry = new ExtrudeGeometry(shape, {
|
|
74
|
+
depth: 0.03,
|
|
75
|
+
bevelEnabled: true,
|
|
76
|
+
bevelThickness: 0.01,
|
|
77
|
+
bevelSize: 0.012,
|
|
78
|
+
bevelSegments: 4,
|
|
79
|
+
})
|
|
80
|
+
geometry.translate(0, 0, -0.015)
|
|
81
|
+
geometry.rotateX(-Math.PI / 2)
|
|
82
|
+
geometry.computeVertexNormals()
|
|
83
|
+
geometry.computeBoundingSphere()
|
|
84
|
+
return geometry
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A selected wall end's "pull to continue" grip in 3D — the twin of the floor
|
|
89
|
+
* plan's: an emerald arrow lying on the floor just past the end, pointing out
|
|
90
|
+
* along the wall (along its tangent when curved), titled "Drag to draw the next
|
|
91
|
+
* wall" while the pointer rests on it. Pressing it starts
|
|
92
|
+
* `startWallPull`: the wall tool's Walls run opens at that end, the drag shows
|
|
93
|
+
* the next wall, and the release (the canvas click) places it. Screen-sized
|
|
94
|
+
* like the corner picker beside it; rendered in the wall's level frame at its
|
|
95
|
+
* base, by `WallMoveSideHandles`.
|
|
96
|
+
*/
|
|
97
|
+
export function WallPullGrip({ wall, endpoint }: { wall: WallNode; endpoint: 'start' | 'end' }) {
|
|
98
|
+
const [hover, setHover] = useState(false)
|
|
99
|
+
const { camera } = useThree()
|
|
100
|
+
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
101
|
+
const seed = wallPullSeed(wall, endpoint)
|
|
102
|
+
const geometry = useMemo(() => createPullArrowGeometry(), [])
|
|
103
|
+
const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(PULL_HIT_RADIUS), [])
|
|
104
|
+
const hitMaterial = useInvisibleHitAreaMaterial()
|
|
105
|
+
const material = useMemo(
|
|
106
|
+
() =>
|
|
107
|
+
new MeshBasicNodeMaterial({
|
|
108
|
+
color: new Color(PULL_FILL),
|
|
109
|
+
side: DoubleSide,
|
|
110
|
+
transparent: true,
|
|
111
|
+
opacity: 0.95,
|
|
112
|
+
depthTest: false,
|
|
113
|
+
depthWrite: false,
|
|
114
|
+
toneMapped: false,
|
|
115
|
+
}),
|
|
116
|
+
[],
|
|
117
|
+
)
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
material.color.set(hover ? PULL_HOVER_FILL : PULL_FILL)
|
|
120
|
+
}, [hover, material])
|
|
121
|
+
useEffect(() => () => geometry.dispose(), [geometry])
|
|
122
|
+
useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
|
|
123
|
+
useEffect(() => () => material.dispose(), [material])
|
|
124
|
+
// Warm the wall tool's module so the press starts drawing at once.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
void nodeRegistry
|
|
127
|
+
.get('wall')
|
|
128
|
+
?.tool?.()
|
|
129
|
+
.catch(() => {})
|
|
130
|
+
}, [])
|
|
131
|
+
useEffect(
|
|
132
|
+
() => () => {
|
|
133
|
+
hideGripTooltip()
|
|
134
|
+
if (document.body.style.cursor === 'grab') document.body.style.cursor = ''
|
|
135
|
+
},
|
|
136
|
+
[],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if (!seed) return null
|
|
140
|
+
const [dx, dz] = seed.reference
|
|
141
|
+
const offset = PULL_ARROW_OFFSET * zoom
|
|
142
|
+
const position: [number, number, number] = [
|
|
143
|
+
seed.start[0] + dx * offset,
|
|
144
|
+
PULL_FLOOR_LIFT,
|
|
145
|
+
seed.start[1] + dz * offset,
|
|
146
|
+
]
|
|
147
|
+
const rotationY = Math.atan2(-dz, dx)
|
|
148
|
+
|
|
149
|
+
const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
|
|
150
|
+
if (event.button !== 0) return
|
|
151
|
+
event.stopPropagation()
|
|
152
|
+
suppressBoxSelectForPointer(event)
|
|
153
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
154
|
+
setHover(false)
|
|
155
|
+
hideGripTooltip()
|
|
156
|
+
if (document.body.style.cursor === 'grab') document.body.style.cursor = ''
|
|
157
|
+
startWallPull({ wallId: wall.id, endpoint, origin: '3d' })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<group position={position} rotation={[0, rotationY, 0]} scale={zoom}>
|
|
162
|
+
<group position={[PULL_ARROW_LENGTH / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
163
|
+
<InvisibleHandleHitArea
|
|
164
|
+
geometry={hitGeometry}
|
|
165
|
+
material={hitMaterial}
|
|
166
|
+
onPointerDown={onPointerDown}
|
|
167
|
+
onPointerEnter={(event) => {
|
|
168
|
+
event.stopPropagation()
|
|
169
|
+
setHover(true)
|
|
170
|
+
showGripTooltip(event.nativeEvent.clientX, event.nativeEvent.clientY)
|
|
171
|
+
document.body.style.cursor = 'grab'
|
|
172
|
+
}}
|
|
173
|
+
onPointerLeave={(event) => {
|
|
174
|
+
event.stopPropagation()
|
|
175
|
+
setHover(false)
|
|
176
|
+
hideGripTooltip()
|
|
177
|
+
if (document.body.style.cursor === 'grab') document.body.style.cursor = ''
|
|
178
|
+
}}
|
|
179
|
+
scale={1}
|
|
180
|
+
/>
|
|
181
|
+
</group>
|
|
182
|
+
<mesh
|
|
183
|
+
frustumCulled={false}
|
|
184
|
+
geometry={geometry}
|
|
185
|
+
layers={EDITOR_LAYER}
|
|
186
|
+
material={material}
|
|
187
|
+
raycast={NO_RAYCAST}
|
|
188
|
+
renderOrder={1003}
|
|
189
|
+
scale={hover ? 1.15 : 1}
|
|
190
|
+
/>
|
|
191
|
+
</group>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
@@ -203,7 +203,7 @@ function WallRegionHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
203
203
|
const frame: WallFrame = {
|
|
204
204
|
wall: effectiveWall,
|
|
205
205
|
baseElevation: getWallBaseElevationForNodes(effectiveWall, nodes),
|
|
206
|
-
wallHeight: getWallEffectiveHeightForNodes(effectiveWall, nodes),
|
|
206
|
+
wallHeight: getWallEffectiveHeightForNodes(effectiveWall, nodes, 0.5),
|
|
207
207
|
angle: Math.atan2(
|
|
208
208
|
effectiveWall.end[1] - effectiveWall.start[1],
|
|
209
209
|
effectiveWall.end[0] - effectiveWall.start[0],
|
|
@@ -426,7 +426,7 @@ function RegionEdgeHandle({
|
|
|
426
426
|
const face = region.face
|
|
427
427
|
const faceSurface = getWallFaceSurface(initialNode, face)
|
|
428
428
|
const baseAt = faceBaseSampler(initialNode.id, face)
|
|
429
|
-
const wallHeight = getWallEffectiveHeightForNodes(initialNode, nodes)
|
|
429
|
+
const wallHeight = getWallEffectiveHeightForNodes(initialNode, nodes, 0.5)
|
|
430
430
|
const faceHeight = regionFaceHeight(region, faceSurface.length, wallHeight, baseAt)
|
|
431
431
|
const bounds = resolveRegionBounds(region, faceSurface.length, faceHeight)
|
|
432
432
|
const midU = (bounds.u0 + bounds.u1) / 2
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '@pascal-app/core'
|
|
17
17
|
import { useWallSnapIndicator, type WallSnapKind } from '@pascal-app/editor'
|
|
18
18
|
import { useViewer } from '@pascal-app/viewer'
|
|
19
|
+
import { Html } from '@react-three/drei'
|
|
19
20
|
import { useFrame } from '@react-three/fiber'
|
|
20
21
|
import { memo, useMemo, useRef } from 'react'
|
|
21
22
|
import { BoxGeometry, CircleGeometry, CylinderGeometry, type Group } from 'three'
|
|
@@ -31,10 +32,14 @@ import { EDITOR_LAYER } from '../../lib/constants'
|
|
|
31
32
|
*
|
|
32
33
|
* endpoint (corner) → square midpoint → triangle
|
|
33
34
|
* intersection → ✕ cross wall body (edge) → circle
|
|
35
|
+
* perpendicular → ⊥ corner projection guide → diamond
|
|
36
|
+
* grid length → small +
|
|
34
37
|
*
|
|
35
38
|
* Subscribes to the shared `useWallSnapIndicator` store (published by the wall
|
|
36
39
|
* draft + endpoint-move tools). The vertical mouse pillar and the corner
|
|
37
|
-
* (endpoint) square are green; the other floor glyphs are indigo.
|
|
40
|
+
* (endpoint) square are green; the other floor glyphs are indigo. A point that
|
|
41
|
+
* carries a `label` (a wall pulled from a wall end) also names what it caught
|
|
42
|
+
* in a pill beside the foot of the pillar.
|
|
38
43
|
*
|
|
39
44
|
* The point carries only XZ (building-local plan coords); like the alignment
|
|
40
45
|
* guides it's lifted to the active level's building-local Y each frame so it
|
|
@@ -135,6 +140,23 @@ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
|
|
|
135
140
|
renderOrder={1002}
|
|
136
141
|
/>
|
|
137
142
|
<SnapMarker kind={point.kind} x={point.x} z={point.z} />
|
|
143
|
+
{point.label ? (
|
|
144
|
+
<Html
|
|
145
|
+
position={[point.x, 0.05, point.z]}
|
|
146
|
+
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
147
|
+
zIndexRange={[24, 0]}
|
|
148
|
+
>
|
|
149
|
+
<div
|
|
150
|
+
className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-sans font-semibold text-[11px] text-white"
|
|
151
|
+
style={{
|
|
152
|
+
backgroundColor: point.kind === 'endpoint' ? '#16a34a' : '#6366f1',
|
|
153
|
+
transform: 'translate(12px, 8px)',
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{point.label}
|
|
157
|
+
</div>
|
|
158
|
+
</Html>
|
|
159
|
+
) : null}
|
|
138
160
|
</group>
|
|
139
161
|
)
|
|
140
162
|
})
|
|
@@ -143,9 +165,10 @@ type WallTopHighlightSegment = {
|
|
|
143
165
|
angle: number
|
|
144
166
|
center: [number, number]
|
|
145
167
|
length: number
|
|
168
|
+
tCenter: number
|
|
146
169
|
}
|
|
147
170
|
|
|
148
|
-
function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode
|
|
171
|
+
function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>, t = 0.5): number {
|
|
149
172
|
const levelId = resolveLevelId(wall, nodes as Record<string, AnyNode>)
|
|
150
173
|
const support = spatialGridManager.getSlabSupportForWall(
|
|
151
174
|
levelId,
|
|
@@ -159,12 +182,16 @@ function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>) {
|
|
|
159
182
|
wall.justification,
|
|
160
183
|
)
|
|
161
184
|
return (
|
|
162
|
-
getWallTop(wall, levelId, nodes as Record<string, AnyNode>, support.elevation) +
|
|
185
|
+
getWallTop(wall, levelId, nodes as Record<string, AnyNode>, support.elevation, t) +
|
|
163
186
|
WALL_TOP_HIGHLIGHT_LIFT
|
|
164
187
|
)
|
|
165
188
|
}
|
|
166
189
|
|
|
167
|
-
function buildHighlightSegment(
|
|
190
|
+
function buildHighlightSegment(
|
|
191
|
+
start: [number, number],
|
|
192
|
+
end: [number, number],
|
|
193
|
+
tCenter = 0.5,
|
|
194
|
+
): WallTopHighlightSegment | null {
|
|
168
195
|
const dx = end[0] - start[0]
|
|
169
196
|
const dz = end[1] - start[1]
|
|
170
197
|
const length = Math.hypot(dx, dz)
|
|
@@ -174,15 +201,20 @@ function buildHighlightSegment(start: [number, number], end: [number, number]) {
|
|
|
174
201
|
angle: -Math.atan2(dz, dx),
|
|
175
202
|
center: [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2] as [number, number],
|
|
176
203
|
length,
|
|
204
|
+
tCenter,
|
|
177
205
|
}
|
|
178
206
|
}
|
|
179
207
|
|
|
180
208
|
function buildWallTopHighlightSegments(wall: WallNode): WallTopHighlightSegment[] {
|
|
181
209
|
if (!isCurvedWall(wall)) {
|
|
182
|
-
const segment = buildHighlightSegment(wall.start, wall.end)
|
|
210
|
+
const segment = buildHighlightSegment(wall.start, wall.end, 0.5)
|
|
183
211
|
return segment ? [segment] : []
|
|
184
212
|
}
|
|
185
213
|
|
|
214
|
+
const dx = wall.end[0] - wall.start[0]
|
|
215
|
+
const dz = wall.end[1] - wall.start[1]
|
|
216
|
+
const chordLenSq = dx * dx + dz * dz
|
|
217
|
+
|
|
186
218
|
const sampleCount = Math.max(
|
|
187
219
|
8,
|
|
188
220
|
Math.ceil(getWallCurveLength(wall) / CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH),
|
|
@@ -191,7 +223,16 @@ function buildWallTopHighlightSegments(wall: WallNode): WallTopHighlightSegment[
|
|
|
191
223
|
let previous = getWallCurveFrameAt(wall, 0).point
|
|
192
224
|
for (let index = 1; index <= sampleCount; index += 1) {
|
|
193
225
|
const current = getWallCurveFrameAt(wall, index / sampleCount).point
|
|
194
|
-
const
|
|
226
|
+
const midX = (previous.x + current.x) / 2
|
|
227
|
+
const midY = (previous.y + current.y) / 2
|
|
228
|
+
const tCenter =
|
|
229
|
+
chordLenSq > 1e-12
|
|
230
|
+
? Math.max(
|
|
231
|
+
0,
|
|
232
|
+
Math.min(1, ((midX - wall.start[0]) * dx + (midY - wall.start[1]) * dz) / chordLenSq),
|
|
233
|
+
)
|
|
234
|
+
: 0.5
|
|
235
|
+
const segment = buildHighlightSegment([previous.x, previous.y], [current.x, current.y], tCenter)
|
|
195
236
|
if (segment) segments.push(segment)
|
|
196
237
|
previous = current
|
|
197
238
|
}
|
|
@@ -206,38 +247,40 @@ function WallTopHighlight({
|
|
|
206
247
|
wall: WallNode
|
|
207
248
|
}) {
|
|
208
249
|
const segments = useMemo(() => buildWallTopHighlightSegments(wall), [wall])
|
|
209
|
-
const y = getWallTopY(wall, nodes)
|
|
210
250
|
const width = Math.max(getWallThickness(wall) + WALL_TOP_HIGHLIGHT_OVERHANG, 0.24)
|
|
211
251
|
const glowWidth = Math.max(getWallThickness(wall) + WALL_TOP_GLOW_OVERHANG, 0.42)
|
|
212
252
|
|
|
213
253
|
return (
|
|
214
254
|
<>
|
|
215
|
-
{segments.map((segment, index) =>
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
255
|
+
{segments.map((segment, index) => {
|
|
256
|
+
const y = getWallTopY(wall, nodes, segment.tCenter)
|
|
257
|
+
return (
|
|
258
|
+
<group key={`${wall.id}:${index}`}>
|
|
259
|
+
<mesh
|
|
260
|
+
frustumCulled={false}
|
|
261
|
+
geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
|
|
262
|
+
layers={EDITOR_LAYER}
|
|
263
|
+
material={wallTopHighlightGlowMaterial}
|
|
264
|
+
position={[segment.center[0], y - 0.003, segment.center[1]]}
|
|
265
|
+
raycast={NO_RAYCAST}
|
|
266
|
+
renderOrder={1003}
|
|
267
|
+
rotation={[0, segment.angle, 0]}
|
|
268
|
+
scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_GLOW_HEIGHT, glowWidth]}
|
|
269
|
+
/>
|
|
270
|
+
<mesh
|
|
271
|
+
frustumCulled={false}
|
|
272
|
+
geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
|
|
273
|
+
layers={EDITOR_LAYER}
|
|
274
|
+
material={wallTopHighlightMaterial}
|
|
275
|
+
position={[segment.center[0], y + 0.002, segment.center[1]]}
|
|
276
|
+
raycast={NO_RAYCAST}
|
|
277
|
+
renderOrder={1004}
|
|
278
|
+
rotation={[0, segment.angle, 0]}
|
|
279
|
+
scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_HIGHLIGHT_HEIGHT, width]}
|
|
280
|
+
/>
|
|
281
|
+
</group>
|
|
282
|
+
)
|
|
283
|
+
})}
|
|
241
284
|
</>
|
|
242
285
|
)
|
|
243
286
|
}
|
|
@@ -295,6 +338,64 @@ function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }
|
|
|
295
338
|
</>
|
|
296
339
|
)
|
|
297
340
|
}
|
|
341
|
+
if (kind === 'perpendicular') {
|
|
342
|
+
// ⊥ — a base bar with an upright from its middle.
|
|
343
|
+
return (
|
|
344
|
+
<>
|
|
345
|
+
<mesh
|
|
346
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
347
|
+
layers={EDITOR_LAYER}
|
|
348
|
+
material={beaconMaterial}
|
|
349
|
+
position={[x, y, z]}
|
|
350
|
+
renderOrder={1001}
|
|
351
|
+
scale={[MARKER * 2.6, 1, MARKER * 0.45]}
|
|
352
|
+
/>
|
|
353
|
+
<mesh
|
|
354
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
355
|
+
layers={EDITOR_LAYER}
|
|
356
|
+
material={beaconMaterial}
|
|
357
|
+
position={[x, y, z - MARKER * 0.8]}
|
|
358
|
+
renderOrder={1001}
|
|
359
|
+
scale={[MARKER * 0.45, 1, MARKER * 1.6]}
|
|
360
|
+
/>
|
|
361
|
+
</>
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
if (kind === 'alignment') {
|
|
365
|
+
return (
|
|
366
|
+
<mesh
|
|
367
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
368
|
+
layers={EDITOR_LAYER}
|
|
369
|
+
material={beaconMaterial}
|
|
370
|
+
position={[x, y, z]}
|
|
371
|
+
renderOrder={1001}
|
|
372
|
+
rotation={[0, Math.PI / 4, 0]}
|
|
373
|
+
scale={[MARKER * 1.4, 1, MARKER * 1.4]}
|
|
374
|
+
/>
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
if (kind === 'grid') {
|
|
378
|
+
return (
|
|
379
|
+
<>
|
|
380
|
+
<mesh
|
|
381
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
382
|
+
layers={EDITOR_LAYER}
|
|
383
|
+
material={beaconMaterial}
|
|
384
|
+
position={[x, y, z]}
|
|
385
|
+
renderOrder={1001}
|
|
386
|
+
scale={[MARKER * 1.8, 1, MARKER * 0.35]}
|
|
387
|
+
/>
|
|
388
|
+
<mesh
|
|
389
|
+
geometry={FLAT_BOX_GEOMETRY}
|
|
390
|
+
layers={EDITOR_LAYER}
|
|
391
|
+
material={beaconMaterial}
|
|
392
|
+
position={[x, y, z]}
|
|
393
|
+
renderOrder={1001}
|
|
394
|
+
scale={[MARKER * 0.35, 1, MARKER * 1.8]}
|
|
395
|
+
/>
|
|
396
|
+
</>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
298
399
|
// 'wall' (edge / along-wall) → circle
|
|
299
400
|
return (
|
|
300
401
|
<mesh
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { type AnyNode, BuildingNode, LevelNode, WallNode } from '@pascal-app/core'
|
|
3
|
+
import { normalizePersistedEditorLayoutState } from '../../store/use-editor'
|
|
4
|
+
import { adjacentWallLevels, adjacentWallOutlines } from './floorplan-adjacent-walls-layer'
|
|
5
|
+
|
|
6
|
+
function house() {
|
|
7
|
+
const building = BuildingNode.parse({ id: 'building_adj' })
|
|
8
|
+
const levels = ['ground', 'upper', 'roof'].map((name, level) =>
|
|
9
|
+
LevelNode.parse({
|
|
10
|
+
id: `level_adj_${name}`,
|
|
11
|
+
parentId: building.id,
|
|
12
|
+
level,
|
|
13
|
+
height: 3,
|
|
14
|
+
...(name === 'roof' ? { metadata: { role: 'roof' } } : {}),
|
|
15
|
+
}),
|
|
16
|
+
)
|
|
17
|
+
building.children = levels.map((level) => level.id)
|
|
18
|
+
const walls = [
|
|
19
|
+
WallNode.parse({
|
|
20
|
+
id: 'wall_adj_a',
|
|
21
|
+
parentId: levels[1]!.id,
|
|
22
|
+
start: [0, 0],
|
|
23
|
+
end: [6, 0],
|
|
24
|
+
thickness: 0.2,
|
|
25
|
+
}),
|
|
26
|
+
WallNode.parse({
|
|
27
|
+
id: 'wall_adj_b',
|
|
28
|
+
parentId: levels[1]!.id,
|
|
29
|
+
start: [6, 0],
|
|
30
|
+
end: [6, 4],
|
|
31
|
+
thickness: 0.2,
|
|
32
|
+
}),
|
|
33
|
+
WallNode.parse({ id: 'wall_adj_c', parentId: levels[0]!.id, start: [0, 0], end: [6, 0] }),
|
|
34
|
+
]
|
|
35
|
+
return Object.fromEntries(
|
|
36
|
+
[building, ...levels, ...walls].map((node) => [node.id, node]),
|
|
37
|
+
) as Record<string, AnyNode>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('the roof plan traces the top storey’s walls below, both faces, joined at the corner', () => {
|
|
41
|
+
const nodes = house()
|
|
42
|
+
expect(adjacentWallLevels(nodes, 'level_adj_roof', { below: true, above: true })).toEqual({
|
|
43
|
+
below: 'level_adj_upper',
|
|
44
|
+
above: null,
|
|
45
|
+
})
|
|
46
|
+
expect(adjacentWallLevels(nodes, 'level_adj_upper', { below: true, above: false })).toEqual({
|
|
47
|
+
below: 'level_adj_ground',
|
|
48
|
+
above: null,
|
|
49
|
+
})
|
|
50
|
+
const outlines = adjacentWallOutlines(nodes, 'level_adj_upper')
|
|
51
|
+
expect(outlines.map((outline) => outline.id).sort()).toEqual(['wall_adj_a', 'wall_adj_b'])
|
|
52
|
+
const points = outlines
|
|
53
|
+
.find((outline) => outline.id === 'wall_adj_a')!
|
|
54
|
+
.points.split(' ')
|
|
55
|
+
.map((pair) => pair.split(',').map(Number) as [number, number])
|
|
56
|
+
const zs = points.map(([, z]) => z)
|
|
57
|
+
// Both faces of the 0.2 m wall, and mitred into the corner at x = 6 (past the centreline).
|
|
58
|
+
expect(Math.max(...zs) - Math.min(...zs)).toBeCloseTo(0.2)
|
|
59
|
+
expect(Math.max(...points.map(([x]) => x))).toBeCloseTo(6.1)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('walls below start on for the roof plan and off for the floor plans, each kept as set', () => {
|
|
63
|
+
const fresh = normalizePersistedEditorLayoutState(undefined)
|
|
64
|
+
expect(fresh.showRoofWallsBelow).toBe(true)
|
|
65
|
+
expect(fresh.showWallsBelow).toBe(false)
|
|
66
|
+
// Saved before the roof plan had its own setting: the roof plan still starts on.
|
|
67
|
+
expect(normalizePersistedEditorLayoutState({ showWallsBelow: false }).showRoofWallsBelow).toBe(
|
|
68
|
+
true,
|
|
69
|
+
)
|
|
70
|
+
const turnedOff = normalizePersistedEditorLayoutState({
|
|
71
|
+
showRoofWallsBelow: false,
|
|
72
|
+
showWallsBelow: true,
|
|
73
|
+
})
|
|
74
|
+
expect(turnedOff.showRoofWallsBelow).toBe(false)
|
|
75
|
+
expect(turnedOff.showWallsBelow).toBe(true)
|
|
76
|
+
})
|