@pascal-app/editor 1.0.3 → 1.0.5
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/README.md +106 -0
- package/package.json +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +275 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +409 -197
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +663 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +14 -19
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +790 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +284 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +230 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +187 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +392 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +122 -0
- package/src/lib/paint-pass-through.ts +78 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +265 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +1017 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +580 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +201 -0
- package/src/store/view-layout.ts +199 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
type BuildingNode,
|
|
5
5
|
createSceneApi,
|
|
6
6
|
emitter,
|
|
7
|
+
floorPlatePaintRefusal,
|
|
7
8
|
type GridEvent,
|
|
8
|
-
|
|
9
|
-
getEffectiveSegmentSurfaceMaterial,
|
|
9
|
+
getRoofSegmentDeckReach,
|
|
10
10
|
getRoofSegmentSurfaceY,
|
|
11
11
|
getSelectableKinds,
|
|
12
12
|
type ItemNode,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type RoofSegmentEvent,
|
|
20
20
|
type RoofSegmentNode,
|
|
21
21
|
resolveLevelId,
|
|
22
|
+
type SlabNode,
|
|
22
23
|
type StairEvent,
|
|
23
24
|
type StairSegmentEvent,
|
|
24
25
|
type StairSurfaceMaterialRole,
|
|
@@ -26,18 +27,18 @@ import {
|
|
|
26
27
|
useLiveNodeOverrides,
|
|
27
28
|
useRegistryVersion,
|
|
28
29
|
useScene,
|
|
30
|
+
wallFaceScriptPaintRefusal,
|
|
29
31
|
} from '@pascal-app/core'
|
|
30
|
-
|
|
31
|
-
import {
|
|
32
|
-
createMaterial,
|
|
33
|
-
createMaterialFromPresetRef,
|
|
34
|
-
getRoofMaterialArray,
|
|
35
|
-
registerMaterialCacheCleanup,
|
|
36
|
-
useViewer,
|
|
37
|
-
} from '@pascal-app/viewer'
|
|
32
|
+
import { getRoofMaterialArray, registerMaterialCacheCleanup, useViewer } from '@pascal-app/viewer'
|
|
38
33
|
import { useThree } from '@react-three/fiber'
|
|
39
34
|
import { useCallback, useEffect, useRef } from 'react'
|
|
40
35
|
import { type BufferGeometry, Color, type Material, type Mesh, type Object3D, Vector3 } from 'three'
|
|
36
|
+
import {
|
|
37
|
+
hoverRoomFromHit,
|
|
38
|
+
resolveEditorRoomHit,
|
|
39
|
+
roomPickingEnabled,
|
|
40
|
+
useRoomRecords,
|
|
41
|
+
} from '../../hooks/use-selected-room'
|
|
41
42
|
import {
|
|
42
43
|
canDirectMoveNode,
|
|
43
44
|
canDirectRotateNode,
|
|
@@ -48,33 +49,67 @@ import {
|
|
|
48
49
|
shouldStartDirectMoveDrag,
|
|
49
50
|
} from '../../lib/direct-manipulation'
|
|
50
51
|
import { createEditorApi } from '../../lib/editor-api'
|
|
52
|
+
import {
|
|
53
|
+
isFootprintConstructionHit,
|
|
54
|
+
isRoomOwnedPlate,
|
|
55
|
+
roomOwnedPlateDrillTarget,
|
|
56
|
+
} from '../../lib/floor-footprints'
|
|
51
57
|
import { selectionEnabled } from '../../lib/interaction/scope'
|
|
52
58
|
import {
|
|
53
59
|
type ActivePaintMaterial,
|
|
54
|
-
buildRoofSegmentSurfaceMaterialPatch,
|
|
55
60
|
buildRoofSurfaceMaterialPatch,
|
|
56
61
|
hasActivePaintMaterial,
|
|
57
62
|
resolveActivePaintMaterialFromSelection,
|
|
58
63
|
} from '../../lib/material-paint'
|
|
64
|
+
import { paintCommitRoute } from '../../lib/paint-commit-route'
|
|
65
|
+
import { eyedropperMaterial, paintMaterialKey } from '../../lib/paint-eyedropper'
|
|
66
|
+
import {
|
|
67
|
+
openingPartHidden,
|
|
68
|
+
paintPassesThrough,
|
|
69
|
+
wallHitInOpening,
|
|
70
|
+
} from '../../lib/paint-pass-through'
|
|
59
71
|
import {
|
|
60
72
|
combinePaintPreviews,
|
|
61
73
|
createPaintPreviewOwner,
|
|
62
74
|
type PaintPreviewCleanup,
|
|
63
75
|
} from '../../lib/paint-preview-owner'
|
|
64
76
|
import {
|
|
65
|
-
|
|
77
|
+
bindPaintPickHold,
|
|
78
|
+
isPaintErasing,
|
|
79
|
+
isPaintPicking,
|
|
80
|
+
paintPickReturnMode,
|
|
81
|
+
paintRegionModeActive,
|
|
82
|
+
usePaintRegionMode,
|
|
83
|
+
} from '../../lib/paint-region-mode'
|
|
84
|
+
import {
|
|
66
85
|
commitPaintScopeFanout,
|
|
86
|
+
effectivePaintScope,
|
|
67
87
|
nodeSlotRoles,
|
|
68
88
|
type PaintHoverInfo,
|
|
89
|
+
paintHoverInfo,
|
|
90
|
+
paintScopeRole,
|
|
91
|
+
paintSurfaceLabel,
|
|
69
92
|
resolvePaintScopeTargets,
|
|
70
93
|
slotDisplayLabel,
|
|
71
94
|
type WallPaintHit,
|
|
72
95
|
} from '../../lib/paint-scope'
|
|
96
|
+
import {
|
|
97
|
+
ceilingAffectedSurfaces,
|
|
98
|
+
mergePaintSurfaces,
|
|
99
|
+
paintSurfaceMeshes,
|
|
100
|
+
plateAffectedSurfaces,
|
|
101
|
+
platePreviewSurfaces,
|
|
102
|
+
usePaintOutline,
|
|
103
|
+
} from '../../lib/plate-paint-affected'
|
|
73
104
|
import { getHoveredRoofSegmentOutlineProxy } from '../../lib/roof-hover-outline-proxy'
|
|
105
|
+
import { sameRoom } from '../../lib/room-selection'
|
|
106
|
+
import { selectRoom, shouldInterceptRoom } from '../../lib/room-selection-commands'
|
|
107
|
+
import { roomKeyForZone } from '../../lib/room-zone-routing'
|
|
74
108
|
import {
|
|
75
109
|
emitCanvasNodeSelection,
|
|
110
|
+
enterBuildingFromCanvas,
|
|
111
|
+
resolveCanvasBuildingId,
|
|
76
112
|
resolveCanvasSelectionNode,
|
|
77
|
-
resolveNodeSelectionTarget,
|
|
78
113
|
resolveSelectedIdsForNodeClick,
|
|
79
114
|
type SelectionModifierKeys,
|
|
80
115
|
selectionModifiersFromEvent,
|
|
@@ -88,7 +123,7 @@ import {
|
|
|
88
123
|
zoneAtWorldPoint,
|
|
89
124
|
} from '../../lib/units'
|
|
90
125
|
import useDirectManipulationFeedback from '../../store/use-direct-manipulation-feedback'
|
|
91
|
-
import useEditor, { type MaterialTargetRole } from './../../store/use-editor'
|
|
126
|
+
import useEditor, { type MaterialTargetRole, type Phase } from './../../store/use-editor'
|
|
92
127
|
import useInteractionScope, {
|
|
93
128
|
getEditingHole,
|
|
94
129
|
getMovingNode,
|
|
@@ -100,6 +135,7 @@ import { boxSelectHandled, suppressBoxSelectForPointer } from '../tools/select/b
|
|
|
100
135
|
import { armGroupMove3d } from './group-move-3d'
|
|
101
136
|
import { classifyParticipant } from './group-transform-shared'
|
|
102
137
|
import { swallowNextClick } from './node-arrow-handles'
|
|
138
|
+
import { RoomHighlight3D } from './room-highlight'
|
|
103
139
|
import { setEditorThreeContext } from './three-context-bridge'
|
|
104
140
|
|
|
105
141
|
const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
|
@@ -111,24 +147,6 @@ const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
|
|
111
147
|
return nodeLevelId === currentLevelId
|
|
112
148
|
}
|
|
113
149
|
|
|
114
|
-
type SelectableNodeType =
|
|
115
|
-
| 'wall'
|
|
116
|
-
| 'fence'
|
|
117
|
-
| 'item'
|
|
118
|
-
| 'column'
|
|
119
|
-
| 'building'
|
|
120
|
-
| 'elevator'
|
|
121
|
-
| 'zone'
|
|
122
|
-
| 'slab'
|
|
123
|
-
| 'ceiling'
|
|
124
|
-
| 'roof'
|
|
125
|
-
| 'roof-segment'
|
|
126
|
-
| 'stair'
|
|
127
|
-
| 'stair-segment'
|
|
128
|
-
| 'spawn'
|
|
129
|
-
| 'window'
|
|
130
|
-
| 'door'
|
|
131
|
-
|
|
132
150
|
type PaintInteraction = {
|
|
133
151
|
key: string
|
|
134
152
|
apply: (() => void) | null
|
|
@@ -141,7 +159,6 @@ type PaintInteraction = {
|
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
interface SelectionStrategy {
|
|
144
|
-
types: SelectableNodeType[]
|
|
145
162
|
handleSelect: (
|
|
146
163
|
node: AnyNode,
|
|
147
164
|
nativeEvent?: any,
|
|
@@ -183,6 +200,18 @@ export const resolveBuildingId = (
|
|
|
183
200
|
return null
|
|
184
201
|
}
|
|
185
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Furnishings: picking one leaves the room context (a sofa is not part of the
|
|
205
|
+
* room's shell). Door and window catalog items are openings, not furnishings.
|
|
206
|
+
*/
|
|
207
|
+
function isFurnishingNode(node: AnyNode): boolean {
|
|
208
|
+
if (node.type === 'item') {
|
|
209
|
+
const category = (node as ItemNode).asset.category
|
|
210
|
+
return category !== 'door' && category !== 'window'
|
|
211
|
+
}
|
|
212
|
+
return nodeRegistry.get(node.type)?.category === 'furnish'
|
|
213
|
+
}
|
|
214
|
+
|
|
186
215
|
function resolveStairMaterialTarget(
|
|
187
216
|
event: StairEvent | StairSegmentEvent,
|
|
188
217
|
): StairSurfaceMaterialRole | null {
|
|
@@ -233,6 +262,14 @@ function resolveRoofMaterialTarget(
|
|
|
233
262
|
return null
|
|
234
263
|
}
|
|
235
264
|
|
|
265
|
+
function isCeilingGridHit(event: NodeEvent): boolean {
|
|
266
|
+
return (
|
|
267
|
+
!event.viaHandle &&
|
|
268
|
+
event.node.type === 'ceiling' &&
|
|
269
|
+
getEventObject(event)?.name === 'ceiling-grid'
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
236
273
|
function getEventObject(event: NodeEvent): Object3D {
|
|
237
274
|
const eventWithObject = event as NodeEvent & { object?: Object3D }
|
|
238
275
|
return eventWithObject.object ?? event.nativeEvent.object
|
|
@@ -309,6 +346,31 @@ function meshSlotRoles(node: AnyNode): string[] {
|
|
|
309
346
|
const roofSelectionWorldPoint = new Vector3()
|
|
310
347
|
const wallPaintWorldPoint = new Vector3()
|
|
311
348
|
|
|
349
|
+
const roomHitPoint = new Vector3()
|
|
350
|
+
const roomHitXZ: [number, number] = [0, 0]
|
|
351
|
+
export function roomForEvent(event: NodeEvent) {
|
|
352
|
+
if (event.node.type !== 'wall' && event.node.type !== 'slab' && event.node.type !== 'ceiling')
|
|
353
|
+
return null
|
|
354
|
+
// The floor edge band and the foundation belong to the footprint, not the
|
|
355
|
+
// room above: the plate takes the pick and opens Floor & foundation.
|
|
356
|
+
if (isFootprintConstructionHit(event.node, event.object)) return null
|
|
357
|
+
const levelId = resolveLevelId(event.node, useScene.getState().nodes)
|
|
358
|
+
if (!levelId) return null
|
|
359
|
+
if (event.viaHandle && event.node.type === 'ceiling') {
|
|
360
|
+
roomHitXZ[0] = event.position[0]
|
|
361
|
+
roomHitXZ[1] = event.position[2]
|
|
362
|
+
return resolveEditorRoomHit(event.node, levelId, roomHitXZ, '3d')
|
|
363
|
+
}
|
|
364
|
+
const level = sceneRegistry.nodes.get(levelId)
|
|
365
|
+
if (!level) return null
|
|
366
|
+
level.updateWorldMatrix(true, false)
|
|
367
|
+
roomHitPoint.set(...event.position)
|
|
368
|
+
level.worldToLocal(roomHitPoint)
|
|
369
|
+
roomHitXZ[0] = roomHitPoint.x
|
|
370
|
+
roomHitXZ[1] = roomHitPoint.z
|
|
371
|
+
return resolveEditorRoomHit(event.node, levelId, roomHitXZ, '3d')
|
|
372
|
+
}
|
|
373
|
+
|
|
312
374
|
function resolveWallPaintHit(event: NodeEvent): WallPaintHit | undefined {
|
|
313
375
|
const wall = event.node
|
|
314
376
|
if (wall.type !== 'wall') return undefined
|
|
@@ -331,7 +393,7 @@ function resolveWallPaintHit(event: NodeEvent): WallPaintHit | undefined {
|
|
|
331
393
|
}
|
|
332
394
|
}
|
|
333
395
|
|
|
334
|
-
function resolveRoofSegmentSelectionTarget(event: NodeEvent): RoofSegmentNode | null {
|
|
396
|
+
export function resolveRoofSegmentSelectionTarget(event: NodeEvent): RoofSegmentNode | null {
|
|
335
397
|
const roof = event.node
|
|
336
398
|
if (roof.type !== 'roof') return null
|
|
337
399
|
|
|
@@ -351,9 +413,9 @@ function resolveRoofSegmentSelectionTarget(event: NodeEvent): RoofSegmentNode |
|
|
|
351
413
|
|
|
352
414
|
object.updateWorldMatrix(true, false)
|
|
353
415
|
const local = object.worldToLocal(roofSelectionWorldPoint.clone())
|
|
354
|
-
const
|
|
355
|
-
const halfWidth = segment.width / 2 +
|
|
356
|
-
const halfDepth = segment.depth / 2 +
|
|
416
|
+
const reach = getRoofSegmentDeckReach(segment)
|
|
417
|
+
const halfWidth = segment.width / 2 + reach.x
|
|
418
|
+
const halfDepth = segment.depth / 2 + reach.z
|
|
357
419
|
|
|
358
420
|
if (Math.abs(local.x) > halfWidth || Math.abs(local.z) > halfDepth) {
|
|
359
421
|
continue
|
|
@@ -368,7 +430,11 @@ function resolveRoofSegmentSelectionTarget(event: NodeEvent): RoofSegmentNode |
|
|
|
368
430
|
return bestSegment?.node ?? firstSegment
|
|
369
431
|
}
|
|
370
432
|
|
|
371
|
-
function resolveSelectModeNodeTarget(event: NodeEvent): AnyNode {
|
|
433
|
+
function resolveSelectModeNodeTarget(event: NodeEvent, drill = false): AnyNode {
|
|
434
|
+
if (!drill && event.node.type === 'stair-segment' && event.node.parentId) {
|
|
435
|
+
const parent = useScene.getState().nodes[event.node.parentId as AnyNodeId]
|
|
436
|
+
if (parent?.type === 'stair') return parent
|
|
437
|
+
}
|
|
372
438
|
if (event.node.type === 'roof') {
|
|
373
439
|
if (
|
|
374
440
|
shouldPreserveSelectedRoofHostTarget({
|
|
@@ -420,52 +486,14 @@ function applyRoofPaintPreview(
|
|
|
420
486
|
useViewer.getState().textures,
|
|
421
487
|
useViewer.getState().colorPreset,
|
|
422
488
|
useViewer.getState().sceneTheme,
|
|
489
|
+
null,
|
|
490
|
+
useScene.getState().materials,
|
|
423
491
|
)
|
|
424
492
|
if (!previewMaterial) return null
|
|
425
493
|
|
|
426
494
|
return previewMeshMaterial(mesh, previewMaterial)
|
|
427
495
|
}
|
|
428
496
|
|
|
429
|
-
function applyRoofSegmentPaintPreview(
|
|
430
|
-
node: RoofSegmentNode,
|
|
431
|
-
parent: RoofNode | null,
|
|
432
|
-
role: 'top' | 'edge' | 'wall',
|
|
433
|
-
material: ActivePaintMaterial,
|
|
434
|
-
): PaintPreviewCleanup | null {
|
|
435
|
-
const mesh = getRegisteredMesh(node.id)
|
|
436
|
-
if (!mesh) return null
|
|
437
|
-
|
|
438
|
-
// Synthesise the segment node as if the paint had committed, then build
|
|
439
|
-
// the same 4-slot array the renderer would. Mirrors getRoofMaterialArray
|
|
440
|
-
// layout (slot 0 ← edge, 1 ← wall, 2 ← wall, 3 ← top) so the preview
|
|
441
|
-
// material lands on the matching CSG groups.
|
|
442
|
-
const previewNode: RoofSegmentNode = {
|
|
443
|
-
...node,
|
|
444
|
-
...buildRoofSegmentSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
|
445
|
-
}
|
|
446
|
-
const resolveSlot = (r: 'top' | 'edge' | 'wall'): Material | null => {
|
|
447
|
-
const parentSpec = parent ? getEffectiveRoofSurfaceMaterial(parent, r) : undefined
|
|
448
|
-
const spec = getEffectiveSegmentSurfaceMaterial(previewNode, r, parentSpec)
|
|
449
|
-
if (typeof spec.materialPreset === 'string') {
|
|
450
|
-
const resolved = createMaterialFromPresetRef(spec.materialPreset)
|
|
451
|
-
if (resolved) return resolved
|
|
452
|
-
}
|
|
453
|
-
if (spec.material !== undefined) return createMaterial(spec.material)
|
|
454
|
-
return null
|
|
455
|
-
}
|
|
456
|
-
const edge = resolveSlot('edge')
|
|
457
|
-
const wall = resolveSlot('wall')
|
|
458
|
-
const top = resolveSlot('top')
|
|
459
|
-
if (!(edge || wall || top)) return null
|
|
460
|
-
const fallback = parent ? getRoofMaterialArray(parent) : null
|
|
461
|
-
const fb = (n: number) => fallback?.[n] ?? null
|
|
462
|
-
// Per-role only, then the parent's themed slot — matches the renderer so the
|
|
463
|
-
// preview never bleeds a painted surface onto the segment's other surfaces.
|
|
464
|
-
const arr: Material[] = [edge ?? fb(0)!, wall ?? fb(1)!, wall ?? fb(2)!, top ?? fb(3)!]
|
|
465
|
-
if (arr.some((m) => !m)) return null
|
|
466
|
-
return previewMeshMaterial(mesh, arr)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
497
|
// Chimney + dormer paint dispatch lives on their NodeDefinition's
|
|
470
498
|
// `capabilities.paint` (see packages/nodes/src/{chimney,dormer}/
|
|
471
499
|
// paint.ts). The generic registry-driven arm in this file consults
|
|
@@ -504,7 +532,7 @@ const HIGHLIGHT_PROFILES = {
|
|
|
504
532
|
} as const
|
|
505
533
|
|
|
506
534
|
type HighlightKind = keyof typeof HIGHLIGHT_PROFILES
|
|
507
|
-
type HoverHighlightMode = 'default' | 'delete' | 'paint-ready' | 'paint-disabled'
|
|
535
|
+
type HoverHighlightMode = 'default' | 'delete' | 'paint-ready' | 'erase-ready' | 'paint-disabled'
|
|
508
536
|
|
|
509
537
|
type HighlightableMaterial = Material & {
|
|
510
538
|
color?: Color
|
|
@@ -611,9 +639,8 @@ const computeNextIds = (
|
|
|
611
639
|
})
|
|
612
640
|
}
|
|
613
641
|
|
|
614
|
-
const SELECTION_STRATEGIES: Record<
|
|
642
|
+
const SELECTION_STRATEGIES: Record<Phase, SelectionStrategy> = {
|
|
615
643
|
site: {
|
|
616
|
-
types: ['building'],
|
|
617
644
|
handleSelect: (node) => {
|
|
618
645
|
useViewer.getState().setSelection({ buildingId: (node as BuildingNode).id })
|
|
619
646
|
},
|
|
@@ -623,24 +650,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
623
650
|
isValid: (node) => node.type === 'building',
|
|
624
651
|
},
|
|
625
652
|
|
|
626
|
-
|
|
627
|
-
types: [
|
|
628
|
-
'wall',
|
|
629
|
-
'fence',
|
|
630
|
-
'item',
|
|
631
|
-
'column',
|
|
632
|
-
'elevator',
|
|
633
|
-
'zone',
|
|
634
|
-
'slab',
|
|
635
|
-
'ceiling',
|
|
636
|
-
'roof',
|
|
637
|
-
'roof-segment',
|
|
638
|
-
'stair',
|
|
639
|
-
'stair-segment',
|
|
640
|
-
'spawn',
|
|
641
|
-
'window',
|
|
642
|
-
'door',
|
|
643
|
-
],
|
|
653
|
+
building: {
|
|
644
654
|
handleSelect: (node, nativeEvent, modifierKeys, baseSelectedIds) => {
|
|
645
655
|
const { selection, setSelection } = useViewer.getState()
|
|
646
656
|
const nodes = useScene.getState().nodes
|
|
@@ -662,10 +672,15 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
662
672
|
updates.buildingId = buildingId
|
|
663
673
|
}
|
|
664
674
|
|
|
675
|
+
if (node.type === 'zone' && !useViewer.getState().focusedUnitId) {
|
|
676
|
+
const room = roomKeyForZone(node.id)
|
|
677
|
+
if (room) {
|
|
678
|
+
if (!sameRoom(useEditor.getState().room, room)) selectRoom(room)
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
}
|
|
665
682
|
if (node.type === 'zone') {
|
|
666
683
|
updates.zoneId = node.id
|
|
667
|
-
// Don't reset selectedIds in structure phase for zone, but if we changed level, it might reset them via hierarchy guard.
|
|
668
|
-
// Wait, the hierarchy guard resets zoneId if levelId changes. That's fine since we provide zoneId.
|
|
669
684
|
setSelection(updates)
|
|
670
685
|
} else {
|
|
671
686
|
updates.selectedIds = computeNextIds(
|
|
@@ -679,21 +694,12 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
679
694
|
}
|
|
680
695
|
},
|
|
681
696
|
handleDeselect: () => {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
useViewer.getState().setSelection({ zoneId: null })
|
|
685
|
-
} else {
|
|
686
|
-
useViewer.getState().setSelection({ selectedIds: [] })
|
|
687
|
-
}
|
|
697
|
+
useEditor.getState().clearRoom()
|
|
698
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
688
699
|
},
|
|
689
700
|
isValid: (node) => {
|
|
690
701
|
if (!isNodeInCurrentLevel(node)) return false
|
|
691
|
-
|
|
692
|
-
if (structureLayer === 'zones') {
|
|
693
|
-
if (node.type === 'zone') return true
|
|
694
|
-
return false
|
|
695
|
-
}
|
|
696
|
-
if (
|
|
702
|
+
return (
|
|
697
703
|
node.type === 'wall' ||
|
|
698
704
|
node.type === 'fence' ||
|
|
699
705
|
node.type === 'column' ||
|
|
@@ -704,75 +710,36 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
704
710
|
node.type === 'roof-segment' ||
|
|
705
711
|
node.type === 'stair' ||
|
|
706
712
|
node.type === 'stair-segment' ||
|
|
707
|
-
node.type === 'spawn'
|
|
713
|
+
node.type === 'spawn' ||
|
|
714
|
+
node.type === 'window' ||
|
|
715
|
+
node.type === 'door' ||
|
|
716
|
+
node.type === 'item' ||
|
|
717
|
+
isRegistrySelectable(node.type)
|
|
708
718
|
)
|
|
709
|
-
return true
|
|
710
|
-
if (node.type === 'item') {
|
|
711
|
-
return (
|
|
712
|
-
(node as ItemNode).asset.category === 'door' ||
|
|
713
|
-
(node as ItemNode).asset.category === 'window'
|
|
714
|
-
)
|
|
715
|
-
}
|
|
716
|
-
if (node.type === 'window' || node.type === 'door') return true
|
|
717
|
-
|
|
718
|
-
// Registry-driven: any kind whose NodeDefinition declares the
|
|
719
|
-
// `selectable` capability is also selectable in structure phase. Phase 4
|
|
720
|
-
// makes this the only path and deletes the hardcoded chain above.
|
|
721
|
-
if (isRegistrySelectable(node.type)) return true
|
|
722
|
-
|
|
723
|
-
return false
|
|
724
|
-
},
|
|
725
|
-
},
|
|
726
|
-
|
|
727
|
-
furnish: {
|
|
728
|
-
types: ['item'],
|
|
729
|
-
handleSelect: (node, nativeEvent, modifierKeys, baseSelectedIds) => {
|
|
730
|
-
const { selection, setSelection } = useViewer.getState()
|
|
731
|
-
const nodes = useScene.getState().nodes
|
|
732
|
-
const nodeLevelId = resolveLevelId(node, nodes)
|
|
733
|
-
const buildingId = resolveBuildingId(nodeLevelId, nodes)
|
|
734
|
-
|
|
735
|
-
const updates: any = {}
|
|
736
|
-
if (nodeLevelId !== 'default' && nodeLevelId !== selection.levelId) {
|
|
737
|
-
updates.levelId = nodeLevelId
|
|
738
|
-
}
|
|
739
|
-
if (buildingId && buildingId !== selection.buildingId) {
|
|
740
|
-
updates.buildingId = buildingId
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
updates.selectedIds = computeNextIds(
|
|
744
|
-
node,
|
|
745
|
-
selection.selectedIds,
|
|
746
|
-
nativeEvent,
|
|
747
|
-
modifierKeys,
|
|
748
|
-
baseSelectedIds,
|
|
749
|
-
)
|
|
750
|
-
setSelection(updates)
|
|
751
|
-
},
|
|
752
|
-
handleDeselect: () => {
|
|
753
|
-
useViewer.getState().setSelection({ selectedIds: [] })
|
|
754
|
-
},
|
|
755
|
-
isValid: (node) => {
|
|
756
|
-
if (!isNodeInCurrentLevel(node)) return false
|
|
757
|
-
// Item: door/window-category items belong to structure phase, not furnish.
|
|
758
|
-
if (node.type === 'item') {
|
|
759
|
-
const item = node as ItemNode
|
|
760
|
-
return item.asset.category !== 'door' && item.asset.category !== 'window'
|
|
761
|
-
}
|
|
762
|
-
// Registry-driven kinds with `category: 'furnish'` (shelf today,
|
|
763
|
-
// future furniture kinds): selectable in furnish phase if their
|
|
764
|
-
// definition declares the `selectable` capability. Without this
|
|
765
|
-
// branch, shelf clicks routed to furnish phase via resolveNodeSelectionTarget
|
|
766
|
-
// would be rejected here — single-click selection broken.
|
|
767
|
-
const def = nodeRegistry.get(node.type)
|
|
768
|
-
if (def && def.category === 'furnish' && def.capabilities.selectable) return true
|
|
769
|
-
return false
|
|
770
719
|
},
|
|
771
720
|
},
|
|
772
721
|
}
|
|
773
722
|
|
|
774
723
|
export const SelectionManager = () => {
|
|
775
|
-
const
|
|
724
|
+
const roomLevelId = useViewer((s) => s.selection.levelId)
|
|
725
|
+
useRoomRecords(roomLevelId)
|
|
726
|
+
useEffect(
|
|
727
|
+
() =>
|
|
728
|
+
useViewer.subscribe((state, previous) => {
|
|
729
|
+
if (
|
|
730
|
+
state.selection.levelId !== previous.selection.levelId ||
|
|
731
|
+
(state.selection.zoneId !== previous.selection.zoneId && state.selection.zoneId) ||
|
|
732
|
+
(state.selection.selectedIds !== previous.selection.selectedIds &&
|
|
733
|
+
state.selection.selectedIds.some((id) => {
|
|
734
|
+
const node = useScene.getState().nodes[id as AnyNodeId]
|
|
735
|
+
return node && isFurnishingNode(node)
|
|
736
|
+
}))
|
|
737
|
+
) {
|
|
738
|
+
useEditor.getState().clearRoom()
|
|
739
|
+
}
|
|
740
|
+
}),
|
|
741
|
+
[],
|
|
742
|
+
)
|
|
776
743
|
const mode = useEditor((s) => s.mode)
|
|
777
744
|
// The canvas element — cursor styling must land here, not on `document.body`:
|
|
778
745
|
// the editor wraps the canvas in a div with a custom `cursor: url(...)`, which
|
|
@@ -825,6 +792,18 @@ export const SelectionManager = () => {
|
|
|
825
792
|
// The last hover event, replayed when the application scope cycles so the
|
|
826
793
|
// preview + chip update under a stationary cursor (Shift fires no pointer move).
|
|
827
794
|
let lastEnterEvent: NodeEvent | null = null
|
|
795
|
+
// Paint hover never stops a pointer event. R3F keeps a hover that stopped
|
|
796
|
+
// propagation stopping every later move over that object, which would pin
|
|
797
|
+
// the pointer to a door or a wall it should pass through where it opens
|
|
798
|
+
// (see `paint-pass-through`). So the nearest surface that takes a pointer
|
|
799
|
+
// event claims it; farther ones see the claim and stand down.
|
|
800
|
+
let claimedEvent: unknown = null
|
|
801
|
+
let hoveredNodeId: string | null = null
|
|
802
|
+
const claim = (event: NodeEvent) => {
|
|
803
|
+
claimedEvent = event.nativeEvent.nativeEvent
|
|
804
|
+
}
|
|
805
|
+
const claimedByNearer = (event: NodeEvent) =>
|
|
806
|
+
claimedEvent !== null && claimedEvent === event.nativeEvent.nativeEvent
|
|
828
807
|
|
|
829
808
|
const clearActivePreview = () => {
|
|
830
809
|
activePreview?.restore()
|
|
@@ -835,6 +814,7 @@ export const SelectionManager = () => {
|
|
|
835
814
|
useEditor.getState().activePaintMaterial ??
|
|
836
815
|
resolveActivePaintMaterialFromSelection({
|
|
837
816
|
nodes: useScene.getState().nodes,
|
|
817
|
+
materials: useScene.getState().materials,
|
|
838
818
|
selectedId:
|
|
839
819
|
useViewer.getState().selection.selectedIds.length === 1
|
|
840
820
|
? (useViewer.getState().selection.selectedIds[0] ?? null)
|
|
@@ -842,8 +822,50 @@ export const SelectionManager = () => {
|
|
|
842
822
|
selectedMaterialTarget: useEditor.getState().selectedMaterialTarget,
|
|
843
823
|
})
|
|
844
824
|
|
|
825
|
+
// The eyedropper: the hover shows what a click would take (the cursor
|
|
826
|
+
// swatch), the click takes it and returns to the sub-mode it came from.
|
|
827
|
+
const pickInteraction = (
|
|
828
|
+
node: AnyNode,
|
|
829
|
+
role: string | null,
|
|
830
|
+
event: NodeEvent,
|
|
831
|
+
materialIndex: number | null,
|
|
832
|
+
): PaintInteraction => {
|
|
833
|
+
const picked = role
|
|
834
|
+
? eyedropperMaterial({
|
|
835
|
+
node,
|
|
836
|
+
role,
|
|
837
|
+
nodes: useScene.getState().nodes,
|
|
838
|
+
materials: useScene.getState().materials,
|
|
839
|
+
hitObject: getEventObject(event),
|
|
840
|
+
materialIndex,
|
|
841
|
+
})
|
|
842
|
+
: null
|
|
843
|
+
return {
|
|
844
|
+
key: `pick:${node.id}:${role ?? 'none'}:${paintMaterialKey(picked)}`,
|
|
845
|
+
hoveredId: node.id as AnyNodeId,
|
|
846
|
+
hoverMode: picked ? 'paint-ready' : 'paint-disabled',
|
|
847
|
+
paintHover:
|
|
848
|
+
picked && role
|
|
849
|
+
? { scopes: ['single'], slotLabel: paintSurfaceLabel(node, role), nodeNoun: node.type }
|
|
850
|
+
: null,
|
|
851
|
+
apply: picked
|
|
852
|
+
? () => useEditor.getState().armMaterialPaint(picked, paintPickReturnMode())
|
|
853
|
+
: null,
|
|
854
|
+
preview: () => {
|
|
855
|
+
usePaintRegionMode.setState({ picked })
|
|
856
|
+
return () => {
|
|
857
|
+
if (usePaintRegionMode.getState().picked === picked)
|
|
858
|
+
usePaintRegionMode.setState({ picked: null })
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
845
864
|
const resolvePaintInteraction = (event: NodeEvent): PaintInteraction | null => {
|
|
846
|
-
|
|
865
|
+
// A region sub-mode draws its own region before painting; the
|
|
866
|
+
// whole-surface hover and click stand down.
|
|
867
|
+
if (paintRegionModeActive('material-paint')) return null
|
|
868
|
+
const eraser = isPaintErasing()
|
|
847
869
|
const activePaintMaterial = resolveActivePaintMaterial()
|
|
848
870
|
const node = event.node
|
|
849
871
|
|
|
@@ -885,6 +907,25 @@ export const SelectionManager = () => {
|
|
|
885
907
|
hitObject: getEventObject(event),
|
|
886
908
|
ray: event.nativeEvent.ray,
|
|
887
909
|
})
|
|
910
|
+
// Not claiming the hover lets the pointer event reach the surface behind:
|
|
911
|
+
// an empty door or window, or a wall's collision hit in one of its holes.
|
|
912
|
+
if (
|
|
913
|
+
paintPassesThrough(node, role) ||
|
|
914
|
+
((node.type === 'door' || node.type === 'window') &&
|
|
915
|
+
openingPartHidden(
|
|
916
|
+
getRegisteredNodeObject(node.id),
|
|
917
|
+
event.nativeEvent.ray,
|
|
918
|
+
event.nativeEvent.intersections,
|
|
919
|
+
)) ||
|
|
920
|
+
(node.type === 'wall' &&
|
|
921
|
+
wallHitInOpening(
|
|
922
|
+
getRegisteredNodeObject(node.id),
|
|
923
|
+
event.nativeEvent.ray,
|
|
924
|
+
event.nativeEvent.distance,
|
|
925
|
+
))
|
|
926
|
+
)
|
|
927
|
+
return null
|
|
928
|
+
if (isPaintPicking()) return pickInteraction(node, role, event, materialIndex ?? null)
|
|
888
929
|
const compatible = role !== null && paintEnabled
|
|
889
930
|
// Derive the node's slots (declared, else mesh tags) once — drives both
|
|
890
931
|
// the chip's available scopes and the whole-object fan-out.
|
|
@@ -893,7 +934,13 @@ export const SelectionManager = () => {
|
|
|
893
934
|
// / all matching / room). The scope is part of the key so cycling it
|
|
894
935
|
// (Shift) re-keys the interaction → the preview re-applies for the new
|
|
895
936
|
// spread instead of being deduped to the single-surface preview.
|
|
896
|
-
const
|
|
937
|
+
const hover = compatible && role ? paintHoverInfo(node, role, slotRoles) : null
|
|
938
|
+
// A scope carried over from another surface paints what the chip
|
|
939
|
+
// shows for this one: the narrowest when it isn't offered here.
|
|
940
|
+
const scope = effectivePaintScope(
|
|
941
|
+
useEditor.getState().paintScope,
|
|
942
|
+
hover?.scopes ?? ['single'],
|
|
943
|
+
)
|
|
897
944
|
const wallHit = resolveWallPaintHit(event)
|
|
898
945
|
const scopeTargets =
|
|
899
946
|
compatible && role
|
|
@@ -907,30 +954,47 @@ export const SelectionManager = () => {
|
|
|
907
954
|
wallHit,
|
|
908
955
|
})
|
|
909
956
|
: []
|
|
910
|
-
|
|
957
|
+
// What the click changes on floor plates, fallbacks included (a room's
|
|
958
|
+
// floor carries its unpainted steps): the outline. The preview draws
|
|
959
|
+
// the same set, each follower through the surface it follows.
|
|
960
|
+
const scopeRole = compatible && role ? paintScopeRole(node, role, scope) : null
|
|
961
|
+
const sceneNodes = useScene.getState().nodes
|
|
962
|
+
const affected = scopeRole
|
|
963
|
+
? (plateAffectedSurfaces(sceneNodes, node, scopeRole, { erasing: eraser }) ??
|
|
964
|
+
ceilingAffectedSurfaces(node, scopeRole))
|
|
965
|
+
: null
|
|
966
|
+
const previewTargets = mergePaintSurfaces(
|
|
967
|
+
scopeTargets,
|
|
968
|
+
scopeRole
|
|
969
|
+
? (platePreviewSurfaces(sceneNodes, node, scopeRole) ??
|
|
970
|
+
ceilingAffectedSurfaces(node, scopeRole))
|
|
971
|
+
: null,
|
|
972
|
+
)
|
|
973
|
+
const scopeTargetKey = previewTargets
|
|
911
974
|
.map((target) => `${target.nodeId}:${target.role}`)
|
|
912
975
|
.sort()
|
|
913
976
|
.join(',')
|
|
914
977
|
return {
|
|
915
978
|
key: `${node.type}:${node.id}:${role ?? 'unsupported'}:${eraser ? 'erase' : 'paint'}:${scope}:${scopeTargetKey}`,
|
|
916
979
|
hoveredId: node.id as AnyNodeId,
|
|
917
|
-
hoverMode: compatible ? 'paint-ready' : 'paint-disabled',
|
|
918
|
-
paintHover:
|
|
919
|
-
compatible && role
|
|
920
|
-
? {
|
|
921
|
-
scopes: availablePaintScopes({ node, slotRoles }),
|
|
922
|
-
slotLabel: slotDisplayLabel(node, role),
|
|
923
|
-
nodeNoun: node.type,
|
|
924
|
-
}
|
|
925
|
-
: null,
|
|
980
|
+
hoverMode: compatible ? (eraser ? 'erase-ready' : 'paint-ready') : 'paint-disabled',
|
|
981
|
+
paintHover: hover,
|
|
926
982
|
apply:
|
|
927
983
|
compatible && role
|
|
928
984
|
? () => {
|
|
985
|
+
// A surface with nothing of its own to paint (a footprint's
|
|
986
|
+
// top where no room stands), or one a script builds, says
|
|
987
|
+
// so instead of painting.
|
|
988
|
+
const refusal =
|
|
989
|
+
floorPlatePaintRefusal(useScene.getState().nodes, node, role) ??
|
|
990
|
+
wallFaceScriptPaintRefusal(node, role)
|
|
991
|
+
usePaintRegionMode.getState().setNotice(refusal?.message ?? null)
|
|
992
|
+
if (refusal) return
|
|
929
993
|
// Spread targets are all the same slot-model kind, so one
|
|
930
|
-
// batched commit writes them in a single undo step;
|
|
931
|
-
//
|
|
932
|
-
// non-slot kinds too)
|
|
933
|
-
if (scopeTargets.
|
|
994
|
+
// batched commit writes them in a single undo step; only
|
|
995
|
+
// the hovered surface itself keeps the kind's own commit
|
|
996
|
+
// (covers non-slot kinds too) — see `paintCommitRoute`.
|
|
997
|
+
if (paintCommitRoute(scopeTargets, node.id, role) === 'fanout') {
|
|
934
998
|
commitPaintScopeFanout(
|
|
935
999
|
scopeTargets,
|
|
936
1000
|
paintSpec.material,
|
|
@@ -964,10 +1028,14 @@ export const SelectionManager = () => {
|
|
|
964
1028
|
// hovered surface. Each target is the same kind, so its own
|
|
965
1029
|
// paint capability builds the preview; restores combine.
|
|
966
1030
|
const restores: PaintPreviewCleanup[] = []
|
|
967
|
-
const
|
|
1031
|
+
const liveNodes = useScene.getState().nodes
|
|
1032
|
+
if (affected) {
|
|
1033
|
+
usePaintOutline.setState({ surfaces: affected })
|
|
1034
|
+
restores.push(() => usePaintOutline.setState({ surfaces: null }))
|
|
1035
|
+
}
|
|
968
1036
|
try {
|
|
969
|
-
for (const target of
|
|
970
|
-
const targetNode =
|
|
1037
|
+
for (const target of previewTargets) {
|
|
1038
|
+
const targetNode = liveNodes[target.nodeId]
|
|
971
1039
|
const targetRoot = getRegisteredNodeObject(target.nodeId)
|
|
972
1040
|
const targetCap = targetNode
|
|
973
1041
|
? nodeRegistry.get(targetNode.type)?.capabilities?.paint
|
|
@@ -975,6 +1043,8 @@ export const SelectionManager = () => {
|
|
|
975
1043
|
if (!(targetNode && targetRoot && targetCap)) continue
|
|
976
1044
|
const restore = targetCap.applyPreview({
|
|
977
1045
|
node: targetNode,
|
|
1046
|
+
nodes: liveNodes,
|
|
1047
|
+
materials: useScene.getState().materials,
|
|
978
1048
|
role: target.role,
|
|
979
1049
|
material: paintSpec.material,
|
|
980
1050
|
materialPreset: paintSpec.materialPreset,
|
|
@@ -993,82 +1063,50 @@ export const SelectionManager = () => {
|
|
|
993
1063
|
}
|
|
994
1064
|
}
|
|
995
1065
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
: node.parentId
|
|
1002
|
-
? useScene.getState().nodes[node.parentId as AnyNodeId]
|
|
1003
|
-
: null
|
|
1004
|
-
if (roofNode?.type !== 'roof') return null
|
|
1005
|
-
|
|
1006
|
-
const role = resolveRoofMaterialTarget(event as RoofEvent | RoofSegmentEvent)
|
|
1066
|
+
// A segment paints through its definition's `capabilities.paint`; the merged shell's hit is
|
|
1067
|
+
// the roof's, and fans to it.
|
|
1068
|
+
if (node.type === 'roof') {
|
|
1069
|
+
const role = resolveRoofMaterialTarget(event as RoofEvent)
|
|
1070
|
+
if (isPaintPicking()) return pickInteraction(node, role, event, null)
|
|
1007
1071
|
const compatible = role !== null && paintEnabled
|
|
1008
|
-
// Painting directly on a segment (only possible in segment edit
|
|
1009
|
-
// mode, where the per-segment mesh is visible) writes to the
|
|
1010
|
-
// segment's own role-specific fields. Painting the merged shell
|
|
1011
|
-
// — or a roof node directly — keeps fanning to the parent roof.
|
|
1012
|
-
const segmentTarget = isSegmentHit ? (node as RoofSegmentNode) : null
|
|
1013
1072
|
return {
|
|
1014
|
-
key:
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
hoveredId: (segmentTarget ? segmentTarget.id : roofNode.id) as AnyNodeId,
|
|
1018
|
-
hoverMode: compatible ? 'paint-ready' : 'paint-disabled',
|
|
1073
|
+
key: `roof:${node.id}:${role ?? 'unsupported'}:${eraser ? 'erase' : 'paint'}`,
|
|
1074
|
+
hoveredId: node.id as AnyNodeId,
|
|
1075
|
+
hoverMode: compatible ? (eraser ? 'erase-ready' : 'paint-ready') : 'paint-disabled',
|
|
1019
1076
|
// Roof isn't on the slot model (role-specific fields, custom commit),
|
|
1020
1077
|
// so it offers only the single surface — but still labels it.
|
|
1021
1078
|
paintHover:
|
|
1022
1079
|
compatible && role
|
|
1023
1080
|
? {
|
|
1024
1081
|
scopes: ['single'],
|
|
1025
|
-
slotLabel: slotDisplayLabel(
|
|
1082
|
+
slotLabel: slotDisplayLabel(node, role),
|
|
1026
1083
|
nodeNoun: 'roof',
|
|
1027
1084
|
}
|
|
1028
1085
|
: null,
|
|
1029
1086
|
apply:
|
|
1030
1087
|
compatible && role
|
|
1031
1088
|
? () => {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
buildRoofSegmentSurfaceMaterialPatch(
|
|
1037
|
-
segmentTarget,
|
|
1038
|
-
role,
|
|
1039
|
-
paintSpec.material,
|
|
1040
|
-
paintSpec.materialPreset,
|
|
1041
|
-
),
|
|
1042
|
-
)
|
|
1043
|
-
} else {
|
|
1044
|
-
sceneState.updateNode(
|
|
1045
|
-
roofNode.id as AnyNodeId,
|
|
1089
|
+
useScene
|
|
1090
|
+
.getState()
|
|
1091
|
+
.updateNode(
|
|
1092
|
+
node.id as AnyNodeId,
|
|
1046
1093
|
buildRoofSurfaceMaterialPatch(
|
|
1047
|
-
|
|
1094
|
+
node,
|
|
1048
1095
|
role,
|
|
1049
1096
|
paintSpec.material,
|
|
1050
1097
|
paintSpec.materialPreset,
|
|
1051
1098
|
),
|
|
1052
1099
|
)
|
|
1053
|
-
}
|
|
1054
1100
|
}
|
|
1055
1101
|
: null,
|
|
1056
1102
|
preview:
|
|
1057
1103
|
compatible && role
|
|
1058
|
-
? () =>
|
|
1059
|
-
segmentTarget
|
|
1060
|
-
? applyRoofSegmentPaintPreview(
|
|
1061
|
-
segmentTarget,
|
|
1062
|
-
roofNode as RoofNode,
|
|
1063
|
-
role,
|
|
1064
|
-
paintSpec,
|
|
1065
|
-
)
|
|
1066
|
-
: applyRoofPaintPreview(roofNode as RoofNode, role, paintSpec)
|
|
1104
|
+
? () => applyRoofPaintPreview(node, role, paintSpec)
|
|
1067
1105
|
: () => previewCursor('not-allowed'),
|
|
1068
1106
|
}
|
|
1069
1107
|
}
|
|
1070
1108
|
|
|
1071
|
-
// Only `roof`
|
|
1109
|
+
// Only `roof` reaches a legacy paint arm (above) — every
|
|
1072
1110
|
// other paintable kind declares `capabilities.paint` and returns from the
|
|
1073
1111
|
// registry-driven dispatch at the top of this function.
|
|
1074
1112
|
|
|
@@ -1091,17 +1129,20 @@ export const SelectionManager = () => {
|
|
|
1091
1129
|
previewOwner.wrap(resolvePaintInteraction(event))
|
|
1092
1130
|
|
|
1093
1131
|
const onEnter = (event: NodeEvent) => {
|
|
1132
|
+
if (isCeilingGridHit(event)) return
|
|
1094
1133
|
// A host-driven drag (handle resize/rotate) sets `inputDragging`.
|
|
1095
1134
|
// useNodeEvents now emits hover events during such a drag so surface
|
|
1096
1135
|
// move tools keep tracking the cursor — but paint preview must not fire
|
|
1097
1136
|
// mid-drag, so gate on `inputDragging` here too.
|
|
1098
1137
|
if (boxSelectHandled || useViewer.getState().inputDragging) return
|
|
1138
|
+
if (claimedByNearer(event)) return
|
|
1099
1139
|
|
|
1100
1140
|
const interaction = getPaintInteraction(event)
|
|
1101
1141
|
if (!interaction) return
|
|
1102
1142
|
|
|
1103
|
-
event
|
|
1143
|
+
claim(event)
|
|
1104
1144
|
lastEnterEvent = event
|
|
1145
|
+
hoveredNodeId = interaction.hoveredId
|
|
1105
1146
|
|
|
1106
1147
|
// Drive the paint HUD off this hover: the interaction carries the scopes +
|
|
1107
1148
|
// labels for the painted surface (`null` when it isn't paintable — no
|
|
@@ -1123,36 +1164,43 @@ export const SelectionManager = () => {
|
|
|
1123
1164
|
}
|
|
1124
1165
|
|
|
1125
1166
|
const onLeave = (event: NodeEvent) => {
|
|
1126
|
-
|
|
1127
|
-
|
|
1167
|
+
if (isCeilingGridHit(event)) return
|
|
1168
|
+
// Only the hovered node leaving ends the hover; a surface behind it (under
|
|
1169
|
+
// an opening that never stops the pointer) leaving says nothing about it.
|
|
1170
|
+
// This goes by the node, not by resolving the hit again: a leave event's
|
|
1171
|
+
// ray no longer meets the node, so that resolved to nothing and every
|
|
1172
|
+
// leave was ignored — the last surface stayed previewed, with its HUD.
|
|
1173
|
+
if (event.node.id !== hoveredNodeId) return
|
|
1128
1174
|
|
|
1129
1175
|
// Leaving any surface → the HUD shows the "hover a surface" hint again.
|
|
1176
|
+
hoveredNodeId = null
|
|
1130
1177
|
lastEnterEvent = null
|
|
1131
1178
|
useEditor.getState().setPaintHover(null)
|
|
1132
|
-
|
|
1133
|
-
if (activePreview?.key !== interaction.key) {
|
|
1134
|
-
return
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1179
|
+
usePaintOutline.setState({ surfaces: null })
|
|
1137
1180
|
clearActivePreview()
|
|
1138
|
-
if (useViewer.getState().hoveredId ===
|
|
1181
|
+
if (useViewer.getState().hoveredId === event.node.id) {
|
|
1139
1182
|
useViewer.setState({ hoveredId: null })
|
|
1140
1183
|
}
|
|
1141
1184
|
setHoverHighlightMode('default')
|
|
1142
1185
|
}
|
|
1143
1186
|
|
|
1144
1187
|
const onClick = (event: NodeEvent) => {
|
|
1188
|
+
if (isCeilingGridHit(event)) return
|
|
1145
1189
|
if (boxSelectHandled) return
|
|
1190
|
+
if (claimedByNearer(event)) return
|
|
1146
1191
|
|
|
1147
1192
|
const interaction = getPaintInteraction(event)
|
|
1148
1193
|
if (!interaction) return
|
|
1149
1194
|
|
|
1150
|
-
event
|
|
1195
|
+
claim(event)
|
|
1151
1196
|
|
|
1152
1197
|
if (!interaction.apply) {
|
|
1153
1198
|
return
|
|
1154
1199
|
}
|
|
1155
1200
|
|
|
1201
|
+
// Picking switches sub-mode synchronously and replays the hover, replacing
|
|
1202
|
+
// the event claim. Consume this click before a farther hit can paint.
|
|
1203
|
+
if (isPaintPicking()) event.stopPropagation()
|
|
1156
1204
|
interaction.apply()
|
|
1157
1205
|
sfxEmitter.emit('sfx:paint-apply')
|
|
1158
1206
|
if (activePreview?.key === interaction.key) {
|
|
@@ -1202,11 +1250,22 @@ export const SelectionManager = () => {
|
|
|
1202
1250
|
const unsubscribePaintScope = useEditor.subscribe((state, prev) => {
|
|
1203
1251
|
if (state.paintScope === prev.paintScope || !lastEnterEvent) return
|
|
1204
1252
|
clearActivePreview()
|
|
1253
|
+
claimedEvent = null
|
|
1254
|
+
onEnter(lastEnterEvent)
|
|
1255
|
+
})
|
|
1256
|
+
const releasePickHold = bindPaintPickHold(window)
|
|
1257
|
+
// So does switching the sub-mode (the eyedropper held on Alt, a mode icon).
|
|
1258
|
+
const unsubscribePaintMode = usePaintRegionMode.subscribe((state, prev) => {
|
|
1259
|
+
if (state.mode === prev.mode || !lastEnterEvent) return
|
|
1260
|
+
clearActivePreview()
|
|
1261
|
+
claimedEvent = null
|
|
1205
1262
|
onEnter(lastEnterEvent)
|
|
1206
1263
|
})
|
|
1207
1264
|
|
|
1208
1265
|
return () => {
|
|
1209
1266
|
unsubscribePaintScope()
|
|
1267
|
+
unsubscribePaintMode()
|
|
1268
|
+
releasePickHold()
|
|
1210
1269
|
for (const type of subscribedKinds) {
|
|
1211
1270
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
|
1212
1271
|
emitter.off(`${type}:move` as any, onEnter as any)
|
|
@@ -1215,6 +1274,7 @@ export const SelectionManager = () => {
|
|
|
1215
1274
|
}
|
|
1216
1275
|
clearActivePreview()
|
|
1217
1276
|
useViewer.setState({ hoveredId: null })
|
|
1277
|
+
usePaintOutline.setState({ surfaces: null })
|
|
1218
1278
|
setHoverHighlightMode('default')
|
|
1219
1279
|
useEditor.getState().setPaintHover(null)
|
|
1220
1280
|
}
|
|
@@ -1260,9 +1320,20 @@ export const SelectionManager = () => {
|
|
|
1260
1320
|
if (movingNode || isCurveReshape) return
|
|
1261
1321
|
|
|
1262
1322
|
const onPointerDown = (event: NodeEvent) => {
|
|
1323
|
+
if (isCeilingGridHit(event)) return
|
|
1263
1324
|
if (!selectionEnabled(useInteractionScope.getState().scope)) return
|
|
1325
|
+
if (useEditor.getState().phase === 'site') return
|
|
1264
1326
|
const pointer = pointerEventFromNodeEvent(event)
|
|
1265
|
-
if (pointer.button !== 0) return
|
|
1327
|
+
if (pointer.button !== 0 || pointer.altKey) return
|
|
1328
|
+
if (
|
|
1329
|
+
roomPickingEnabled() &&
|
|
1330
|
+
shouldInterceptRoom(
|
|
1331
|
+
roomForEvent(event),
|
|
1332
|
+
selectionModifiersFromEvent(pointer),
|
|
1333
|
+
event.node.id,
|
|
1334
|
+
)
|
|
1335
|
+
)
|
|
1336
|
+
return
|
|
1266
1337
|
const handleOwnsPointer = pointerEventHitsEditorHandle(event.nativeEvent)
|
|
1267
1338
|
|
|
1268
1339
|
// Plain press on a transformable member of a multi-selection arms the
|
|
@@ -1577,6 +1648,7 @@ export const SelectionManager = () => {
|
|
|
1577
1648
|
if (movingNode || isCurveReshape) return
|
|
1578
1649
|
|
|
1579
1650
|
const onClick = (event: NodeEvent) => {
|
|
1651
|
+
if (isCeilingGridHit(event)) return
|
|
1580
1652
|
// Skip if box-select just completed (drag ended over a node)
|
|
1581
1653
|
if (boxSelectHandled) return
|
|
1582
1654
|
|
|
@@ -1588,8 +1660,11 @@ export const SelectionManager = () => {
|
|
|
1588
1660
|
// body click so only the reshape tool handles the release. (Scoped to
|
|
1589
1661
|
// `endpoint`: hole-edit relies on node clicks to exit, just below.)
|
|
1590
1662
|
const activeScope = useInteractionScope.getState().scope
|
|
1591
|
-
if (
|
|
1592
|
-
|
|
1663
|
+
if (
|
|
1664
|
+
!selectionEnabled(activeScope) &&
|
|
1665
|
+
!(activeScope.kind === 'reshaping' && activeScope.reshape === 'hole')
|
|
1666
|
+
)
|
|
1667
|
+
return
|
|
1593
1668
|
|
|
1594
1669
|
if (dispatchSceneAction(event.node, getEventObject(event))) {
|
|
1595
1670
|
event.stopPropagation()
|
|
@@ -1606,21 +1681,14 @@ export const SelectionManager = () => {
|
|
|
1606
1681
|
selectedIds: useViewer.getState().selection.selectedIds,
|
|
1607
1682
|
})
|
|
1608
1683
|
|
|
1609
|
-
// A ceiling is selectable only through its corner handles, never via
|
|
1610
|
-
// the `ceiling-grid` body mesh. When the grid is revealed (ceiling
|
|
1611
|
-
// selected, or an item placed beneath it) a top-down click hits the
|
|
1612
|
-
// grid first; selecting the ceiling there both re-selects it as a
|
|
1613
|
-
// no-op and stops propagation, blocking the hosted item below. By
|
|
1614
|
-
// ignoring non-handle ceiling clicks (without stopping propagation)
|
|
1615
|
-
// the click falls through to the item underneath.
|
|
1616
|
-
if (node.type === 'ceiling' && !event.viaHandle) return
|
|
1617
|
-
|
|
1618
1684
|
// Unit focus turns clicks inside a zone into the paint gesture:
|
|
1619
1685
|
// membership toggles, the zone stays unselected, focus stays.
|
|
1620
1686
|
const focusedUnitId = useViewer.getState().focusedUnitId
|
|
1621
1687
|
if (focusedUnitId) {
|
|
1622
1688
|
const zone =
|
|
1623
|
-
node.type === 'zone'
|
|
1689
|
+
node.type === 'zone'
|
|
1690
|
+
? node
|
|
1691
|
+
: zoneAtWorldPoint(event.position[0], event.position[2], event.position[1])
|
|
1624
1692
|
if (zone) {
|
|
1625
1693
|
event.stopPropagation()
|
|
1626
1694
|
clickHandledRef.current = true
|
|
@@ -1632,35 +1700,42 @@ export const SelectionManager = () => {
|
|
|
1632
1700
|
}
|
|
1633
1701
|
}
|
|
1634
1702
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
useEditor.getState().setPhase(target.phase)
|
|
1647
|
-
currentPhase = target.phase
|
|
1648
|
-
}
|
|
1703
|
+
// From site the house is one thing: a hit anywhere on it picks its
|
|
1704
|
+
// building and goes inside, where the next click picks a room or element.
|
|
1705
|
+
if (useEditor.getState().phase === 'site') {
|
|
1706
|
+
if (!enterBuildingFromCanvas(node)) return
|
|
1707
|
+
event.stopPropagation()
|
|
1708
|
+
clickHandledRef.current = true
|
|
1709
|
+
setTimeout(() => {
|
|
1710
|
+
clickHandledRef.current = false
|
|
1711
|
+
}, 50)
|
|
1712
|
+
return
|
|
1713
|
+
}
|
|
1649
1714
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1715
|
+
if (isNodeInCurrentLevel(node) && roomPickingEnabled()) {
|
|
1716
|
+
const room = roomForEvent(event)
|
|
1717
|
+
if (
|
|
1718
|
+
room &&
|
|
1719
|
+
shouldInterceptRoom(
|
|
1720
|
+
room,
|
|
1721
|
+
selectionModifiersFromEvent(event.nativeEvent, modifierKeysRef.current),
|
|
1722
|
+
node.id,
|
|
1723
|
+
)
|
|
1724
|
+
) {
|
|
1725
|
+
event.stopPropagation()
|
|
1726
|
+
clickHandledRef.current = true
|
|
1727
|
+
setTimeout(() => {
|
|
1728
|
+
clickHandledRef.current = false
|
|
1729
|
+
}, 50)
|
|
1730
|
+
selectRoom(room)
|
|
1731
|
+
return
|
|
1659
1732
|
}
|
|
1733
|
+
useEditor.getState().setHoveredRoom(null)
|
|
1660
1734
|
}
|
|
1661
1735
|
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1736
|
+
const selectedIdsBeforeRouting = useViewer.getState().selection.selectedIds
|
|
1737
|
+
const activeStrategy = SELECTION_STRATEGIES.building
|
|
1738
|
+
if (activeStrategy.isValid(node)) {
|
|
1664
1739
|
event.stopPropagation()
|
|
1665
1740
|
clickHandledRef.current = true
|
|
1666
1741
|
// Reset the handled flag after a short delay so the grid:click that the
|
|
@@ -1673,13 +1748,22 @@ export const SelectionManager = () => {
|
|
|
1673
1748
|
clickHandledRef.current = false
|
|
1674
1749
|
}, 50)
|
|
1675
1750
|
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1751
|
+
// Room first: a raised or sunken room's plate and a mezzanine's plate
|
|
1752
|
+
// belong to their room, never selected on their own. Past the room (the
|
|
1753
|
+
// drill click, or Alt) a raised or sunken room continues to the
|
|
1754
|
+
// footprint floor it stands on, like a ground-level room; a mezzanine
|
|
1755
|
+
// stays on its mezzanine.
|
|
1756
|
+
let footprint: SlabNode | null = null
|
|
1757
|
+
if (roomPickingEnabled() && isRoomOwnedPlate(node)) {
|
|
1758
|
+
footprint = roomOwnedPlateDrillTarget(useScene.getState().nodes, node as SlabNode)
|
|
1759
|
+
if (!footprint) {
|
|
1760
|
+
const hit = roomForEvent(event)
|
|
1761
|
+
if (hit) selectRoom(hit)
|
|
1762
|
+
return
|
|
1681
1763
|
}
|
|
1682
1764
|
}
|
|
1765
|
+
|
|
1766
|
+
let nodeToSelect: AnyNode = footprint ?? node
|
|
1683
1767
|
nodeToSelect = resolveCanvasSelectionNode({
|
|
1684
1768
|
node: nodeToSelect,
|
|
1685
1769
|
nodes: useScene.getState().nodes,
|
|
@@ -1701,16 +1785,21 @@ export const SelectionManager = () => {
|
|
|
1701
1785
|
// Move button. The first (selecting) click can't hit this because the
|
|
1702
1786
|
// node isn't yet in `selectedIdsBeforeRouting`.
|
|
1703
1787
|
const nativeEvent = event.nativeEvent
|
|
1704
|
-
const hasModifier =
|
|
1788
|
+
const hasModifier =
|
|
1789
|
+
nativeEvent.altKey || nativeEvent.shiftKey || isCommandModifier(nativeEvent)
|
|
1705
1790
|
const isAlreadySole =
|
|
1706
1791
|
selectedIdsBeforeRouting.length === 1 && selectedIdsBeforeRouting[0] === nodeToSelect.id
|
|
1707
1792
|
if (
|
|
1708
1793
|
useEditor.getState().mode !== 'delete' &&
|
|
1709
1794
|
!hasModifier &&
|
|
1710
1795
|
isAlreadySole &&
|
|
1796
|
+
!expandSessionSelectionForNode(nodeToSelect.id) &&
|
|
1711
1797
|
!getMovingNode() &&
|
|
1712
1798
|
canDirectMoveNode(nodeToSelect)
|
|
1713
1799
|
) {
|
|
1800
|
+
// The move tool mounts before the browser's click that ends this
|
|
1801
|
+
// gesture, which would otherwise commit the node where it stands.
|
|
1802
|
+
swallowNextClick()
|
|
1714
1803
|
sfxEmitter.emit('sfx:item-pick')
|
|
1715
1804
|
const moveTarget = resolveDirectManipulationNode(nodeToSelect, useScene.getState().nodes)
|
|
1716
1805
|
useEditor.getState().setMovingNode(moveTarget as never)
|
|
@@ -1732,7 +1821,7 @@ export const SelectionManager = () => {
|
|
|
1732
1821
|
// `capabilities.paint` route through this entry — wall,
|
|
1733
1822
|
// chimney, dormer use it today. The legacy stair / roof /
|
|
1734
1823
|
// single-surface arms below stay until they migrate too.
|
|
1735
|
-
if (nodeToSelect.type === node.type) {
|
|
1824
|
+
if (!footprint && nodeToSelect.type === node.type) {
|
|
1736
1825
|
const paintCap = nodeRegistry.get(node.type)?.capabilities?.paint
|
|
1737
1826
|
if (paintCap) {
|
|
1738
1827
|
const materialIndex = getIntersectionMaterialIndex(
|
|
@@ -1829,27 +1918,23 @@ export const SelectionManager = () => {
|
|
|
1829
1918
|
const onGridClick = (event: GridEvent) => {
|
|
1830
1919
|
if (clickHandledRef.current) return
|
|
1831
1920
|
if (boxSelectHandled) return
|
|
1832
|
-
if (useInteractionScope.getState().scope
|
|
1921
|
+
if (!selectionEnabled(useInteractionScope.getState().scope)) return
|
|
1833
1922
|
const nativeEvent = event.nativeEvent
|
|
1834
1923
|
if (nativeEvent?.metaKey || nativeEvent?.ctrlKey || nativeEvent?.shiftKey) return
|
|
1835
1924
|
// Unit focus: a ground click inside a zone paints it; elsewhere it
|
|
1836
1925
|
// leaves focus and the unit selection alone.
|
|
1837
1926
|
const focusedUnitId = useViewer.getState().focusedUnitId
|
|
1838
1927
|
if (focusedUnitId) {
|
|
1839
|
-
const zone = zoneAtLevelPoint(
|
|
1928
|
+
const zone = zoneAtLevelPoint(
|
|
1929
|
+
event.localPosition[0],
|
|
1930
|
+
event.localPosition[2],
|
|
1931
|
+
event.localPosition[1],
|
|
1932
|
+
)
|
|
1840
1933
|
if (zone) paintZoneMembership(focusedUnitId, zone.id)
|
|
1841
1934
|
return
|
|
1842
1935
|
}
|
|
1843
|
-
|
|
1844
|
-
const activeStrategy = SELECTION_STRATEGIES[phase]
|
|
1845
|
-
if (activeStrategy) activeStrategy.handleDeselect()
|
|
1936
|
+
SELECTION_STRATEGIES[useEditor.getState().phase].handleDeselect()
|
|
1846
1937
|
useEditor.getState().setSelectedMaterialTarget(null)
|
|
1847
|
-
|
|
1848
|
-
// When deselecting from zone mode, return to structure select
|
|
1849
|
-
if (phase === 'structure' && structureLayer === 'zones') {
|
|
1850
|
-
useEditor.getState().setStructureLayer('elements')
|
|
1851
|
-
useEditor.getState().setMode('select')
|
|
1852
|
-
}
|
|
1853
1938
|
}
|
|
1854
1939
|
emitter.on('grid:click', onGridClick)
|
|
1855
1940
|
|
|
@@ -1861,7 +1946,7 @@ export const SelectionManager = () => {
|
|
|
1861
1946
|
}
|
|
1862
1947
|
}, [isCurveReshape, mode, movingNode, registryVersion])
|
|
1863
1948
|
|
|
1864
|
-
//
|
|
1949
|
+
// Hover and double-click
|
|
1865
1950
|
useEffect(() => {
|
|
1866
1951
|
// re-subscribe when plugin kinds register after mount (async plugin load)
|
|
1867
1952
|
void registryVersion
|
|
@@ -1869,96 +1954,94 @@ export const SelectionManager = () => {
|
|
|
1869
1954
|
if (movingNode || isCurveReshape) return
|
|
1870
1955
|
|
|
1871
1956
|
const onEnter = (event: NodeEvent) => {
|
|
1872
|
-
if (
|
|
1957
|
+
if (isCeilingGridHit(event)) return
|
|
1958
|
+
if (!selectionEnabled(useInteractionScope.getState().scope)) return
|
|
1873
1959
|
// A host-driven drag (handle resize/rotate, box-select) sets
|
|
1874
1960
|
// `inputDragging`. useNodeEvents still emits hover events during it so
|
|
1875
1961
|
// surface move tools keep tracking — but the select-hover outline must
|
|
1876
1962
|
// stay put, so don't repaint under the cursor mid-drag.
|
|
1877
1963
|
if (useViewer.getState().inputDragging) return
|
|
1964
|
+
// An editor handle anywhere along the ray owns the pointer — it takes the
|
|
1965
|
+
// press (handles draw on top of walls), so it takes the hover too: no
|
|
1966
|
+
// stopPropagation, so the handle behind the wall still hears its enter.
|
|
1967
|
+
if (pointerEventHitsEditorHandle(event.nativeEvent)) {
|
|
1968
|
+
clearSelectHover(event)
|
|
1969
|
+
return
|
|
1970
|
+
}
|
|
1878
1971
|
const node = resolveCanvasSelectionNode({
|
|
1879
1972
|
node: resolveSelectModeNodeTarget(event),
|
|
1880
1973
|
nodes: useScene.getState().nodes,
|
|
1881
1974
|
selectedIds: useViewer.getState().selection.selectedIds,
|
|
1882
1975
|
})
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
// Ignore zones unless specifically in zones layer
|
|
1893
|
-
if (node.type === 'zone') {
|
|
1894
|
-
if (currentPhase !== 'structure' || useEditor.getState().structureLayer !== 'zones') {
|
|
1895
|
-
return
|
|
1896
|
-
}
|
|
1976
|
+
// From site the click picks the whole building, so the hover shows it.
|
|
1977
|
+
if (useEditor.getState().phase === 'site') {
|
|
1978
|
+
const buildingId = resolveCanvasBuildingId(node, useScene.getState().nodes)
|
|
1979
|
+
if (!buildingId) return
|
|
1980
|
+
event.stopPropagation()
|
|
1981
|
+
useViewer.getState().setHoveredId(buildingId as AnyNodeId)
|
|
1982
|
+
return
|
|
1897
1983
|
}
|
|
1898
1984
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1985
|
+
if (node.type === 'building' || node.type === 'site') return
|
|
1986
|
+
// Rooms hover through their walls and floors; a zone's own volume never does.
|
|
1987
|
+
if (node.type === 'zone') return
|
|
1988
|
+
if (!isNodeInCurrentLevel(node)) return
|
|
1903
1989
|
|
|
1904
1990
|
event.stopPropagation()
|
|
1905
|
-
|
|
1991
|
+
if (
|
|
1992
|
+
roomPickingEnabled() &&
|
|
1993
|
+
hoverRoomFromHit(
|
|
1994
|
+
roomForEvent(event),
|
|
1995
|
+
selectionModifiersFromEvent(event.nativeEvent, modifierKeysRef.current),
|
|
1996
|
+
node.id,
|
|
1997
|
+
)
|
|
1998
|
+
)
|
|
1999
|
+
return
|
|
2000
|
+
useViewer.getState().setHoveredId(node.id)
|
|
1906
2001
|
}
|
|
1907
2002
|
|
|
1908
|
-
const
|
|
2003
|
+
const clearSelectHover = (event: NodeEvent) => {
|
|
2004
|
+
useEditor.getState().setHoveredRoom(null)
|
|
1909
2005
|
if (useViewer.getState().inputDragging) return
|
|
1910
|
-
const
|
|
2006
|
+
const nodes = useScene.getState().nodes
|
|
2007
|
+
const node = resolveCanvasSelectionNode({
|
|
1911
2008
|
node: resolveSelectModeNodeTarget(event),
|
|
1912
|
-
nodes
|
|
2009
|
+
nodes,
|
|
1913
2010
|
selectedIds: useViewer.getState().selection.selectedIds,
|
|
1914
|
-
})
|
|
2011
|
+
})
|
|
2012
|
+
const nodeId =
|
|
2013
|
+
useEditor.getState().phase === 'site' ? resolveCanvasBuildingId(node, nodes) : node.id
|
|
1915
2014
|
if (nodeId && useViewer.getState().hoveredId === nodeId) {
|
|
1916
2015
|
useViewer.setState({ hoveredId: null })
|
|
1917
2016
|
}
|
|
1918
2017
|
}
|
|
1919
2018
|
|
|
2019
|
+
const onLeave = (event: NodeEvent) => {
|
|
2020
|
+
if (isCeilingGridHit(event)) return
|
|
2021
|
+
clearSelectHover(event)
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1920
2024
|
const onDoubleClick = (event: NodeEvent) => {
|
|
2025
|
+
if (isCeilingGridHit(event)) return
|
|
1921
2026
|
if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
|
|
1922
|
-
|
|
1923
|
-
node: resolveSelectModeNodeTarget(event),
|
|
2027
|
+
const node = resolveCanvasSelectionNode({
|
|
2028
|
+
node: resolveSelectModeNodeTarget(event, true),
|
|
1924
2029
|
nodes: useScene.getState().nodes,
|
|
1925
2030
|
selectedIds: useViewer.getState().selection.selectedIds,
|
|
1926
2031
|
})
|
|
1927
|
-
|
|
1928
|
-
const currentPhase = useEditor.getState().phase
|
|
1929
|
-
|
|
1930
|
-
const selectedIdsBeforeRouting = useViewer.getState().selection.selectedIds
|
|
1931
|
-
const target = resolveNodeSelectionTarget(node)
|
|
1932
|
-
let targetPhase: 'site' | 'structure' | 'furnish' | null = target?.phase ?? null
|
|
1933
|
-
let targetStructureLayer = target?.structureLayer
|
|
1934
|
-
let forceSelect = false
|
|
1935
|
-
|
|
1936
|
-
if (node.type === 'building' || node.type === 'site') {
|
|
1937
|
-
if (currentPhase === 'structure' || currentPhase === 'furnish') {
|
|
1938
|
-
return // Ignore building/site double clicks if we are already inside a building
|
|
1939
|
-
}
|
|
1940
|
-
if (node.type === 'building') {
|
|
1941
|
-
targetPhase = 'structure'
|
|
1942
|
-
targetStructureLayer = 'elements'
|
|
1943
|
-
}
|
|
1944
|
-
} else {
|
|
1945
|
-
if (node.type === 'roof-segment' && currentPhase === 'structure') {
|
|
1946
|
-
forceSelect = true // allow double click to dive into roof-segment even if already in structure phase
|
|
1947
|
-
}
|
|
1948
|
-
if (node.type === 'stair-segment' && currentPhase === 'structure') {
|
|
1949
|
-
forceSelect = true // allow double click to dive into stair-segment even if already in structure phase
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
2032
|
+
if (useEditor.getState().phase !== 'building' || !isNodeInCurrentLevel(node)) return
|
|
1952
2033
|
|
|
1953
2034
|
// While a unit is focused a double-click inside a zone selects that
|
|
1954
2035
|
// zone (focus stays); the two clicks before it cancel each other's paint.
|
|
1955
2036
|
if (useViewer.getState().focusedUnitId) {
|
|
1956
2037
|
const zone =
|
|
1957
|
-
node.type === 'zone'
|
|
2038
|
+
node.type === 'zone'
|
|
2039
|
+
? node
|
|
2040
|
+
: zoneAtWorldPoint(event.position[0], event.position[2], event.position[1])
|
|
1958
2041
|
if (zone) {
|
|
1959
2042
|
event.stopPropagation()
|
|
1960
2043
|
cancelPendingZonePaint(zone.id)
|
|
1961
|
-
SELECTION_STRATEGIES.
|
|
2044
|
+
SELECTION_STRATEGIES.building.handleSelect(
|
|
1962
2045
|
zone,
|
|
1963
2046
|
event.nativeEvent,
|
|
1964
2047
|
modifierKeysRef.current,
|
|
@@ -1968,34 +2051,16 @@ export const SelectionManager = () => {
|
|
|
1968
2051
|
}
|
|
1969
2052
|
}
|
|
1970
2053
|
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
if ((targetPhase && targetPhase !== useEditor.getState().phase) || forceSelect) {
|
|
2054
|
+
// A single click selects the whole roof or stair; the double-click dives
|
|
2055
|
+
// into the segment under the cursor.
|
|
2056
|
+
if (node.type === 'roof-segment' || node.type === 'stair-segment') {
|
|
1976
2057
|
event.stopPropagation()
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
targetPhase === 'structure' &&
|
|
1984
|
-
targetStructureLayer &&
|
|
1985
|
-
targetStructureLayer !== useEditor.getState().structureLayer
|
|
1986
|
-
) {
|
|
1987
|
-
useEditor.getState().setStructureLayer(targetStructureLayer)
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
const strategy = SELECTION_STRATEGIES[targetPhase || currentPhase]
|
|
1991
|
-
if (strategy) {
|
|
1992
|
-
strategy.handleSelect(
|
|
1993
|
-
node,
|
|
1994
|
-
event.nativeEvent,
|
|
1995
|
-
modifierKeysRef.current,
|
|
1996
|
-
selectedIdsBeforeRouting,
|
|
1997
|
-
)
|
|
1998
|
-
}
|
|
2058
|
+
SELECTION_STRATEGIES.building.handleSelect(
|
|
2059
|
+
node,
|
|
2060
|
+
event.nativeEvent,
|
|
2061
|
+
modifierKeysRef.current,
|
|
2062
|
+
useViewer.getState().selection.selectedIds,
|
|
2063
|
+
)
|
|
1999
2064
|
}
|
|
2000
2065
|
}
|
|
2001
2066
|
|
|
@@ -2025,6 +2090,7 @@ export const SelectionManager = () => {
|
|
|
2025
2090
|
|
|
2026
2091
|
subscribedKinds.forEach((type) => {
|
|
2027
2092
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
|
2093
|
+
emitter.on(`${type}:move` as any, onEnter as any)
|
|
2028
2094
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
|
2029
2095
|
emitter.on(`${type}:double-click` as any, onDoubleClick as any)
|
|
2030
2096
|
})
|
|
@@ -2032,6 +2098,7 @@ export const SelectionManager = () => {
|
|
|
2032
2098
|
return () => {
|
|
2033
2099
|
subscribedKinds.forEach((type) => {
|
|
2034
2100
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
|
2101
|
+
emitter.off(`${type}:move` as any, onEnter as any)
|
|
2035
2102
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
|
2036
2103
|
emitter.off(`${type}:double-click` as any, onDoubleClick as any)
|
|
2037
2104
|
})
|
|
@@ -2045,6 +2112,7 @@ export const SelectionManager = () => {
|
|
|
2045
2112
|
if (mode !== 'delete') return
|
|
2046
2113
|
|
|
2047
2114
|
const onClick = (event: NodeEvent) => {
|
|
2115
|
+
if (isCeilingGridHit(event)) return
|
|
2048
2116
|
const node = event.node
|
|
2049
2117
|
if (!isNodeInCurrentLevel(node)) return
|
|
2050
2118
|
|
|
@@ -2063,14 +2131,16 @@ export const SelectionManager = () => {
|
|
|
2063
2131
|
}
|
|
2064
2132
|
|
|
2065
2133
|
const onEnter = (event: NodeEvent) => {
|
|
2134
|
+
if (isCeilingGridHit(event)) return
|
|
2066
2135
|
const node = event.node
|
|
2067
2136
|
if (!isNodeInCurrentLevel(node)) return
|
|
2068
2137
|
if (node.type === 'building' || node.type === 'site') return
|
|
2069
2138
|
event.stopPropagation()
|
|
2070
|
-
useViewer.
|
|
2139
|
+
useViewer.getState().setHoveredId(node.id)
|
|
2071
2140
|
}
|
|
2072
2141
|
|
|
2073
2142
|
const onLeave = (event: NodeEvent) => {
|
|
2143
|
+
if (isCeilingGridHit(event)) return
|
|
2074
2144
|
const nodeId = event?.node?.id
|
|
2075
2145
|
if (nodeId && useViewer.getState().hoveredId === nodeId) {
|
|
2076
2146
|
useViewer.setState({ hoveredId: null })
|
|
@@ -2121,6 +2191,7 @@ export const SelectionManager = () => {
|
|
|
2121
2191
|
<SelectionStateSync />
|
|
2122
2192
|
<SelectionMaterialSync />
|
|
2123
2193
|
<EditorOutlinerSync />
|
|
2194
|
+
<RoomHighlight3D />
|
|
2124
2195
|
</>
|
|
2125
2196
|
)
|
|
2126
2197
|
}
|
|
@@ -2372,39 +2443,18 @@ const EditorOutlinerSync = () => {
|
|
|
2372
2443
|
const registryVersion = useRegistryVersion()
|
|
2373
2444
|
const outliner = useViewer((s) => s.outliner)
|
|
2374
2445
|
const nodes = useScene((s) => s.nodes)
|
|
2446
|
+
const paintOutline = usePaintOutline((s) => s.surfaces)
|
|
2375
2447
|
|
|
2376
2448
|
useEffect(() => {
|
|
2377
2449
|
void geometryRevision
|
|
2378
2450
|
void registryVersion
|
|
2379
2451
|
let idsToHighlight: string[] = []
|
|
2380
2452
|
|
|
2381
|
-
// 1.
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
break
|
|
2387
|
-
|
|
2388
|
-
case 'structure':
|
|
2389
|
-
// Highlight selected items (walls/slabs)
|
|
2390
|
-
// We IGNORE buildingId even if it's set in the store
|
|
2391
|
-
idsToHighlight = Array.from(new Set([...selection.selectedIds, ...previewSelectedIds]))
|
|
2392
|
-
break
|
|
2393
|
-
|
|
2394
|
-
case 'furnish':
|
|
2395
|
-
// Highlight selected furniture/items
|
|
2396
|
-
idsToHighlight = Array.from(new Set([...selection.selectedIds, ...previewSelectedIds]))
|
|
2397
|
-
break
|
|
2398
|
-
|
|
2399
|
-
default:
|
|
2400
|
-
// Pure Viewer mode: Highlight based on the "deepest" selection
|
|
2401
|
-
if (selection.selectedIds.length > 0 || previewSelectedIds.length > 0) {
|
|
2402
|
-
idsToHighlight = Array.from(new Set([...selection.selectedIds, ...previewSelectedIds]))
|
|
2403
|
-
} else if (selection.levelId) {
|
|
2404
|
-
idsToHighlight = [selection.levelId]
|
|
2405
|
-
} else if (selection.buildingId) {
|
|
2406
|
-
idsToHighlight = [selection.buildingId]
|
|
2407
|
-
}
|
|
2453
|
+
// 1. Site outlines the building; inside it, the selection.
|
|
2454
|
+
if (phase === 'site') {
|
|
2455
|
+
if (selection.buildingId) idsToHighlight = [selection.buildingId]
|
|
2456
|
+
} else {
|
|
2457
|
+
idsToHighlight = Array.from(new Set([...selection.selectedIds, ...previewSelectedIds]))
|
|
2408
2458
|
}
|
|
2409
2459
|
|
|
2410
2460
|
// 2. Sync with the imperative outliner arrays (mutate in place to keep references)
|
|
@@ -2417,7 +2467,11 @@ const EditorOutlinerSync = () => {
|
|
|
2417
2467
|
}
|
|
2418
2468
|
|
|
2419
2469
|
outliner.hoveredObjects.length = 0
|
|
2420
|
-
if (
|
|
2470
|
+
if (paintOutline) {
|
|
2471
|
+
// Paint on a floor plate: exactly the surfaces the click changes.
|
|
2472
|
+
for (const mesh of paintSurfaceMeshes(paintOutline, (id) => sceneRegistry.nodes.get(id)))
|
|
2473
|
+
if (mesh.parent) outliner.hoveredObjects.push(mesh)
|
|
2474
|
+
} else if (hoveredId) {
|
|
2421
2475
|
if (!nodes[hoveredId as AnyNodeId]) {
|
|
2422
2476
|
useViewer.setState({ hoveredId: null })
|
|
2423
2477
|
} else {
|
|
@@ -2440,6 +2494,7 @@ const EditorOutlinerSync = () => {
|
|
|
2440
2494
|
hoveredId,
|
|
2441
2495
|
outliner,
|
|
2442
2496
|
nodes,
|
|
2497
|
+
paintOutline,
|
|
2443
2498
|
])
|
|
2444
2499
|
|
|
2445
2500
|
return null
|