@pascal-app/editor 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -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 +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -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-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- 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 +294 -169
- 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/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 +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -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 +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- 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 +670 -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.tsx +220 -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 +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- 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-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- 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 +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- 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/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- 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 +93 -46
- 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/move-registry-node-tool.tsx +3 -1
- 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/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- 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/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- 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 +20 -14
- 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 +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- 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 +43 -4
- 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 +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- 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 +725 -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/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- 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 +176 -211
- 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 +0 -27
- 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 +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- 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-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -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 +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- 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 +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- 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/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- 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 +283 -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 +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -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/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -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 +232 -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 +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -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 +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -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 +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- 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 +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- 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 +186 -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 +407 -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/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 +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -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 +111 -0
- package/src/lib/paint-pass-through.ts +76 -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 +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- 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 +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- 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 +266 -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 +973 -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 +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- 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 +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -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/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -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 +569 -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-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-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-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-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -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 +175 -0
- package/src/store/view-layout.ts +196 -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/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/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,6 +4,7 @@ import {
|
|
|
4
4
|
type BuildingNode,
|
|
5
5
|
createSceneApi,
|
|
6
6
|
emitter,
|
|
7
|
+
floorPlatePaintRefusal,
|
|
7
8
|
type GridEvent,
|
|
8
9
|
getEffectiveRoofSurfaceMaterial,
|
|
9
10
|
getEffectiveSegmentSurfaceMaterial,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
type RoofSegmentEvent,
|
|
20
21
|
type RoofSegmentNode,
|
|
21
22
|
resolveLevelId,
|
|
23
|
+
type SlabNode,
|
|
22
24
|
type StairEvent,
|
|
23
25
|
type StairSegmentEvent,
|
|
24
26
|
type StairSurfaceMaterialRole,
|
|
@@ -27,17 +29,22 @@ import {
|
|
|
27
29
|
useRegistryVersion,
|
|
28
30
|
useScene,
|
|
29
31
|
} from '@pascal-app/core'
|
|
30
|
-
|
|
31
32
|
import {
|
|
32
33
|
createMaterial,
|
|
33
|
-
createMaterialFromPresetRef,
|
|
34
34
|
getRoofMaterialArray,
|
|
35
35
|
registerMaterialCacheCleanup,
|
|
36
|
+
resolveMaterialRef,
|
|
36
37
|
useViewer,
|
|
37
38
|
} from '@pascal-app/viewer'
|
|
38
39
|
import { useThree } from '@react-three/fiber'
|
|
39
40
|
import { useCallback, useEffect, useRef } from 'react'
|
|
40
41
|
import { type BufferGeometry, Color, type Material, type Mesh, type Object3D, Vector3 } from 'three'
|
|
42
|
+
import {
|
|
43
|
+
hoverRoomFromHit,
|
|
44
|
+
resolveEditorRoomHit,
|
|
45
|
+
roomPickingEnabled,
|
|
46
|
+
useRoomRecords,
|
|
47
|
+
} from '../../hooks/use-selected-room'
|
|
41
48
|
import {
|
|
42
49
|
canDirectMoveNode,
|
|
43
50
|
canDirectRotateNode,
|
|
@@ -48,6 +55,11 @@ import {
|
|
|
48
55
|
shouldStartDirectMoveDrag,
|
|
49
56
|
} from '../../lib/direct-manipulation'
|
|
50
57
|
import { createEditorApi } from '../../lib/editor-api'
|
|
58
|
+
import {
|
|
59
|
+
isFootprintConstructionHit,
|
|
60
|
+
isRoomOwnedPlate,
|
|
61
|
+
roomOwnedPlateDrillTarget,
|
|
62
|
+
} from '../../lib/floor-footprints'
|
|
51
63
|
import { selectionEnabled } from '../../lib/interaction/scope'
|
|
52
64
|
import {
|
|
53
65
|
type ActivePaintMaterial,
|
|
@@ -56,25 +68,55 @@ import {
|
|
|
56
68
|
hasActivePaintMaterial,
|
|
57
69
|
resolveActivePaintMaterialFromSelection,
|
|
58
70
|
} from '../../lib/material-paint'
|
|
71
|
+
import { paintCommitRoute } from '../../lib/paint-commit-route'
|
|
72
|
+
import { eyedropperMaterial, paintMaterialKey } from '../../lib/paint-eyedropper'
|
|
73
|
+
import {
|
|
74
|
+
openingPartHidden,
|
|
75
|
+
paintPassesThrough,
|
|
76
|
+
wallHitInOpening,
|
|
77
|
+
} from '../../lib/paint-pass-through'
|
|
59
78
|
import {
|
|
60
79
|
combinePaintPreviews,
|
|
61
80
|
createPaintPreviewOwner,
|
|
62
81
|
type PaintPreviewCleanup,
|
|
63
82
|
} from '../../lib/paint-preview-owner'
|
|
64
83
|
import {
|
|
65
|
-
|
|
84
|
+
bindPaintPickHold,
|
|
85
|
+
isPaintErasing,
|
|
86
|
+
isPaintPicking,
|
|
87
|
+
paintPickReturnMode,
|
|
88
|
+
paintRegionModeActive,
|
|
89
|
+
usePaintRegionMode,
|
|
90
|
+
} from '../../lib/paint-region-mode'
|
|
91
|
+
import {
|
|
66
92
|
commitPaintScopeFanout,
|
|
93
|
+
effectivePaintScope,
|
|
67
94
|
nodeSlotRoles,
|
|
68
95
|
type PaintHoverInfo,
|
|
96
|
+
paintHoverInfo,
|
|
97
|
+
paintScopeRole,
|
|
98
|
+
paintSurfaceLabel,
|
|
69
99
|
resolvePaintScopeTargets,
|
|
70
100
|
slotDisplayLabel,
|
|
71
101
|
type WallPaintHit,
|
|
72
102
|
} from '../../lib/paint-scope'
|
|
103
|
+
import {
|
|
104
|
+
ceilingAffectedSurfaces,
|
|
105
|
+
mergePaintSurfaces,
|
|
106
|
+
paintSurfaceMeshes,
|
|
107
|
+
plateAffectedSurfaces,
|
|
108
|
+
platePreviewSurfaces,
|
|
109
|
+
usePaintOutline,
|
|
110
|
+
} from '../../lib/plate-paint-affected'
|
|
73
111
|
import { getHoveredRoofSegmentOutlineProxy } from '../../lib/roof-hover-outline-proxy'
|
|
112
|
+
import { sameRoom } from '../../lib/room-selection'
|
|
113
|
+
import { selectRoom, shouldInterceptRoom } from '../../lib/room-selection-commands'
|
|
114
|
+
import { roomKeyForZone } from '../../lib/room-zone-routing'
|
|
74
115
|
import {
|
|
75
116
|
emitCanvasNodeSelection,
|
|
117
|
+
enterBuildingFromCanvas,
|
|
118
|
+
resolveCanvasBuildingId,
|
|
76
119
|
resolveCanvasSelectionNode,
|
|
77
|
-
resolveNodeSelectionTarget,
|
|
78
120
|
resolveSelectedIdsForNodeClick,
|
|
79
121
|
type SelectionModifierKeys,
|
|
80
122
|
selectionModifiersFromEvent,
|
|
@@ -88,7 +130,7 @@ import {
|
|
|
88
130
|
zoneAtWorldPoint,
|
|
89
131
|
} from '../../lib/units'
|
|
90
132
|
import useDirectManipulationFeedback from '../../store/use-direct-manipulation-feedback'
|
|
91
|
-
import useEditor, { type MaterialTargetRole } from './../../store/use-editor'
|
|
133
|
+
import useEditor, { type MaterialTargetRole, type Phase } from './../../store/use-editor'
|
|
92
134
|
import useInteractionScope, {
|
|
93
135
|
getEditingHole,
|
|
94
136
|
getMovingNode,
|
|
@@ -100,6 +142,7 @@ import { boxSelectHandled, suppressBoxSelectForPointer } from '../tools/select/b
|
|
|
100
142
|
import { armGroupMove3d } from './group-move-3d'
|
|
101
143
|
import { classifyParticipant } from './group-transform-shared'
|
|
102
144
|
import { swallowNextClick } from './node-arrow-handles'
|
|
145
|
+
import { RoomHighlight3D } from './room-highlight'
|
|
103
146
|
import { setEditorThreeContext } from './three-context-bridge'
|
|
104
147
|
|
|
105
148
|
const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
|
@@ -111,24 +154,6 @@ const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
|
|
111
154
|
return nodeLevelId === currentLevelId
|
|
112
155
|
}
|
|
113
156
|
|
|
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
157
|
type PaintInteraction = {
|
|
133
158
|
key: string
|
|
134
159
|
apply: (() => void) | null
|
|
@@ -141,7 +166,6 @@ type PaintInteraction = {
|
|
|
141
166
|
}
|
|
142
167
|
|
|
143
168
|
interface SelectionStrategy {
|
|
144
|
-
types: SelectableNodeType[]
|
|
145
169
|
handleSelect: (
|
|
146
170
|
node: AnyNode,
|
|
147
171
|
nativeEvent?: any,
|
|
@@ -183,6 +207,18 @@ export const resolveBuildingId = (
|
|
|
183
207
|
return null
|
|
184
208
|
}
|
|
185
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Furnishings: picking one leaves the room context (a sofa is not part of the
|
|
212
|
+
* room's shell). Door and window catalog items are openings, not furnishings.
|
|
213
|
+
*/
|
|
214
|
+
function isFurnishingNode(node: AnyNode): boolean {
|
|
215
|
+
if (node.type === 'item') {
|
|
216
|
+
const category = (node as ItemNode).asset.category
|
|
217
|
+
return category !== 'door' && category !== 'window'
|
|
218
|
+
}
|
|
219
|
+
return nodeRegistry.get(node.type)?.category === 'furnish'
|
|
220
|
+
}
|
|
221
|
+
|
|
186
222
|
function resolveStairMaterialTarget(
|
|
187
223
|
event: StairEvent | StairSegmentEvent,
|
|
188
224
|
): StairSurfaceMaterialRole | null {
|
|
@@ -233,6 +269,14 @@ function resolveRoofMaterialTarget(
|
|
|
233
269
|
return null
|
|
234
270
|
}
|
|
235
271
|
|
|
272
|
+
function isCeilingGridHit(event: NodeEvent): boolean {
|
|
273
|
+
return (
|
|
274
|
+
!event.viaHandle &&
|
|
275
|
+
event.node.type === 'ceiling' &&
|
|
276
|
+
getEventObject(event)?.name === 'ceiling-grid'
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
236
280
|
function getEventObject(event: NodeEvent): Object3D {
|
|
237
281
|
const eventWithObject = event as NodeEvent & { object?: Object3D }
|
|
238
282
|
return eventWithObject.object ?? event.nativeEvent.object
|
|
@@ -309,6 +353,31 @@ function meshSlotRoles(node: AnyNode): string[] {
|
|
|
309
353
|
const roofSelectionWorldPoint = new Vector3()
|
|
310
354
|
const wallPaintWorldPoint = new Vector3()
|
|
311
355
|
|
|
356
|
+
const roomHitPoint = new Vector3()
|
|
357
|
+
const roomHitXZ: [number, number] = [0, 0]
|
|
358
|
+
function roomForEvent(event: NodeEvent) {
|
|
359
|
+
if (event.node.type !== 'wall' && event.node.type !== 'slab' && event.node.type !== 'ceiling')
|
|
360
|
+
return null
|
|
361
|
+
// The floor edge band and the foundation belong to the footprint, not the
|
|
362
|
+
// room above: the plate takes the pick and opens Floor & foundation.
|
|
363
|
+
if (isFootprintConstructionHit(event.node, event.object)) return null
|
|
364
|
+
const levelId = resolveLevelId(event.node, useScene.getState().nodes)
|
|
365
|
+
if (!levelId) return null
|
|
366
|
+
if (event.viaHandle && event.node.type === 'ceiling') {
|
|
367
|
+
roomHitXZ[0] = event.position[0]
|
|
368
|
+
roomHitXZ[1] = event.position[2]
|
|
369
|
+
return resolveEditorRoomHit(event.node, levelId, roomHitXZ, '3d')
|
|
370
|
+
}
|
|
371
|
+
const level = sceneRegistry.nodes.get(levelId)
|
|
372
|
+
if (!level) return null
|
|
373
|
+
level.updateWorldMatrix(true, false)
|
|
374
|
+
roomHitPoint.set(...event.position)
|
|
375
|
+
level.worldToLocal(roomHitPoint)
|
|
376
|
+
roomHitXZ[0] = roomHitPoint.x
|
|
377
|
+
roomHitXZ[1] = roomHitPoint.z
|
|
378
|
+
return resolveEditorRoomHit(event.node, levelId, roomHitXZ, '3d')
|
|
379
|
+
}
|
|
380
|
+
|
|
312
381
|
function resolveWallPaintHit(event: NodeEvent): WallPaintHit | undefined {
|
|
313
382
|
const wall = event.node
|
|
314
383
|
if (wall.type !== 'wall') return undefined
|
|
@@ -368,7 +437,11 @@ function resolveRoofSegmentSelectionTarget(event: NodeEvent): RoofSegmentNode |
|
|
|
368
437
|
return bestSegment?.node ?? firstSegment
|
|
369
438
|
}
|
|
370
439
|
|
|
371
|
-
function resolveSelectModeNodeTarget(event: NodeEvent): AnyNode {
|
|
440
|
+
function resolveSelectModeNodeTarget(event: NodeEvent, drill = false): AnyNode {
|
|
441
|
+
if (!drill && event.node.type === 'stair-segment' && event.node.parentId) {
|
|
442
|
+
const parent = useScene.getState().nodes[event.node.parentId as AnyNodeId]
|
|
443
|
+
if (parent?.type === 'stair') return parent
|
|
444
|
+
}
|
|
372
445
|
if (event.node.type === 'roof') {
|
|
373
446
|
if (
|
|
374
447
|
shouldPreserveSelectedRoofHostTarget({
|
|
@@ -420,6 +493,8 @@ function applyRoofPaintPreview(
|
|
|
420
493
|
useViewer.getState().textures,
|
|
421
494
|
useViewer.getState().colorPreset,
|
|
422
495
|
useViewer.getState().sceneTheme,
|
|
496
|
+
null,
|
|
497
|
+
useScene.getState().materials,
|
|
423
498
|
)
|
|
424
499
|
if (!previewMaterial) return null
|
|
425
500
|
|
|
@@ -443,13 +518,12 @@ function applyRoofSegmentPaintPreview(
|
|
|
443
518
|
...node,
|
|
444
519
|
...buildRoofSegmentSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
|
445
520
|
}
|
|
521
|
+
const sceneMaterials = useScene.getState().materials
|
|
446
522
|
const resolveSlot = (r: 'top' | 'edge' | 'wall'): Material | null => {
|
|
447
523
|
const parentSpec = parent ? getEffectiveRoofSurfaceMaterial(parent, r) : undefined
|
|
448
524
|
const spec = getEffectiveSegmentSurfaceMaterial(previewNode, r, parentSpec)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (resolved) return resolved
|
|
452
|
-
}
|
|
525
|
+
const resolved = resolveMaterialRef(spec.materialPreset, sceneMaterials)
|
|
526
|
+
if (resolved) return resolved
|
|
453
527
|
if (spec.material !== undefined) return createMaterial(spec.material)
|
|
454
528
|
return null
|
|
455
529
|
}
|
|
@@ -457,7 +531,9 @@ function applyRoofSegmentPaintPreview(
|
|
|
457
531
|
const wall = resolveSlot('wall')
|
|
458
532
|
const top = resolveSlot('top')
|
|
459
533
|
if (!(edge || wall || top)) return null
|
|
460
|
-
const fallback = parent
|
|
534
|
+
const fallback = parent
|
|
535
|
+
? getRoofMaterialArray(parent, undefined, undefined, undefined, undefined, null, sceneMaterials)
|
|
536
|
+
: null
|
|
461
537
|
const fb = (n: number) => fallback?.[n] ?? null
|
|
462
538
|
// Per-role only, then the parent's themed slot — matches the renderer so the
|
|
463
539
|
// preview never bleeds a painted surface onto the segment's other surfaces.
|
|
@@ -504,7 +580,7 @@ const HIGHLIGHT_PROFILES = {
|
|
|
504
580
|
} as const
|
|
505
581
|
|
|
506
582
|
type HighlightKind = keyof typeof HIGHLIGHT_PROFILES
|
|
507
|
-
type HoverHighlightMode = 'default' | 'delete' | 'paint-ready' | 'paint-disabled'
|
|
583
|
+
type HoverHighlightMode = 'default' | 'delete' | 'paint-ready' | 'erase-ready' | 'paint-disabled'
|
|
508
584
|
|
|
509
585
|
type HighlightableMaterial = Material & {
|
|
510
586
|
color?: Color
|
|
@@ -611,9 +687,8 @@ const computeNextIds = (
|
|
|
611
687
|
})
|
|
612
688
|
}
|
|
613
689
|
|
|
614
|
-
const SELECTION_STRATEGIES: Record<
|
|
690
|
+
const SELECTION_STRATEGIES: Record<Phase, SelectionStrategy> = {
|
|
615
691
|
site: {
|
|
616
|
-
types: ['building'],
|
|
617
692
|
handleSelect: (node) => {
|
|
618
693
|
useViewer.getState().setSelection({ buildingId: (node as BuildingNode).id })
|
|
619
694
|
},
|
|
@@ -623,24 +698,7 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
623
698
|
isValid: (node) => node.type === 'building',
|
|
624
699
|
},
|
|
625
700
|
|
|
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
|
-
],
|
|
701
|
+
building: {
|
|
644
702
|
handleSelect: (node, nativeEvent, modifierKeys, baseSelectedIds) => {
|
|
645
703
|
const { selection, setSelection } = useViewer.getState()
|
|
646
704
|
const nodes = useScene.getState().nodes
|
|
@@ -662,10 +720,15 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
662
720
|
updates.buildingId = buildingId
|
|
663
721
|
}
|
|
664
722
|
|
|
723
|
+
if (node.type === 'zone' && !useViewer.getState().focusedUnitId) {
|
|
724
|
+
const room = roomKeyForZone(node.id)
|
|
725
|
+
if (room) {
|
|
726
|
+
if (!sameRoom(useEditor.getState().room, room)) selectRoom(room)
|
|
727
|
+
return
|
|
728
|
+
}
|
|
729
|
+
}
|
|
665
730
|
if (node.type === 'zone') {
|
|
666
731
|
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
732
|
setSelection(updates)
|
|
670
733
|
} else {
|
|
671
734
|
updates.selectedIds = computeNextIds(
|
|
@@ -679,21 +742,12 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
679
742
|
}
|
|
680
743
|
},
|
|
681
744
|
handleDeselect: () => {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
useViewer.getState().setSelection({ zoneId: null })
|
|
685
|
-
} else {
|
|
686
|
-
useViewer.getState().setSelection({ selectedIds: [] })
|
|
687
|
-
}
|
|
745
|
+
useEditor.getState().clearRoom()
|
|
746
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
688
747
|
},
|
|
689
748
|
isValid: (node) => {
|
|
690
749
|
if (!isNodeInCurrentLevel(node)) return false
|
|
691
|
-
|
|
692
|
-
if (structureLayer === 'zones') {
|
|
693
|
-
if (node.type === 'zone') return true
|
|
694
|
-
return false
|
|
695
|
-
}
|
|
696
|
-
if (
|
|
750
|
+
return (
|
|
697
751
|
node.type === 'wall' ||
|
|
698
752
|
node.type === 'fence' ||
|
|
699
753
|
node.type === 'column' ||
|
|
@@ -704,75 +758,36 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|
|
704
758
|
node.type === 'roof-segment' ||
|
|
705
759
|
node.type === 'stair' ||
|
|
706
760
|
node.type === 'stair-segment' ||
|
|
707
|
-
node.type === 'spawn'
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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,
|
|
761
|
+
node.type === 'spawn' ||
|
|
762
|
+
node.type === 'window' ||
|
|
763
|
+
node.type === 'door' ||
|
|
764
|
+
node.type === 'item' ||
|
|
765
|
+
isRegistrySelectable(node.type)
|
|
749
766
|
)
|
|
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
767
|
},
|
|
771
768
|
},
|
|
772
769
|
}
|
|
773
770
|
|
|
774
771
|
export const SelectionManager = () => {
|
|
775
|
-
const
|
|
772
|
+
const roomLevelId = useViewer((s) => s.selection.levelId)
|
|
773
|
+
useRoomRecords(roomLevelId)
|
|
774
|
+
useEffect(
|
|
775
|
+
() =>
|
|
776
|
+
useViewer.subscribe((state, previous) => {
|
|
777
|
+
if (
|
|
778
|
+
state.selection.levelId !== previous.selection.levelId ||
|
|
779
|
+
(state.selection.zoneId !== previous.selection.zoneId && state.selection.zoneId) ||
|
|
780
|
+
(state.selection.selectedIds !== previous.selection.selectedIds &&
|
|
781
|
+
state.selection.selectedIds.some((id) => {
|
|
782
|
+
const node = useScene.getState().nodes[id as AnyNodeId]
|
|
783
|
+
return node && isFurnishingNode(node)
|
|
784
|
+
}))
|
|
785
|
+
) {
|
|
786
|
+
useEditor.getState().clearRoom()
|
|
787
|
+
}
|
|
788
|
+
}),
|
|
789
|
+
[],
|
|
790
|
+
)
|
|
776
791
|
const mode = useEditor((s) => s.mode)
|
|
777
792
|
// The canvas element — cursor styling must land here, not on `document.body`:
|
|
778
793
|
// the editor wraps the canvas in a div with a custom `cursor: url(...)`, which
|
|
@@ -825,6 +840,18 @@ export const SelectionManager = () => {
|
|
|
825
840
|
// The last hover event, replayed when the application scope cycles so the
|
|
826
841
|
// preview + chip update under a stationary cursor (Shift fires no pointer move).
|
|
827
842
|
let lastEnterEvent: NodeEvent | null = null
|
|
843
|
+
// Paint hover never stops a pointer event. R3F keeps a hover that stopped
|
|
844
|
+
// propagation stopping every later move over that object, which would pin
|
|
845
|
+
// the pointer to a door or a wall it should pass through where it opens
|
|
846
|
+
// (see `paint-pass-through`). So the nearest surface that takes a pointer
|
|
847
|
+
// event claims it; farther ones see the claim and stand down.
|
|
848
|
+
let claimedEvent: unknown = null
|
|
849
|
+
let hoveredNodeId: string | null = null
|
|
850
|
+
const claim = (event: NodeEvent) => {
|
|
851
|
+
claimedEvent = event.nativeEvent.nativeEvent
|
|
852
|
+
}
|
|
853
|
+
const claimedByNearer = (event: NodeEvent) =>
|
|
854
|
+
claimedEvent !== null && claimedEvent === event.nativeEvent.nativeEvent
|
|
828
855
|
|
|
829
856
|
const clearActivePreview = () => {
|
|
830
857
|
activePreview?.restore()
|
|
@@ -835,6 +862,7 @@ export const SelectionManager = () => {
|
|
|
835
862
|
useEditor.getState().activePaintMaterial ??
|
|
836
863
|
resolveActivePaintMaterialFromSelection({
|
|
837
864
|
nodes: useScene.getState().nodes,
|
|
865
|
+
materials: useScene.getState().materials,
|
|
838
866
|
selectedId:
|
|
839
867
|
useViewer.getState().selection.selectedIds.length === 1
|
|
840
868
|
? (useViewer.getState().selection.selectedIds[0] ?? null)
|
|
@@ -842,8 +870,50 @@ export const SelectionManager = () => {
|
|
|
842
870
|
selectedMaterialTarget: useEditor.getState().selectedMaterialTarget,
|
|
843
871
|
})
|
|
844
872
|
|
|
873
|
+
// The eyedropper: the hover shows what a click would take (the cursor
|
|
874
|
+
// swatch), the click takes it and returns to the sub-mode it came from.
|
|
875
|
+
const pickInteraction = (
|
|
876
|
+
node: AnyNode,
|
|
877
|
+
role: string | null,
|
|
878
|
+
event: NodeEvent,
|
|
879
|
+
materialIndex: number | null,
|
|
880
|
+
): PaintInteraction => {
|
|
881
|
+
const picked = role
|
|
882
|
+
? eyedropperMaterial({
|
|
883
|
+
node,
|
|
884
|
+
role,
|
|
885
|
+
nodes: useScene.getState().nodes,
|
|
886
|
+
materials: useScene.getState().materials,
|
|
887
|
+
hitObject: getEventObject(event),
|
|
888
|
+
materialIndex,
|
|
889
|
+
})
|
|
890
|
+
: null
|
|
891
|
+
return {
|
|
892
|
+
key: `pick:${node.id}:${role ?? 'none'}:${paintMaterialKey(picked)}`,
|
|
893
|
+
hoveredId: node.id as AnyNodeId,
|
|
894
|
+
hoverMode: picked ? 'paint-ready' : 'paint-disabled',
|
|
895
|
+
paintHover:
|
|
896
|
+
picked && role
|
|
897
|
+
? { scopes: ['single'], slotLabel: paintSurfaceLabel(node, role), nodeNoun: node.type }
|
|
898
|
+
: null,
|
|
899
|
+
apply: picked
|
|
900
|
+
? () => useEditor.getState().armMaterialPaint(picked, paintPickReturnMode())
|
|
901
|
+
: null,
|
|
902
|
+
preview: () => {
|
|
903
|
+
usePaintRegionMode.setState({ picked })
|
|
904
|
+
return () => {
|
|
905
|
+
if (usePaintRegionMode.getState().picked === picked)
|
|
906
|
+
usePaintRegionMode.setState({ picked: null })
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
845
912
|
const resolvePaintInteraction = (event: NodeEvent): PaintInteraction | null => {
|
|
846
|
-
|
|
913
|
+
// A region sub-mode draws its own region before painting; the
|
|
914
|
+
// whole-surface hover and click stand down.
|
|
915
|
+
if (paintRegionModeActive('material-paint')) return null
|
|
916
|
+
const eraser = isPaintErasing()
|
|
847
917
|
const activePaintMaterial = resolveActivePaintMaterial()
|
|
848
918
|
const node = event.node
|
|
849
919
|
|
|
@@ -885,6 +955,25 @@ export const SelectionManager = () => {
|
|
|
885
955
|
hitObject: getEventObject(event),
|
|
886
956
|
ray: event.nativeEvent.ray,
|
|
887
957
|
})
|
|
958
|
+
// Not claiming the hover lets the pointer event reach the surface behind:
|
|
959
|
+
// an empty door or window, or a wall's collision hit in one of its holes.
|
|
960
|
+
if (
|
|
961
|
+
paintPassesThrough(node, role) ||
|
|
962
|
+
((node.type === 'door' || node.type === 'window') &&
|
|
963
|
+
openingPartHidden(
|
|
964
|
+
getRegisteredNodeObject(node.id),
|
|
965
|
+
event.nativeEvent.ray,
|
|
966
|
+
event.nativeEvent.intersections,
|
|
967
|
+
)) ||
|
|
968
|
+
(node.type === 'wall' &&
|
|
969
|
+
wallHitInOpening(
|
|
970
|
+
getRegisteredNodeObject(node.id),
|
|
971
|
+
event.nativeEvent.ray,
|
|
972
|
+
event.nativeEvent.distance,
|
|
973
|
+
))
|
|
974
|
+
)
|
|
975
|
+
return null
|
|
976
|
+
if (isPaintPicking()) return pickInteraction(node, role, event, materialIndex ?? null)
|
|
888
977
|
const compatible = role !== null && paintEnabled
|
|
889
978
|
// Derive the node's slots (declared, else mesh tags) once — drives both
|
|
890
979
|
// the chip's available scopes and the whole-object fan-out.
|
|
@@ -893,7 +982,13 @@ export const SelectionManager = () => {
|
|
|
893
982
|
// / all matching / room). The scope is part of the key so cycling it
|
|
894
983
|
// (Shift) re-keys the interaction → the preview re-applies for the new
|
|
895
984
|
// spread instead of being deduped to the single-surface preview.
|
|
896
|
-
const
|
|
985
|
+
const hover = compatible && role ? paintHoverInfo(node, role, slotRoles) : null
|
|
986
|
+
// A scope carried over from another surface paints what the chip
|
|
987
|
+
// shows for this one: the narrowest when it isn't offered here.
|
|
988
|
+
const scope = effectivePaintScope(
|
|
989
|
+
useEditor.getState().paintScope,
|
|
990
|
+
hover?.scopes ?? ['single'],
|
|
991
|
+
)
|
|
897
992
|
const wallHit = resolveWallPaintHit(event)
|
|
898
993
|
const scopeTargets =
|
|
899
994
|
compatible && role
|
|
@@ -907,30 +1002,44 @@ export const SelectionManager = () => {
|
|
|
907
1002
|
wallHit,
|
|
908
1003
|
})
|
|
909
1004
|
: []
|
|
910
|
-
|
|
1005
|
+
// What the click changes on floor plates, fallbacks included (a room's
|
|
1006
|
+
// floor carries its unpainted steps): the outline. The preview draws
|
|
1007
|
+
// the same set, each follower through the surface it follows.
|
|
1008
|
+
const scopeRole = compatible && role ? paintScopeRole(node, role, scope) : null
|
|
1009
|
+
const sceneNodes = useScene.getState().nodes
|
|
1010
|
+
const affected = scopeRole
|
|
1011
|
+
? (plateAffectedSurfaces(sceneNodes, node, scopeRole, { erasing: eraser }) ??
|
|
1012
|
+
ceilingAffectedSurfaces(node, scopeRole))
|
|
1013
|
+
: null
|
|
1014
|
+
const previewTargets = mergePaintSurfaces(
|
|
1015
|
+
scopeTargets,
|
|
1016
|
+
scopeRole
|
|
1017
|
+
? (platePreviewSurfaces(sceneNodes, node, scopeRole) ??
|
|
1018
|
+
ceilingAffectedSurfaces(node, scopeRole))
|
|
1019
|
+
: null,
|
|
1020
|
+
)
|
|
1021
|
+
const scopeTargetKey = previewTargets
|
|
911
1022
|
.map((target) => `${target.nodeId}:${target.role}`)
|
|
912
1023
|
.sort()
|
|
913
1024
|
.join(',')
|
|
914
1025
|
return {
|
|
915
1026
|
key: `${node.type}:${node.id}:${role ?? 'unsupported'}:${eraser ? 'erase' : 'paint'}:${scope}:${scopeTargetKey}`,
|
|
916
1027
|
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,
|
|
1028
|
+
hoverMode: compatible ? (eraser ? 'erase-ready' : 'paint-ready') : 'paint-disabled',
|
|
1029
|
+
paintHover: hover,
|
|
926
1030
|
apply:
|
|
927
1031
|
compatible && role
|
|
928
1032
|
? () => {
|
|
1033
|
+
// A surface with nothing of its own to paint (a footprint's
|
|
1034
|
+
// top where no room stands) says so instead of painting.
|
|
1035
|
+
const refusal = floorPlatePaintRefusal(useScene.getState().nodes, node, role)
|
|
1036
|
+
usePaintRegionMode.getState().setNotice(refusal?.message ?? null)
|
|
1037
|
+
if (refusal) return
|
|
929
1038
|
// 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.
|
|
1039
|
+
// batched commit writes them in a single undo step; only
|
|
1040
|
+
// the hovered surface itself keeps the kind's own commit
|
|
1041
|
+
// (covers non-slot kinds too) — see `paintCommitRoute`.
|
|
1042
|
+
if (paintCommitRoute(scopeTargets, node.id, role) === 'fanout') {
|
|
934
1043
|
commitPaintScopeFanout(
|
|
935
1044
|
scopeTargets,
|
|
936
1045
|
paintSpec.material,
|
|
@@ -964,10 +1073,14 @@ export const SelectionManager = () => {
|
|
|
964
1073
|
// hovered surface. Each target is the same kind, so its own
|
|
965
1074
|
// paint capability builds the preview; restores combine.
|
|
966
1075
|
const restores: PaintPreviewCleanup[] = []
|
|
967
|
-
const
|
|
1076
|
+
const liveNodes = useScene.getState().nodes
|
|
1077
|
+
if (affected) {
|
|
1078
|
+
usePaintOutline.setState({ surfaces: affected })
|
|
1079
|
+
restores.push(() => usePaintOutline.setState({ surfaces: null }))
|
|
1080
|
+
}
|
|
968
1081
|
try {
|
|
969
|
-
for (const target of
|
|
970
|
-
const targetNode =
|
|
1082
|
+
for (const target of previewTargets) {
|
|
1083
|
+
const targetNode = liveNodes[target.nodeId]
|
|
971
1084
|
const targetRoot = getRegisteredNodeObject(target.nodeId)
|
|
972
1085
|
const targetCap = targetNode
|
|
973
1086
|
? nodeRegistry.get(targetNode.type)?.capabilities?.paint
|
|
@@ -975,6 +1088,8 @@ export const SelectionManager = () => {
|
|
|
975
1088
|
if (!(targetNode && targetRoot && targetCap)) continue
|
|
976
1089
|
const restore = targetCap.applyPreview({
|
|
977
1090
|
node: targetNode,
|
|
1091
|
+
nodes: liveNodes,
|
|
1092
|
+
materials: useScene.getState().materials,
|
|
978
1093
|
role: target.role,
|
|
979
1094
|
material: paintSpec.material,
|
|
980
1095
|
materialPreset: paintSpec.materialPreset,
|
|
@@ -1004,6 +1119,8 @@ export const SelectionManager = () => {
|
|
|
1004
1119
|
if (roofNode?.type !== 'roof') return null
|
|
1005
1120
|
|
|
1006
1121
|
const role = resolveRoofMaterialTarget(event as RoofEvent | RoofSegmentEvent)
|
|
1122
|
+
if (isPaintPicking())
|
|
1123
|
+
return pickInteraction(isSegmentHit ? node : roofNode, role, event, null)
|
|
1007
1124
|
const compatible = role !== null && paintEnabled
|
|
1008
1125
|
// Painting directly on a segment (only possible in segment edit
|
|
1009
1126
|
// mode, where the per-segment mesh is visible) writes to the
|
|
@@ -1015,7 +1132,7 @@ export const SelectionManager = () => {
|
|
|
1015
1132
|
segmentTarget ? segmentTarget.id : roofNode.id
|
|
1016
1133
|
}:${role ?? 'unsupported'}:${eraser ? 'erase' : 'paint'}`,
|
|
1017
1134
|
hoveredId: (segmentTarget ? segmentTarget.id : roofNode.id) as AnyNodeId,
|
|
1018
|
-
hoverMode: compatible ? 'paint-ready' : 'paint-disabled',
|
|
1135
|
+
hoverMode: compatible ? (eraser ? 'erase-ready' : 'paint-ready') : 'paint-disabled',
|
|
1019
1136
|
// Roof isn't on the slot model (role-specific fields, custom commit),
|
|
1020
1137
|
// so it offers only the single surface — but still labels it.
|
|
1021
1138
|
paintHover:
|
|
@@ -1091,17 +1208,20 @@ export const SelectionManager = () => {
|
|
|
1091
1208
|
previewOwner.wrap(resolvePaintInteraction(event))
|
|
1092
1209
|
|
|
1093
1210
|
const onEnter = (event: NodeEvent) => {
|
|
1211
|
+
if (isCeilingGridHit(event)) return
|
|
1094
1212
|
// A host-driven drag (handle resize/rotate) sets `inputDragging`.
|
|
1095
1213
|
// useNodeEvents now emits hover events during such a drag so surface
|
|
1096
1214
|
// move tools keep tracking the cursor — but paint preview must not fire
|
|
1097
1215
|
// mid-drag, so gate on `inputDragging` here too.
|
|
1098
1216
|
if (boxSelectHandled || useViewer.getState().inputDragging) return
|
|
1217
|
+
if (claimedByNearer(event)) return
|
|
1099
1218
|
|
|
1100
1219
|
const interaction = getPaintInteraction(event)
|
|
1101
1220
|
if (!interaction) return
|
|
1102
1221
|
|
|
1103
|
-
event
|
|
1222
|
+
claim(event)
|
|
1104
1223
|
lastEnterEvent = event
|
|
1224
|
+
hoveredNodeId = interaction.hoveredId
|
|
1105
1225
|
|
|
1106
1226
|
// Drive the paint HUD off this hover: the interaction carries the scopes +
|
|
1107
1227
|
// labels for the painted surface (`null` when it isn't paintable — no
|
|
@@ -1123,12 +1243,17 @@ export const SelectionManager = () => {
|
|
|
1123
1243
|
}
|
|
1124
1244
|
|
|
1125
1245
|
const onLeave = (event: NodeEvent) => {
|
|
1246
|
+
if (isCeilingGridHit(event)) return
|
|
1126
1247
|
const interaction = getPaintInteraction(event)
|
|
1127
|
-
|
|
1248
|
+
// A surface behind the hovered one (under an opening that never stops
|
|
1249
|
+
// the pointer) leaving says nothing about the hover.
|
|
1250
|
+
if (!interaction || interaction.hoveredId !== hoveredNodeId) return
|
|
1128
1251
|
|
|
1129
1252
|
// Leaving any surface → the HUD shows the "hover a surface" hint again.
|
|
1253
|
+
hoveredNodeId = null
|
|
1130
1254
|
lastEnterEvent = null
|
|
1131
1255
|
useEditor.getState().setPaintHover(null)
|
|
1256
|
+
usePaintOutline.setState({ surfaces: null })
|
|
1132
1257
|
|
|
1133
1258
|
if (activePreview?.key !== interaction.key) {
|
|
1134
1259
|
return
|
|
@@ -1142,17 +1267,22 @@ export const SelectionManager = () => {
|
|
|
1142
1267
|
}
|
|
1143
1268
|
|
|
1144
1269
|
const onClick = (event: NodeEvent) => {
|
|
1270
|
+
if (isCeilingGridHit(event)) return
|
|
1145
1271
|
if (boxSelectHandled) return
|
|
1272
|
+
if (claimedByNearer(event)) return
|
|
1146
1273
|
|
|
1147
1274
|
const interaction = getPaintInteraction(event)
|
|
1148
1275
|
if (!interaction) return
|
|
1149
1276
|
|
|
1150
|
-
event
|
|
1277
|
+
claim(event)
|
|
1151
1278
|
|
|
1152
1279
|
if (!interaction.apply) {
|
|
1153
1280
|
return
|
|
1154
1281
|
}
|
|
1155
1282
|
|
|
1283
|
+
// Picking switches sub-mode synchronously and replays the hover, replacing
|
|
1284
|
+
// the event claim. Consume this click before a farther hit can paint.
|
|
1285
|
+
if (isPaintPicking()) event.stopPropagation()
|
|
1156
1286
|
interaction.apply()
|
|
1157
1287
|
sfxEmitter.emit('sfx:paint-apply')
|
|
1158
1288
|
if (activePreview?.key === interaction.key) {
|
|
@@ -1202,11 +1332,22 @@ export const SelectionManager = () => {
|
|
|
1202
1332
|
const unsubscribePaintScope = useEditor.subscribe((state, prev) => {
|
|
1203
1333
|
if (state.paintScope === prev.paintScope || !lastEnterEvent) return
|
|
1204
1334
|
clearActivePreview()
|
|
1335
|
+
claimedEvent = null
|
|
1336
|
+
onEnter(lastEnterEvent)
|
|
1337
|
+
})
|
|
1338
|
+
const releasePickHold = bindPaintPickHold(window)
|
|
1339
|
+
// So does switching the sub-mode (the eyedropper held on Alt, a mode icon).
|
|
1340
|
+
const unsubscribePaintMode = usePaintRegionMode.subscribe((state, prev) => {
|
|
1341
|
+
if (state.mode === prev.mode || !lastEnterEvent) return
|
|
1342
|
+
clearActivePreview()
|
|
1343
|
+
claimedEvent = null
|
|
1205
1344
|
onEnter(lastEnterEvent)
|
|
1206
1345
|
})
|
|
1207
1346
|
|
|
1208
1347
|
return () => {
|
|
1209
1348
|
unsubscribePaintScope()
|
|
1349
|
+
unsubscribePaintMode()
|
|
1350
|
+
releasePickHold()
|
|
1210
1351
|
for (const type of subscribedKinds) {
|
|
1211
1352
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
|
1212
1353
|
emitter.off(`${type}:move` as any, onEnter as any)
|
|
@@ -1215,6 +1356,7 @@ export const SelectionManager = () => {
|
|
|
1215
1356
|
}
|
|
1216
1357
|
clearActivePreview()
|
|
1217
1358
|
useViewer.setState({ hoveredId: null })
|
|
1359
|
+
usePaintOutline.setState({ surfaces: null })
|
|
1218
1360
|
setHoverHighlightMode('default')
|
|
1219
1361
|
useEditor.getState().setPaintHover(null)
|
|
1220
1362
|
}
|
|
@@ -1260,9 +1402,20 @@ export const SelectionManager = () => {
|
|
|
1260
1402
|
if (movingNode || isCurveReshape) return
|
|
1261
1403
|
|
|
1262
1404
|
const onPointerDown = (event: NodeEvent) => {
|
|
1405
|
+
if (isCeilingGridHit(event)) return
|
|
1263
1406
|
if (!selectionEnabled(useInteractionScope.getState().scope)) return
|
|
1407
|
+
if (useEditor.getState().phase === 'site') return
|
|
1264
1408
|
const pointer = pointerEventFromNodeEvent(event)
|
|
1265
|
-
if (pointer.button !== 0) return
|
|
1409
|
+
if (pointer.button !== 0 || pointer.altKey) return
|
|
1410
|
+
if (
|
|
1411
|
+
roomPickingEnabled() &&
|
|
1412
|
+
shouldInterceptRoom(
|
|
1413
|
+
roomForEvent(event),
|
|
1414
|
+
selectionModifiersFromEvent(pointer),
|
|
1415
|
+
event.node.id,
|
|
1416
|
+
)
|
|
1417
|
+
)
|
|
1418
|
+
return
|
|
1266
1419
|
const handleOwnsPointer = pointerEventHitsEditorHandle(event.nativeEvent)
|
|
1267
1420
|
|
|
1268
1421
|
// Plain press on a transformable member of a multi-selection arms the
|
|
@@ -1577,6 +1730,7 @@ export const SelectionManager = () => {
|
|
|
1577
1730
|
if (movingNode || isCurveReshape) return
|
|
1578
1731
|
|
|
1579
1732
|
const onClick = (event: NodeEvent) => {
|
|
1733
|
+
if (isCeilingGridHit(event)) return
|
|
1580
1734
|
// Skip if box-select just completed (drag ended over a node)
|
|
1581
1735
|
if (boxSelectHandled) return
|
|
1582
1736
|
|
|
@@ -1588,8 +1742,11 @@ export const SelectionManager = () => {
|
|
|
1588
1742
|
// body click so only the reshape tool handles the release. (Scoped to
|
|
1589
1743
|
// `endpoint`: hole-edit relies on node clicks to exit, just below.)
|
|
1590
1744
|
const activeScope = useInteractionScope.getState().scope
|
|
1591
|
-
if (
|
|
1592
|
-
|
|
1745
|
+
if (
|
|
1746
|
+
!selectionEnabled(activeScope) &&
|
|
1747
|
+
!(activeScope.kind === 'reshaping' && activeScope.reshape === 'hole')
|
|
1748
|
+
)
|
|
1749
|
+
return
|
|
1593
1750
|
|
|
1594
1751
|
if (dispatchSceneAction(event.node, getEventObject(event))) {
|
|
1595
1752
|
event.stopPropagation()
|
|
@@ -1606,21 +1763,14 @@ export const SelectionManager = () => {
|
|
|
1606
1763
|
selectedIds: useViewer.getState().selection.selectedIds,
|
|
1607
1764
|
})
|
|
1608
1765
|
|
|
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
1766
|
// Unit focus turns clicks inside a zone into the paint gesture:
|
|
1619
1767
|
// membership toggles, the zone stays unselected, focus stays.
|
|
1620
1768
|
const focusedUnitId = useViewer.getState().focusedUnitId
|
|
1621
1769
|
if (focusedUnitId) {
|
|
1622
1770
|
const zone =
|
|
1623
|
-
node.type === 'zone'
|
|
1771
|
+
node.type === 'zone'
|
|
1772
|
+
? node
|
|
1773
|
+
: zoneAtWorldPoint(event.position[0], event.position[2], event.position[1])
|
|
1624
1774
|
if (zone) {
|
|
1625
1775
|
event.stopPropagation()
|
|
1626
1776
|
clickHandledRef.current = true
|
|
@@ -1632,35 +1782,42 @@ export const SelectionManager = () => {
|
|
|
1632
1782
|
}
|
|
1633
1783
|
}
|
|
1634
1784
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
useEditor.getState().setPhase(target.phase)
|
|
1647
|
-
currentPhase = target.phase
|
|
1648
|
-
}
|
|
1785
|
+
// From site the house is one thing: a hit anywhere on it picks its
|
|
1786
|
+
// building and goes inside, where the next click picks a room or element.
|
|
1787
|
+
if (useEditor.getState().phase === 'site') {
|
|
1788
|
+
if (!enterBuildingFromCanvas(node)) return
|
|
1789
|
+
event.stopPropagation()
|
|
1790
|
+
clickHandledRef.current = true
|
|
1791
|
+
setTimeout(() => {
|
|
1792
|
+
clickHandledRef.current = false
|
|
1793
|
+
}, 50)
|
|
1794
|
+
return
|
|
1795
|
+
}
|
|
1649
1796
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1797
|
+
if (isNodeInCurrentLevel(node) && roomPickingEnabled()) {
|
|
1798
|
+
const room = roomForEvent(event)
|
|
1799
|
+
if (
|
|
1800
|
+
room &&
|
|
1801
|
+
shouldInterceptRoom(
|
|
1802
|
+
room,
|
|
1803
|
+
selectionModifiersFromEvent(event.nativeEvent, modifierKeysRef.current),
|
|
1804
|
+
node.id,
|
|
1805
|
+
)
|
|
1806
|
+
) {
|
|
1807
|
+
event.stopPropagation()
|
|
1808
|
+
clickHandledRef.current = true
|
|
1809
|
+
setTimeout(() => {
|
|
1810
|
+
clickHandledRef.current = false
|
|
1811
|
+
}, 50)
|
|
1812
|
+
selectRoom(room)
|
|
1813
|
+
return
|
|
1659
1814
|
}
|
|
1815
|
+
useEditor.getState().setHoveredRoom(null)
|
|
1660
1816
|
}
|
|
1661
1817
|
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1818
|
+
const selectedIdsBeforeRouting = useViewer.getState().selection.selectedIds
|
|
1819
|
+
const activeStrategy = SELECTION_STRATEGIES.building
|
|
1820
|
+
if (activeStrategy.isValid(node)) {
|
|
1664
1821
|
event.stopPropagation()
|
|
1665
1822
|
clickHandledRef.current = true
|
|
1666
1823
|
// Reset the handled flag after a short delay so the grid:click that the
|
|
@@ -1673,13 +1830,22 @@ export const SelectionManager = () => {
|
|
|
1673
1830
|
clickHandledRef.current = false
|
|
1674
1831
|
}, 50)
|
|
1675
1832
|
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1833
|
+
// Room first: a raised or sunken room's plate and a mezzanine's plate
|
|
1834
|
+
// belong to their room, never selected on their own. Past the room (the
|
|
1835
|
+
// drill click, or Alt) a raised or sunken room continues to the
|
|
1836
|
+
// footprint floor it stands on, like a ground-level room; a mezzanine
|
|
1837
|
+
// stays on its mezzanine.
|
|
1838
|
+
let footprint: SlabNode | null = null
|
|
1839
|
+
if (roomPickingEnabled() && isRoomOwnedPlate(node)) {
|
|
1840
|
+
footprint = roomOwnedPlateDrillTarget(useScene.getState().nodes, node as SlabNode)
|
|
1841
|
+
if (!footprint) {
|
|
1842
|
+
const hit = roomForEvent(event)
|
|
1843
|
+
if (hit) selectRoom(hit)
|
|
1844
|
+
return
|
|
1681
1845
|
}
|
|
1682
1846
|
}
|
|
1847
|
+
|
|
1848
|
+
let nodeToSelect: AnyNode = footprint ?? node
|
|
1683
1849
|
nodeToSelect = resolveCanvasSelectionNode({
|
|
1684
1850
|
node: nodeToSelect,
|
|
1685
1851
|
nodes: useScene.getState().nodes,
|
|
@@ -1701,13 +1867,15 @@ export const SelectionManager = () => {
|
|
|
1701
1867
|
// Move button. The first (selecting) click can't hit this because the
|
|
1702
1868
|
// node isn't yet in `selectedIdsBeforeRouting`.
|
|
1703
1869
|
const nativeEvent = event.nativeEvent
|
|
1704
|
-
const hasModifier =
|
|
1870
|
+
const hasModifier =
|
|
1871
|
+
nativeEvent.altKey || nativeEvent.shiftKey || isCommandModifier(nativeEvent)
|
|
1705
1872
|
const isAlreadySole =
|
|
1706
1873
|
selectedIdsBeforeRouting.length === 1 && selectedIdsBeforeRouting[0] === nodeToSelect.id
|
|
1707
1874
|
if (
|
|
1708
1875
|
useEditor.getState().mode !== 'delete' &&
|
|
1709
1876
|
!hasModifier &&
|
|
1710
1877
|
isAlreadySole &&
|
|
1878
|
+
!expandSessionSelectionForNode(nodeToSelect.id) &&
|
|
1711
1879
|
!getMovingNode() &&
|
|
1712
1880
|
canDirectMoveNode(nodeToSelect)
|
|
1713
1881
|
) {
|
|
@@ -1732,7 +1900,7 @@ export const SelectionManager = () => {
|
|
|
1732
1900
|
// `capabilities.paint` route through this entry — wall,
|
|
1733
1901
|
// chimney, dormer use it today. The legacy stair / roof /
|
|
1734
1902
|
// single-surface arms below stay until they migrate too.
|
|
1735
|
-
if (nodeToSelect.type === node.type) {
|
|
1903
|
+
if (!footprint && nodeToSelect.type === node.type) {
|
|
1736
1904
|
const paintCap = nodeRegistry.get(node.type)?.capabilities?.paint
|
|
1737
1905
|
if (paintCap) {
|
|
1738
1906
|
const materialIndex = getIntersectionMaterialIndex(
|
|
@@ -1829,27 +1997,23 @@ export const SelectionManager = () => {
|
|
|
1829
1997
|
const onGridClick = (event: GridEvent) => {
|
|
1830
1998
|
if (clickHandledRef.current) return
|
|
1831
1999
|
if (boxSelectHandled) return
|
|
1832
|
-
if (useInteractionScope.getState().scope
|
|
2000
|
+
if (!selectionEnabled(useInteractionScope.getState().scope)) return
|
|
1833
2001
|
const nativeEvent = event.nativeEvent
|
|
1834
2002
|
if (nativeEvent?.metaKey || nativeEvent?.ctrlKey || nativeEvent?.shiftKey) return
|
|
1835
2003
|
// Unit focus: a ground click inside a zone paints it; elsewhere it
|
|
1836
2004
|
// leaves focus and the unit selection alone.
|
|
1837
2005
|
const focusedUnitId = useViewer.getState().focusedUnitId
|
|
1838
2006
|
if (focusedUnitId) {
|
|
1839
|
-
const zone = zoneAtLevelPoint(
|
|
2007
|
+
const zone = zoneAtLevelPoint(
|
|
2008
|
+
event.localPosition[0],
|
|
2009
|
+
event.localPosition[2],
|
|
2010
|
+
event.localPosition[1],
|
|
2011
|
+
)
|
|
1840
2012
|
if (zone) paintZoneMembership(focusedUnitId, zone.id)
|
|
1841
2013
|
return
|
|
1842
2014
|
}
|
|
1843
|
-
|
|
1844
|
-
const activeStrategy = SELECTION_STRATEGIES[phase]
|
|
1845
|
-
if (activeStrategy) activeStrategy.handleDeselect()
|
|
2015
|
+
SELECTION_STRATEGIES[useEditor.getState().phase].handleDeselect()
|
|
1846
2016
|
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
2017
|
}
|
|
1854
2018
|
emitter.on('grid:click', onGridClick)
|
|
1855
2019
|
|
|
@@ -1861,7 +2025,7 @@ export const SelectionManager = () => {
|
|
|
1861
2025
|
}
|
|
1862
2026
|
}, [isCurveReshape, mode, movingNode, registryVersion])
|
|
1863
2027
|
|
|
1864
|
-
//
|
|
2028
|
+
// Hover and double-click
|
|
1865
2029
|
useEffect(() => {
|
|
1866
2030
|
// re-subscribe when plugin kinds register after mount (async plugin load)
|
|
1867
2031
|
void registryVersion
|
|
@@ -1869,96 +2033,94 @@ export const SelectionManager = () => {
|
|
|
1869
2033
|
if (movingNode || isCurveReshape) return
|
|
1870
2034
|
|
|
1871
2035
|
const onEnter = (event: NodeEvent) => {
|
|
1872
|
-
if (
|
|
2036
|
+
if (isCeilingGridHit(event)) return
|
|
2037
|
+
if (!selectionEnabled(useInteractionScope.getState().scope)) return
|
|
1873
2038
|
// A host-driven drag (handle resize/rotate, box-select) sets
|
|
1874
2039
|
// `inputDragging`. useNodeEvents still emits hover events during it so
|
|
1875
2040
|
// surface move tools keep tracking — but the select-hover outline must
|
|
1876
2041
|
// stay put, so don't repaint under the cursor mid-drag.
|
|
1877
2042
|
if (useViewer.getState().inputDragging) return
|
|
2043
|
+
// An editor handle anywhere along the ray owns the pointer — it takes the
|
|
2044
|
+
// press (handles draw on top of walls), so it takes the hover too: no
|
|
2045
|
+
// stopPropagation, so the handle behind the wall still hears its enter.
|
|
2046
|
+
if (pointerEventHitsEditorHandle(event.nativeEvent)) {
|
|
2047
|
+
clearSelectHover(event)
|
|
2048
|
+
return
|
|
2049
|
+
}
|
|
1878
2050
|
const node = resolveCanvasSelectionNode({
|
|
1879
2051
|
node: resolveSelectModeNodeTarget(event),
|
|
1880
2052
|
nodes: useScene.getState().nodes,
|
|
1881
2053
|
selectedIds: useViewer.getState().selection.selectedIds,
|
|
1882
2054
|
})
|
|
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
|
-
}
|
|
2055
|
+
// From site the click picks the whole building, so the hover shows it.
|
|
2056
|
+
if (useEditor.getState().phase === 'site') {
|
|
2057
|
+
const buildingId = resolveCanvasBuildingId(node, useScene.getState().nodes)
|
|
2058
|
+
if (!buildingId) return
|
|
2059
|
+
event.stopPropagation()
|
|
2060
|
+
useViewer.getState().setHoveredId(buildingId as AnyNodeId)
|
|
2061
|
+
return
|
|
1897
2062
|
}
|
|
1898
2063
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2064
|
+
if (node.type === 'building' || node.type === 'site') return
|
|
2065
|
+
// Rooms hover through their walls and floors; a zone's own volume never does.
|
|
2066
|
+
if (node.type === 'zone') return
|
|
2067
|
+
if (!isNodeInCurrentLevel(node)) return
|
|
1903
2068
|
|
|
1904
2069
|
event.stopPropagation()
|
|
1905
|
-
|
|
2070
|
+
if (
|
|
2071
|
+
roomPickingEnabled() &&
|
|
2072
|
+
hoverRoomFromHit(
|
|
2073
|
+
roomForEvent(event),
|
|
2074
|
+
selectionModifiersFromEvent(event.nativeEvent, modifierKeysRef.current),
|
|
2075
|
+
node.id,
|
|
2076
|
+
)
|
|
2077
|
+
)
|
|
2078
|
+
return
|
|
2079
|
+
useViewer.getState().setHoveredId(node.id)
|
|
1906
2080
|
}
|
|
1907
2081
|
|
|
1908
|
-
const
|
|
2082
|
+
const clearSelectHover = (event: NodeEvent) => {
|
|
2083
|
+
useEditor.getState().setHoveredRoom(null)
|
|
1909
2084
|
if (useViewer.getState().inputDragging) return
|
|
1910
|
-
const
|
|
2085
|
+
const nodes = useScene.getState().nodes
|
|
2086
|
+
const node = resolveCanvasSelectionNode({
|
|
1911
2087
|
node: resolveSelectModeNodeTarget(event),
|
|
1912
|
-
nodes
|
|
2088
|
+
nodes,
|
|
1913
2089
|
selectedIds: useViewer.getState().selection.selectedIds,
|
|
1914
|
-
})
|
|
2090
|
+
})
|
|
2091
|
+
const nodeId =
|
|
2092
|
+
useEditor.getState().phase === 'site' ? resolveCanvasBuildingId(node, nodes) : node.id
|
|
1915
2093
|
if (nodeId && useViewer.getState().hoveredId === nodeId) {
|
|
1916
2094
|
useViewer.setState({ hoveredId: null })
|
|
1917
2095
|
}
|
|
1918
2096
|
}
|
|
1919
2097
|
|
|
2098
|
+
const onLeave = (event: NodeEvent) => {
|
|
2099
|
+
if (isCeilingGridHit(event)) return
|
|
2100
|
+
clearSelectHover(event)
|
|
2101
|
+
}
|
|
2102
|
+
|
|
1920
2103
|
const onDoubleClick = (event: NodeEvent) => {
|
|
2104
|
+
if (isCeilingGridHit(event)) return
|
|
1921
2105
|
if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
|
|
1922
|
-
|
|
1923
|
-
node: resolveSelectModeNodeTarget(event),
|
|
2106
|
+
const node = resolveCanvasSelectionNode({
|
|
2107
|
+
node: resolveSelectModeNodeTarget(event, true),
|
|
1924
2108
|
nodes: useScene.getState().nodes,
|
|
1925
2109
|
selectedIds: useViewer.getState().selection.selectedIds,
|
|
1926
2110
|
})
|
|
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
|
-
}
|
|
2111
|
+
if (useEditor.getState().phase !== 'building' || !isNodeInCurrentLevel(node)) return
|
|
1952
2112
|
|
|
1953
2113
|
// While a unit is focused a double-click inside a zone selects that
|
|
1954
2114
|
// zone (focus stays); the two clicks before it cancel each other's paint.
|
|
1955
2115
|
if (useViewer.getState().focusedUnitId) {
|
|
1956
2116
|
const zone =
|
|
1957
|
-
node.type === 'zone'
|
|
2117
|
+
node.type === 'zone'
|
|
2118
|
+
? node
|
|
2119
|
+
: zoneAtWorldPoint(event.position[0], event.position[2], event.position[1])
|
|
1958
2120
|
if (zone) {
|
|
1959
2121
|
event.stopPropagation()
|
|
1960
2122
|
cancelPendingZonePaint(zone.id)
|
|
1961
|
-
SELECTION_STRATEGIES.
|
|
2123
|
+
SELECTION_STRATEGIES.building.handleSelect(
|
|
1962
2124
|
zone,
|
|
1963
2125
|
event.nativeEvent,
|
|
1964
2126
|
modifierKeysRef.current,
|
|
@@ -1968,34 +2130,16 @@ export const SelectionManager = () => {
|
|
|
1968
2130
|
}
|
|
1969
2131
|
}
|
|
1970
2132
|
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
if ((targetPhase && targetPhase !== useEditor.getState().phase) || forceSelect) {
|
|
2133
|
+
// A single click selects the whole roof or stair; the double-click dives
|
|
2134
|
+
// into the segment under the cursor.
|
|
2135
|
+
if (node.type === 'roof-segment' || node.type === 'stair-segment') {
|
|
1976
2136
|
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
|
-
}
|
|
2137
|
+
SELECTION_STRATEGIES.building.handleSelect(
|
|
2138
|
+
node,
|
|
2139
|
+
event.nativeEvent,
|
|
2140
|
+
modifierKeysRef.current,
|
|
2141
|
+
useViewer.getState().selection.selectedIds,
|
|
2142
|
+
)
|
|
1999
2143
|
}
|
|
2000
2144
|
}
|
|
2001
2145
|
|
|
@@ -2025,6 +2169,7 @@ export const SelectionManager = () => {
|
|
|
2025
2169
|
|
|
2026
2170
|
subscribedKinds.forEach((type) => {
|
|
2027
2171
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
|
2172
|
+
emitter.on(`${type}:move` as any, onEnter as any)
|
|
2028
2173
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
|
2029
2174
|
emitter.on(`${type}:double-click` as any, onDoubleClick as any)
|
|
2030
2175
|
})
|
|
@@ -2032,6 +2177,7 @@ export const SelectionManager = () => {
|
|
|
2032
2177
|
return () => {
|
|
2033
2178
|
subscribedKinds.forEach((type) => {
|
|
2034
2179
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
|
2180
|
+
emitter.off(`${type}:move` as any, onEnter as any)
|
|
2035
2181
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
|
2036
2182
|
emitter.off(`${type}:double-click` as any, onDoubleClick as any)
|
|
2037
2183
|
})
|
|
@@ -2045,6 +2191,7 @@ export const SelectionManager = () => {
|
|
|
2045
2191
|
if (mode !== 'delete') return
|
|
2046
2192
|
|
|
2047
2193
|
const onClick = (event: NodeEvent) => {
|
|
2194
|
+
if (isCeilingGridHit(event)) return
|
|
2048
2195
|
const node = event.node
|
|
2049
2196
|
if (!isNodeInCurrentLevel(node)) return
|
|
2050
2197
|
|
|
@@ -2063,14 +2210,16 @@ export const SelectionManager = () => {
|
|
|
2063
2210
|
}
|
|
2064
2211
|
|
|
2065
2212
|
const onEnter = (event: NodeEvent) => {
|
|
2213
|
+
if (isCeilingGridHit(event)) return
|
|
2066
2214
|
const node = event.node
|
|
2067
2215
|
if (!isNodeInCurrentLevel(node)) return
|
|
2068
2216
|
if (node.type === 'building' || node.type === 'site') return
|
|
2069
2217
|
event.stopPropagation()
|
|
2070
|
-
useViewer.
|
|
2218
|
+
useViewer.getState().setHoveredId(node.id)
|
|
2071
2219
|
}
|
|
2072
2220
|
|
|
2073
2221
|
const onLeave = (event: NodeEvent) => {
|
|
2222
|
+
if (isCeilingGridHit(event)) return
|
|
2074
2223
|
const nodeId = event?.node?.id
|
|
2075
2224
|
if (nodeId && useViewer.getState().hoveredId === nodeId) {
|
|
2076
2225
|
useViewer.setState({ hoveredId: null })
|
|
@@ -2121,6 +2270,7 @@ export const SelectionManager = () => {
|
|
|
2121
2270
|
<SelectionStateSync />
|
|
2122
2271
|
<SelectionMaterialSync />
|
|
2123
2272
|
<EditorOutlinerSync />
|
|
2273
|
+
<RoomHighlight3D />
|
|
2124
2274
|
</>
|
|
2125
2275
|
)
|
|
2126
2276
|
}
|
|
@@ -2372,39 +2522,18 @@ const EditorOutlinerSync = () => {
|
|
|
2372
2522
|
const registryVersion = useRegistryVersion()
|
|
2373
2523
|
const outliner = useViewer((s) => s.outliner)
|
|
2374
2524
|
const nodes = useScene((s) => s.nodes)
|
|
2525
|
+
const paintOutline = usePaintOutline((s) => s.surfaces)
|
|
2375
2526
|
|
|
2376
2527
|
useEffect(() => {
|
|
2377
2528
|
void geometryRevision
|
|
2378
2529
|
void registryVersion
|
|
2379
2530
|
let idsToHighlight: string[] = []
|
|
2380
2531
|
|
|
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
|
-
}
|
|
2532
|
+
// 1. Site outlines the building; inside it, the selection.
|
|
2533
|
+
if (phase === 'site') {
|
|
2534
|
+
if (selection.buildingId) idsToHighlight = [selection.buildingId]
|
|
2535
|
+
} else {
|
|
2536
|
+
idsToHighlight = Array.from(new Set([...selection.selectedIds, ...previewSelectedIds]))
|
|
2408
2537
|
}
|
|
2409
2538
|
|
|
2410
2539
|
// 2. Sync with the imperative outliner arrays (mutate in place to keep references)
|
|
@@ -2417,7 +2546,11 @@ const EditorOutlinerSync = () => {
|
|
|
2417
2546
|
}
|
|
2418
2547
|
|
|
2419
2548
|
outliner.hoveredObjects.length = 0
|
|
2420
|
-
if (
|
|
2549
|
+
if (paintOutline) {
|
|
2550
|
+
// Paint on a floor plate: exactly the surfaces the click changes.
|
|
2551
|
+
for (const mesh of paintSurfaceMeshes(paintOutline, (id) => sceneRegistry.nodes.get(id)))
|
|
2552
|
+
if (mesh.parent) outliner.hoveredObjects.push(mesh)
|
|
2553
|
+
} else if (hoveredId) {
|
|
2421
2554
|
if (!nodes[hoveredId as AnyNodeId]) {
|
|
2422
2555
|
useViewer.setState({ hoveredId: null })
|
|
2423
2556
|
} else {
|
|
@@ -2440,6 +2573,7 @@ const EditorOutlinerSync = () => {
|
|
|
2440
2573
|
hoveredId,
|
|
2441
2574
|
outliner,
|
|
2442
2575
|
nodes,
|
|
2576
|
+
paintOutline,
|
|
2443
2577
|
])
|
|
2444
2578
|
|
|
2445
2579
|
return null
|