@pascal-app/editor 0.8.0 → 0.9.2
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 +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,621 +1,543 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
type AnyNodeId,
|
|
4
|
-
type CeilingNode,
|
|
5
|
-
emitter,
|
|
6
|
-
type GridEvent,
|
|
7
|
-
type ItemNode,
|
|
8
|
-
type LevelNode,
|
|
9
|
-
type SlabNode,
|
|
10
|
-
sceneRegistry,
|
|
11
|
-
useScene,
|
|
12
|
-
type WallNode,
|
|
13
|
-
type ZoneNode,
|
|
14
|
-
} from '@pascal-app/core'
|
|
1
|
+
import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'
|
|
15
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
16
3
|
import { useThree } from '@react-three/fiber'
|
|
17
|
-
import { useEffect, useRef } from 'react'
|
|
4
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
18
5
|
import {
|
|
19
6
|
Box3,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type Group,
|
|
24
|
-
LineBasicMaterial,
|
|
25
|
-
LineSegments,
|
|
26
|
-
type Mesh,
|
|
7
|
+
type Camera,
|
|
8
|
+
Matrix4,
|
|
9
|
+
type Object3D,
|
|
27
10
|
Plane,
|
|
28
11
|
Raycaster,
|
|
29
12
|
Vector2,
|
|
30
13
|
Vector3,
|
|
31
14
|
} from 'three'
|
|
32
|
-
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
33
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
34
15
|
import useEditor from '../../../store/use-editor'
|
|
35
|
-
import
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
16
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
17
|
+
import {
|
|
18
|
+
clearBoxSelectHandled,
|
|
19
|
+
isBoxSelectPointerSuppressed,
|
|
20
|
+
markBoxSelectHandled,
|
|
21
|
+
} from './box-select-state'
|
|
22
|
+
import {
|
|
23
|
+
convexHull2D,
|
|
24
|
+
type Point2,
|
|
25
|
+
polygonsIntersect,
|
|
26
|
+
rectIntersectsHull,
|
|
27
|
+
segmentIntersectsPolygon,
|
|
28
|
+
} from './marquee-geometry'
|
|
29
|
+
import { PlaneBoxSelectTool } from './plane-box-select-tool'
|
|
30
|
+
import {
|
|
31
|
+
createScreenRectangleSelectionElement,
|
|
32
|
+
hideScreenRectangleSelectionElement,
|
|
33
|
+
intersectScreenRects,
|
|
34
|
+
normalizeScreenRect,
|
|
35
|
+
SCREEN_RECTANGLE_SELECTION_DRAG_THRESHOLD_PX,
|
|
36
|
+
type ScreenRect,
|
|
37
|
+
screenRectFromDomRect,
|
|
38
|
+
updateScreenRectangleSelectionElement,
|
|
39
|
+
} from './screen-rectangle-selection'
|
|
40
|
+
import { collectSelectableCandidateIds } from './select-candidates'
|
|
41
|
+
|
|
42
|
+
const tempBox = new Box3()
|
|
43
|
+
const tempChildBox = new Box3()
|
|
44
|
+
const tempInvWorld = new Matrix4()
|
|
45
|
+
const tempRelMatrix = new Matrix4()
|
|
46
|
+
const tempWorldPoint = new Vector3()
|
|
47
|
+
const tempScreenPoint = new Vector3()
|
|
48
|
+
const tempNDC = new Vector2()
|
|
49
|
+
const tempPlane = new Plane()
|
|
50
|
+
const tempRaycaster = new Raycaster()
|
|
51
|
+
const UP = new Vector3(0, 1, 0)
|
|
52
|
+
const boxCorners = [
|
|
53
|
+
new Vector3(),
|
|
54
|
+
new Vector3(),
|
|
55
|
+
new Vector3(),
|
|
56
|
+
new Vector3(),
|
|
57
|
+
new Vector3(),
|
|
58
|
+
new Vector3(),
|
|
59
|
+
new Vector3(),
|
|
60
|
+
new Vector3(),
|
|
61
|
+
]
|
|
81
62
|
|
|
82
|
-
function
|
|
63
|
+
function haveSameIds(currentIds: string[], nextIds: string[]): boolean {
|
|
83
64
|
return (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Math.min(az, bz) <= cz &&
|
|
87
|
-
cz <= Math.max(az, bz)
|
|
65
|
+
currentIds.length === nextIds.length &&
|
|
66
|
+
currentIds.every((currentId, index) => currentId === nextIds[index])
|
|
88
67
|
)
|
|
89
68
|
}
|
|
90
69
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
[b.maxX, b.minZ, b.maxX, b.maxZ],
|
|
103
|
-
[b.maxX, b.maxZ, b.minX, b.maxZ],
|
|
104
|
-
[b.minX, b.maxZ, b.minX, b.minZ],
|
|
70
|
+
function projectWorldPointToScreen(
|
|
71
|
+
point: Vector3,
|
|
72
|
+
camera: Camera,
|
|
73
|
+
canvasRect: DOMRect,
|
|
74
|
+
): [number, number] | null {
|
|
75
|
+
tempScreenPoint.copy(point).project(camera)
|
|
76
|
+
if (tempScreenPoint.z < -1 || tempScreenPoint.z > 1) return null
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
canvasRect.left + (tempScreenPoint.x * 0.5 + 0.5) * canvasRect.width,
|
|
80
|
+
canvasRect.top + (-tempScreenPoint.y * 0.5 + 0.5) * canvasRect.height,
|
|
105
81
|
]
|
|
106
|
-
for (const [ex1, ez1, ex2, ez2] of edges) {
|
|
107
|
-
if (segmentsIntersect(x1, z1, x2, z2, ex1, ez1, ex2, ez2)) return true
|
|
108
|
-
}
|
|
109
|
-
return false
|
|
110
82
|
}
|
|
111
83
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
[b.minX, b.maxZ, b.minX, b.minZ],
|
|
128
|
-
]
|
|
129
|
-
for (let i = 0; i < polygon.length; i++) {
|
|
130
|
-
const [px1, pz1] = polygon[i]!
|
|
131
|
-
const [px2, pz2] = polygon[(i + 1) % polygon.length]!
|
|
132
|
-
for (const [ex1, ez1, ex2, ez2] of edges) {
|
|
133
|
-
if (segmentsIntersect(px1, pz1, px2, pz2, ex1, ez1, ex2, ez2)) return true
|
|
84
|
+
/**
|
|
85
|
+
* Union bounding box of the object's mesh geometry in the OBJECT's OWN frame
|
|
86
|
+
* (an oriented box). The world AABB the previous implementation used inflates
|
|
87
|
+
* around rotated geometry — a diagonal wall's world AABB spans a whole square
|
|
88
|
+
* — and projecting THAT to a screen AABB inflates again, which made the
|
|
89
|
+
* marquee select objects visually far from the cursor.
|
|
90
|
+
*/
|
|
91
|
+
function computeLocalBox(object: Object3D): Box3 | null {
|
|
92
|
+
tempBox.makeEmpty()
|
|
93
|
+
tempInvWorld.copy(object.matrixWorld).invert()
|
|
94
|
+
object.traverse((child) => {
|
|
95
|
+
const mesh = child as {
|
|
96
|
+
isMesh?: boolean
|
|
97
|
+
geometry?: { boundingBox: Box3 | null; computeBoundingBox: () => void }
|
|
98
|
+
matrixWorld: import('three').Matrix4
|
|
134
99
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
100
|
+
if (!mesh.isMesh || !mesh.geometry) return
|
|
101
|
+
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox()
|
|
102
|
+
const bounds = mesh.geometry.boundingBox
|
|
103
|
+
if (!bounds || bounds.isEmpty()) return
|
|
104
|
+
tempChildBox.copy(bounds)
|
|
105
|
+
tempRelMatrix.multiplyMatrices(tempInvWorld, mesh.matrixWorld)
|
|
106
|
+
tempChildBox.applyMatrix4(tempRelMatrix)
|
|
107
|
+
tempBox.union(tempChildBox)
|
|
108
|
+
})
|
|
109
|
+
return tempBox.isEmpty() ? null : tempBox
|
|
138
110
|
}
|
|
139
111
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
112
|
+
/** Screen-space convex hull of the object's oriented bounding box. */
|
|
113
|
+
function getObjectScreenHull(
|
|
114
|
+
object: Object3D,
|
|
115
|
+
camera: Camera,
|
|
116
|
+
canvasRect: DOMRect,
|
|
117
|
+
): Point2[] | null {
|
|
118
|
+
object.updateWorldMatrix(true, true)
|
|
119
|
+
const localBox = computeLocalBox(object)
|
|
120
|
+
|
|
121
|
+
if (!localBox) {
|
|
122
|
+
object.getWorldPosition(tempWorldPoint)
|
|
123
|
+
const projected = projectWorldPointToScreen(tempWorldPoint, camera, canvasRect)
|
|
124
|
+
return projected ? [projected] : null
|
|
148
125
|
}
|
|
149
|
-
return inside
|
|
150
|
-
}
|
|
151
126
|
|
|
152
|
-
|
|
127
|
+
boxCorners[0]!.set(localBox.min.x, localBox.min.y, localBox.min.z)
|
|
128
|
+
boxCorners[1]!.set(localBox.min.x, localBox.min.y, localBox.max.z)
|
|
129
|
+
boxCorners[2]!.set(localBox.min.x, localBox.max.y, localBox.min.z)
|
|
130
|
+
boxCorners[3]!.set(localBox.min.x, localBox.max.y, localBox.max.z)
|
|
131
|
+
boxCorners[4]!.set(localBox.max.x, localBox.min.y, localBox.min.z)
|
|
132
|
+
boxCorners[5]!.set(localBox.max.x, localBox.min.y, localBox.max.z)
|
|
133
|
+
boxCorners[6]!.set(localBox.max.x, localBox.max.y, localBox.min.z)
|
|
134
|
+
boxCorners[7]!.set(localBox.max.x, localBox.max.y, localBox.max.z)
|
|
135
|
+
|
|
136
|
+
const projectedPoints: Point2[] = []
|
|
137
|
+
for (const corner of boxCorners) {
|
|
138
|
+
corner.applyMatrix4(object.matrixWorld)
|
|
139
|
+
const projected = projectWorldPointToScreen(corner, camera, canvasRect)
|
|
140
|
+
if (projected) projectedPoints.push(projected)
|
|
141
|
+
}
|
|
153
142
|
|
|
154
|
-
|
|
155
|
-
|
|
143
|
+
if (projectedPoints.length === 0) {
|
|
144
|
+
object.getWorldPosition(tempWorldPoint)
|
|
145
|
+
const projected = projectWorldPointToScreen(tempWorldPoint, camera, canvasRect)
|
|
146
|
+
return projected ? [projected] : null
|
|
147
|
+
}
|
|
156
148
|
|
|
157
|
-
|
|
158
|
-
const obj = sceneRegistry.nodes.get(nodeId)
|
|
159
|
-
if (!obj) return null
|
|
160
|
-
obj.getWorldPosition(_tempVec)
|
|
161
|
-
return [_tempVec.x, _tempVec.z]
|
|
149
|
+
return convexHull2D(projectedPoints)
|
|
162
150
|
}
|
|
163
151
|
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
_tempBox.setFromObject(obj)
|
|
170
|
-
|
|
171
|
-
if (_tempBox.isEmpty()) {
|
|
172
|
-
const xz = getNodeWorldXZ(nodeId)
|
|
173
|
-
return Boolean(xz && pointInBounds(xz[0], xz[1], bounds))
|
|
152
|
+
function isObjectVisible(object: Object3D): boolean {
|
|
153
|
+
let current: Object3D | null = object
|
|
154
|
+
while (current) {
|
|
155
|
+
if (!current.visible) return false
|
|
156
|
+
current = current.parent
|
|
174
157
|
}
|
|
175
|
-
|
|
176
|
-
return !(
|
|
177
|
-
_tempBox.max.x < bounds.minX ||
|
|
178
|
-
_tempBox.min.x > bounds.maxX ||
|
|
179
|
-
_tempBox.max.z < bounds.minZ ||
|
|
180
|
-
_tempBox.min.z > bounds.maxZ
|
|
181
|
-
)
|
|
158
|
+
return true
|
|
182
159
|
}
|
|
183
160
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
161
|
+
const isVec2 = (v: unknown): v is [number, number] =>
|
|
162
|
+
Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === 'number')
|
|
163
|
+
const isVec2Array = (v: unknown): v is [number, number][] =>
|
|
164
|
+
Array.isArray(v) && v.length > 0 && v.every(isVec2)
|
|
188
165
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The marquee rect projected onto the active level's floor plane, in the
|
|
168
|
+
* LEVEL frame — a convex quad (perspective keeps rect convexity). Null when
|
|
169
|
+
* any corner ray misses the plane (camera near the horizon); callers fall
|
|
170
|
+
* back to the screen-hull test then.
|
|
171
|
+
*/
|
|
172
|
+
function marqueeGroundQuad(rect: ScreenRect, camera: Camera, canvasRect: DOMRect): Point2[] | null {
|
|
173
|
+
const levelId = useViewer.getState().selection.levelId
|
|
174
|
+
const levelObject = levelId ? sceneRegistry.nodes.get(levelId) : null
|
|
175
|
+
if (!levelObject) return null
|
|
176
|
+
levelObject.updateWorldMatrix(true, false)
|
|
177
|
+
tempInvWorld.copy(levelObject.matrixWorld).invert()
|
|
178
|
+
levelObject.getWorldPosition(tempWorldPoint)
|
|
179
|
+
tempPlane.set(UP, -tempWorldPoint.y)
|
|
192
180
|
|
|
181
|
+
const corners: [number, number][] = [
|
|
182
|
+
[rect.minX, rect.minY],
|
|
183
|
+
[rect.maxX, rect.minY],
|
|
184
|
+
[rect.maxX, rect.maxY],
|
|
185
|
+
[rect.minX, rect.maxY],
|
|
186
|
+
]
|
|
187
|
+
const quad: Point2[] = []
|
|
188
|
+
for (const [cx, cy] of corners) {
|
|
189
|
+
tempNDC.set(
|
|
190
|
+
((cx - canvasRect.left) / canvasRect.width) * 2 - 1,
|
|
191
|
+
-((cy - canvasRect.top) / canvasRect.height) * 2 + 1,
|
|
192
|
+
)
|
|
193
|
+
tempRaycaster.setFromCamera(tempNDC, camera)
|
|
194
|
+
if (!tempRaycaster.ray.intersectPlane(tempPlane, tempWorldPoint)) return null
|
|
195
|
+
tempWorldPoint.applyMatrix4(tempInvWorld)
|
|
196
|
+
quad.push([tempWorldPoint.x, tempWorldPoint.z])
|
|
197
|
+
}
|
|
198
|
+
return quad
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function collectNodeIdsInScreenRect(
|
|
202
|
+
rect: ScreenRect,
|
|
203
|
+
camera: Camera,
|
|
204
|
+
canvas: HTMLCanvasElement,
|
|
205
|
+
): string[] {
|
|
206
|
+
const canvasRect = canvas.getBoundingClientRect()
|
|
193
207
|
const result: string[] = []
|
|
194
208
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
209
|
+
// Plan-footprint membership for the data kinds (walls / fences by their
|
|
210
|
+
// segment, slabs / ceilings / zones by their polygon) — exact under any
|
|
211
|
+
// rotation, matching the plane-marquee tool's semantics. Kinds whose
|
|
212
|
+
// placement lives in mesh transforms (items, columns, …) intersect the
|
|
213
|
+
// marquee with their oriented bbox projected to a screen hull instead.
|
|
214
|
+
const quad = marqueeGroundQuad(rect, camera, canvasRect)
|
|
215
|
+
const nodes = useScene.getState().nodes
|
|
216
|
+
|
|
217
|
+
for (const id of collectSelectableCandidateIds()) {
|
|
218
|
+
const object = sceneRegistry.nodes.get(id)
|
|
219
|
+
if (!object || !isObjectVisible(object)) continue
|
|
220
|
+
|
|
221
|
+
if (quad) {
|
|
222
|
+
const node = nodes[id as keyof typeof nodes] as
|
|
223
|
+
| { start?: unknown; end?: unknown; polygon?: unknown }
|
|
224
|
+
| undefined
|
|
225
|
+
if (node) {
|
|
226
|
+
const { start, end, polygon } = node
|
|
227
|
+
if (isVec2(start) && isVec2(end)) {
|
|
228
|
+
if (segmentIntersectsPolygon(start, end, quad)) result.push(id)
|
|
229
|
+
continue
|
|
216
230
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (!child) continue
|
|
221
|
-
if (
|
|
222
|
-
child.type === 'window' ||
|
|
223
|
-
child.type === 'door' ||
|
|
224
|
-
(child.type === 'item' &&
|
|
225
|
-
((child as ItemNode).asset.category === 'door' ||
|
|
226
|
-
(child as ItemNode).asset.category === 'window'))
|
|
227
|
-
) {
|
|
228
|
-
const xz = getNodeWorldXZ(child.id)
|
|
229
|
-
if (xz && pointInBounds(xz[0], xz[1], bounds)) {
|
|
230
|
-
result.push(child.id)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
} else if (node.type === 'slab') {
|
|
235
|
-
const slab = node as SlabNode
|
|
236
|
-
if (polygonIntersectsBounds(slab.polygon, bounds)) {
|
|
237
|
-
result.push(slab.id)
|
|
238
|
-
}
|
|
239
|
-
} else if (node.type === 'ceiling') {
|
|
240
|
-
const ceiling = node as CeilingNode
|
|
241
|
-
if (polygonIntersectsBounds(ceiling.polygon, bounds)) {
|
|
242
|
-
result.push(ceiling.id)
|
|
243
|
-
}
|
|
244
|
-
} else if (node.type === 'roof') {
|
|
245
|
-
const xz = getNodeWorldXZ(node.id)
|
|
246
|
-
if (xz && pointInBounds(xz[0], xz[1], bounds)) {
|
|
247
|
-
result.push(node.id)
|
|
248
|
-
}
|
|
249
|
-
} else if (node.type === 'stair') {
|
|
250
|
-
if (objectBoundsIntersectsBounds(node.id, bounds)) {
|
|
251
|
-
result.push(node.id)
|
|
252
|
-
}
|
|
253
|
-
} else if (node.type === 'item') {
|
|
254
|
-
const item = node as ItemNode
|
|
255
|
-
if (item.asset.category === 'door' || item.asset.category === 'window') continue
|
|
256
|
-
const xz = getNodeWorldXZ(item.id)
|
|
257
|
-
if (xz && pointInBounds(xz[0], xz[1], bounds)) {
|
|
258
|
-
result.push(item.id)
|
|
231
|
+
if (isVec2Array(polygon)) {
|
|
232
|
+
if (polygonsIntersect(polygon, quad)) result.push(id)
|
|
233
|
+
continue
|
|
259
234
|
}
|
|
260
235
|
}
|
|
261
236
|
}
|
|
237
|
+
|
|
238
|
+
const hull = getObjectScreenHull(object, camera, canvasRect)
|
|
239
|
+
if (hull && rectIntersectsHull(rect, hull)) {
|
|
240
|
+
result.push(id)
|
|
241
|
+
}
|
|
262
242
|
}
|
|
263
243
|
|
|
264
244
|
return result
|
|
265
245
|
}
|
|
266
246
|
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
}
|
|
247
|
+
function commitBoxSelection(ids: string[], event: PointerEvent) {
|
|
248
|
+
const shouldAppend = event.metaKey || event.ctrlKey || event.shiftKey
|
|
249
|
+
const { phase, structureLayer } = useEditor.getState()
|
|
250
|
+
const viewer = useViewer.getState()
|
|
273
251
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
end: Vector3,
|
|
281
|
-
y: number,
|
|
282
|
-
) {
|
|
283
|
-
const cx = (start.x + end.x) / 2
|
|
284
|
-
const cz = (start.z + end.z) / 2
|
|
285
|
-
const w = Math.abs(end.x - start.x)
|
|
286
|
-
const h = Math.abs(end.z - start.z)
|
|
287
|
-
|
|
288
|
-
if (w < 0.01 && h < 0.01) {
|
|
289
|
-
fillMesh.visible = false
|
|
290
|
-
outline.visible = false
|
|
252
|
+
if (phase === 'structure' && structureLayer === 'zones') {
|
|
253
|
+
if (ids.length > 0) {
|
|
254
|
+
viewer.setSelection({ zoneId: ids[0] as ZoneNode['id'] })
|
|
255
|
+
} else if (!shouldAppend) {
|
|
256
|
+
viewer.setSelection({ zoneId: null })
|
|
257
|
+
}
|
|
291
258
|
return
|
|
292
259
|
}
|
|
293
260
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
outline.visible = true
|
|
301
|
-
const oy = y + 0.03
|
|
302
|
-
const x0 = cx - w / 2
|
|
303
|
-
const x1 = cx + w / 2
|
|
304
|
-
const z0 = cz - h / 2
|
|
305
|
-
const z1 = cz + h / 2
|
|
306
|
-
const pos = outline.geometry.attributes.position as BufferAttribute
|
|
307
|
-
// bottom: (x0,z0)→(x1,z0)
|
|
308
|
-
pos.setXYZ(0, x0, oy, z0)
|
|
309
|
-
pos.setXYZ(1, x1, oy, z0)
|
|
310
|
-
// right: (x1,z0)→(x1,z1)
|
|
311
|
-
pos.setXYZ(2, x1, oy, z0)
|
|
312
|
-
pos.setXYZ(3, x1, oy, z1)
|
|
313
|
-
// top: (x1,z1)→(x0,z1)
|
|
314
|
-
pos.setXYZ(4, x1, oy, z1)
|
|
315
|
-
pos.setXYZ(5, x0, oy, z1)
|
|
316
|
-
// left: (x0,z1)→(x0,z0)
|
|
317
|
-
pos.setXYZ(6, x0, oy, z1)
|
|
318
|
-
pos.setXYZ(7, x0, oy, z0)
|
|
319
|
-
pos.needsUpdate = true
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ── Outline geometry (allocated once, reused) ───────────────────────────────
|
|
323
|
-
|
|
324
|
-
function createOutlineSegments(): LineSegments {
|
|
325
|
-
const geo = new BufferGeometry()
|
|
326
|
-
// 4 edges × 2 vertices each = 8 vertices
|
|
327
|
-
const positions = new Float32Array(8 * 3)
|
|
328
|
-
geo.setAttribute('position', new BufferAttribute(positions, 3))
|
|
329
|
-
|
|
330
|
-
const mat = new LineBasicMaterial({
|
|
331
|
-
color: BOX_SELECT_ACCENT_COLOR,
|
|
332
|
-
depthTest: false,
|
|
333
|
-
depthWrite: false,
|
|
334
|
-
transparent: true,
|
|
335
|
-
opacity: 0.85,
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
const segments = new LineSegments(geo, mat)
|
|
339
|
-
segments.layers.set(EDITOR_LAYER)
|
|
340
|
-
segments.renderOrder = 2
|
|
341
|
-
segments.visible = false
|
|
342
|
-
segments.frustumCulled = false
|
|
343
|
-
|
|
344
|
-
return segments
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ── Drag threshold (pixels) ─────────────────────────────────────────────────
|
|
348
|
-
|
|
349
|
-
const BOX_SELECT_ACCENT_COLOR = '#818cf8'
|
|
350
|
-
const DRAG_THRESHOLD_PX = 4
|
|
351
|
-
|
|
352
|
-
function getSnappedGridPosition(x: number, z: number): [number, number] {
|
|
353
|
-
return [Math.round(x * 2) / 2, Math.round(z * 2) / 2]
|
|
354
|
-
}
|
|
261
|
+
if (shouldAppend) {
|
|
262
|
+
viewer.setSelection({
|
|
263
|
+
selectedIds: Array.from(new Set([...viewer.selection.selectedIds, ...ids])),
|
|
264
|
+
})
|
|
265
|
+
return
|
|
266
|
+
}
|
|
355
267
|
|
|
356
|
-
|
|
357
|
-
const [snappedX, snappedZ] = getSnappedGridPosition(x, z)
|
|
358
|
-
target.set(snappedX, y, snappedZ)
|
|
268
|
+
viewer.setSelection({ selectedIds: ids })
|
|
359
269
|
}
|
|
360
270
|
|
|
361
|
-
// ── Component ───────────────────────────────────────────────────────────────
|
|
362
|
-
|
|
363
271
|
export const BoxSelectTool: React.FC = () => {
|
|
272
|
+
const phase = useEditor((s) => s.phase)
|
|
364
273
|
const mode = useEditor((s) => s.mode)
|
|
365
274
|
const selectionTool = useEditor((s) => s.floorplanSelectionTool)
|
|
366
|
-
const isActive = mode === 'select' &&
|
|
275
|
+
const isActive = mode === 'select' && (phase === 'structure' || phase === 'furnish')
|
|
367
276
|
|
|
368
277
|
if (!isActive) return null
|
|
369
278
|
|
|
370
|
-
|
|
279
|
+
if (selectionTool === 'marquee') {
|
|
280
|
+
return <PlaneBoxSelectTool />
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return <ScreenRectangleSelectTool />
|
|
371
284
|
}
|
|
372
285
|
|
|
373
|
-
const
|
|
374
|
-
<Icon
|
|
375
|
-
color="currentColor"
|
|
376
|
-
height={24}
|
|
377
|
-
icon="mdi:select-drag"
|
|
378
|
-
style={{ filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.5))' }}
|
|
379
|
-
width={24}
|
|
380
|
-
/>
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
const BoxSelectToolInner: React.FC = () => {
|
|
286
|
+
const ScreenRectangleSelectTool: React.FC = () => {
|
|
384
287
|
const { camera, gl } = useThree()
|
|
385
288
|
const setPreviewSelectedIds = useViewer((state) => state.setPreviewSelectedIds)
|
|
386
|
-
const
|
|
387
|
-
const rectFillRef = useRef<Mesh>(null!)
|
|
388
|
-
const outlineRef = useRef(createOutlineSegments())
|
|
389
|
-
const startPoint = useRef(new Vector3())
|
|
390
|
-
const currentPoint = useRef(new Vector3())
|
|
391
|
-
const pointerDown = useRef(false)
|
|
392
|
-
const isDragging = useRef(false)
|
|
393
|
-
const startClientX = useRef(0)
|
|
394
|
-
const startClientY = useRef(0)
|
|
395
|
-
const gridY = useRef(0)
|
|
396
|
-
const previousGridPosition = useRef<[number, number] | null>(null)
|
|
289
|
+
const elementRef = useRef<HTMLDivElement | null>(null)
|
|
397
290
|
const previewSelectedIdsRef = useRef<string[]>([])
|
|
291
|
+
const pointerDownRef = useRef(false)
|
|
292
|
+
const isDraggingRef = useRef(false)
|
|
293
|
+
const ownsInputDraggingRef = useRef(false)
|
|
294
|
+
const pointerIdRef = useRef<number | null>(null)
|
|
295
|
+
const startClientXRef = useRef(0)
|
|
296
|
+
const startClientYRef = useRef(0)
|
|
297
|
+
const currentClientXRef = useRef(0)
|
|
298
|
+
const currentClientYRef = useRef(0)
|
|
299
|
+
const spaceDownRef = useRef(false)
|
|
300
|
+
// rAF throttle for the expensive marquee preview pass. pointermove can fire
|
|
301
|
+
// several times per animation frame; the per-node AABB projection in
|
|
302
|
+
// `collectNodeIdsInScreenRect` only needs to run once per frame. We stash the
|
|
303
|
+
// latest clamped rect and process it inside the rAF callback.
|
|
304
|
+
const previewRafRef = useRef<number | null>(null)
|
|
305
|
+
const pendingPreviewRectRef = useRef<ScreenRect | null>(null)
|
|
306
|
+
|
|
307
|
+
const syncPreviewSelectedIds = useCallback(
|
|
308
|
+
(nextIds: string[]) => {
|
|
309
|
+
if (haveSameIds(previewSelectedIdsRef.current, nextIds)) return
|
|
310
|
+
|
|
311
|
+
previewSelectedIdsRef.current = nextIds
|
|
312
|
+
setPreviewSelectedIds(nextIds)
|
|
313
|
+
},
|
|
314
|
+
[setPreviewSelectedIds],
|
|
315
|
+
)
|
|
398
316
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
317
|
+
const resetDrag = useCallback(() => {
|
|
318
|
+
pointerDownRef.current = false
|
|
319
|
+
isDraggingRef.current = false
|
|
320
|
+
pointerIdRef.current = null
|
|
321
|
+
if (previewRafRef.current !== null) {
|
|
322
|
+
cancelAnimationFrame(previewRafRef.current)
|
|
323
|
+
previewRafRef.current = null
|
|
324
|
+
}
|
|
325
|
+
pendingPreviewRectRef.current = null
|
|
326
|
+
hideScreenRectangleSelectionElement(elementRef.current)
|
|
327
|
+
syncPreviewSelectedIds([])
|
|
328
|
+
|
|
329
|
+
if (ownsInputDraggingRef.current) {
|
|
330
|
+
useViewer.getState().setInputDragging(false)
|
|
331
|
+
ownsInputDraggingRef.current = false
|
|
332
|
+
}
|
|
333
|
+
useInteractionScope.getState().endIf((s) => s.kind === 'box-select')
|
|
334
|
+
}, [syncPreviewSelectedIds])
|
|
404
335
|
|
|
405
|
-
// Cleanup outline geometry on unmount
|
|
406
336
|
useEffect(() => {
|
|
407
|
-
const
|
|
337
|
+
const element = createScreenRectangleSelectionElement()
|
|
338
|
+
document.body.appendChild(element)
|
|
339
|
+
elementRef.current = element
|
|
340
|
+
|
|
408
341
|
return () => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
outline.geometry.dispose()
|
|
412
|
-
;(outline.material as LineBasicMaterial).dispose()
|
|
342
|
+
element.remove()
|
|
343
|
+
elementRef.current = null
|
|
413
344
|
}
|
|
414
|
-
}, [
|
|
345
|
+
}, [])
|
|
415
346
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
return
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
const cancelForSpace = () => {
|
|
349
|
+
if (!pointerDownRef.current) return
|
|
350
|
+
markBoxSelectHandled()
|
|
351
|
+
resetDrag()
|
|
419
352
|
}
|
|
420
353
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
354
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
355
|
+
if (event.code !== 'Space') return
|
|
356
|
+
spaceDownRef.current = true
|
|
357
|
+
cancelForSpace()
|
|
358
|
+
}
|
|
424
359
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const levelId = state.selection.levelId
|
|
429
|
-
if (!levelId) return
|
|
430
|
-
const obj = sceneRegistry.nodes.get(levelId)
|
|
431
|
-
if (obj) groundPlane.current.constant = -obj.position.y
|
|
432
|
-
})
|
|
433
|
-
// Set initial value
|
|
434
|
-
const levelId = useViewer.getState().selection.levelId
|
|
435
|
-
if (levelId) {
|
|
436
|
-
const obj = sceneRegistry.nodes.get(levelId)
|
|
437
|
-
if (obj) groundPlane.current.constant = -obj.position.y
|
|
360
|
+
const onKeyUp = (event: KeyboardEvent) => {
|
|
361
|
+
if (event.code !== 'Space') return
|
|
362
|
+
spaceDownRef.current = false
|
|
438
363
|
}
|
|
439
|
-
return unsubscribe
|
|
440
|
-
}, [])
|
|
441
364
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
pointerNDC.current.y = -((e.clientY - rect.top) / rect.height) * 2 + 1
|
|
446
|
-
raycasterRef.current.setFromCamera(pointerNDC.current, camera)
|
|
447
|
-
if (raycasterRef.current.ray.intersectPlane(groundPlane.current, hitPoint.current)) {
|
|
448
|
-
return hitPoint.current
|
|
365
|
+
const onBlur = () => {
|
|
366
|
+
spaceDownRef.current = false
|
|
367
|
+
resetDrag()
|
|
449
368
|
}
|
|
450
|
-
|
|
451
|
-
|
|
369
|
+
|
|
370
|
+
window.addEventListener('keydown', onKeyDown)
|
|
371
|
+
window.addEventListener('keyup', onKeyUp)
|
|
372
|
+
window.addEventListener('blur', onBlur)
|
|
373
|
+
|
|
374
|
+
return () => {
|
|
375
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
376
|
+
window.removeEventListener('keyup', onKeyUp)
|
|
377
|
+
window.removeEventListener('blur', onBlur)
|
|
378
|
+
}
|
|
379
|
+
}, [resetDrag])
|
|
452
380
|
|
|
453
381
|
useEffect(() => {
|
|
454
382
|
const canvas = gl.domElement
|
|
455
383
|
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
setSnappedPoint(startPoint.current, point.x, point.y, point.z)
|
|
464
|
-
setSnappedPoint(currentPoint.current, point.x, point.y, point.z)
|
|
465
|
-
gridY.current = point.y
|
|
466
|
-
pointerDown.current = true
|
|
467
|
-
isDragging.current = false
|
|
468
|
-
previousGridPosition.current = getSnappedGridPosition(point.x, point.z)
|
|
469
|
-
startClientX.current = e.clientX
|
|
470
|
-
startClientY.current = e.clientY
|
|
471
|
-
syncPreviewSelectedIds([])
|
|
384
|
+
const flushPreview = () => {
|
|
385
|
+
previewRafRef.current = null
|
|
386
|
+
const rect = pendingPreviewRectRef.current
|
|
387
|
+
if (!rect) return
|
|
388
|
+
pendingPreviewRectRef.current = null
|
|
389
|
+
syncPreviewSelectedIds(collectNodeIdsInScreenRect(rect, camera, canvas))
|
|
472
390
|
}
|
|
473
391
|
|
|
474
|
-
const
|
|
475
|
-
if (
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
392
|
+
const updateDrag = (event: PointerEvent) => {
|
|
393
|
+
if (!pointerDownRef.current) return
|
|
394
|
+
if (pointerIdRef.current !== null && event.pointerId !== pointerIdRef.current) return
|
|
395
|
+
|
|
396
|
+
const viewer = useViewer.getState()
|
|
397
|
+
if (
|
|
398
|
+
isBoxSelectPointerSuppressed(event) ||
|
|
399
|
+
spaceDownRef.current ||
|
|
400
|
+
viewer.cameraDragging ||
|
|
401
|
+
(viewer.inputDragging && !ownsInputDraggingRef.current)
|
|
402
|
+
) {
|
|
403
|
+
markBoxSelectHandled()
|
|
404
|
+
resetDrag()
|
|
405
|
+
return
|
|
406
|
+
}
|
|
481
407
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
408
|
+
currentClientXRef.current = event.clientX
|
|
409
|
+
currentClientYRef.current = event.clientY
|
|
410
|
+
|
|
411
|
+
const dragDistance = Math.hypot(
|
|
412
|
+
currentClientXRef.current - startClientXRef.current,
|
|
413
|
+
currentClientYRef.current - startClientYRef.current,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if (!isDraggingRef.current && dragDistance >= SCREEN_RECTANGLE_SELECTION_DRAG_THRESHOLD_PX) {
|
|
417
|
+
isDraggingRef.current = true
|
|
418
|
+
ownsInputDraggingRef.current = true
|
|
419
|
+
useViewer.getState().setInputDragging(true)
|
|
420
|
+
useInteractionScope.getState().begin({ kind: 'box-select' })
|
|
421
|
+
markBoxSelectHandled()
|
|
422
|
+
try {
|
|
423
|
+
canvas.setPointerCapture(event.pointerId)
|
|
424
|
+
} catch {}
|
|
425
|
+
}
|
|
488
426
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
427
|
+
if (!isDraggingRef.current) return
|
|
428
|
+
|
|
429
|
+
event.preventDefault()
|
|
430
|
+
const rect = normalizeScreenRect(
|
|
431
|
+
startClientXRef.current,
|
|
432
|
+
startClientYRef.current,
|
|
433
|
+
currentClientXRef.current,
|
|
434
|
+
currentClientYRef.current,
|
|
435
|
+
)
|
|
436
|
+
const clampedRect = intersectScreenRects(
|
|
437
|
+
rect,
|
|
438
|
+
screenRectFromDomRect(canvas.getBoundingClientRect()),
|
|
439
|
+
)
|
|
440
|
+
if (!clampedRect) {
|
|
441
|
+
if (previewRafRef.current !== null) {
|
|
442
|
+
cancelAnimationFrame(previewRafRef.current)
|
|
443
|
+
previewRafRef.current = null
|
|
506
444
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
boxSelectHandled = false
|
|
512
|
-
}, 50)
|
|
445
|
+
pendingPreviewRectRef.current = null
|
|
446
|
+
hideScreenRectangleSelectionElement(elementRef.current)
|
|
447
|
+
syncPreviewSelectedIds([])
|
|
448
|
+
return
|
|
513
449
|
}
|
|
514
|
-
// NOTE: Short clicks (no drag) fall through to the SelectionManager's
|
|
515
|
-
// existing grid:click / node:click handlers — no extra logic needed here.
|
|
516
|
-
|
|
517
|
-
// Hide visuals
|
|
518
|
-
if (rectFillRef.current) rectFillRef.current.visible = false
|
|
519
|
-
if (outlineRef.current) outlineRef.current.visible = false
|
|
520
|
-
syncPreviewSelectedIds([])
|
|
521
450
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
451
|
+
updateScreenRectangleSelectionElement(elementRef.current!, clampedRect)
|
|
452
|
+
// Coalesce the per-node AABB projection to one run per animation frame.
|
|
453
|
+
pendingPreviewRectRef.current = clampedRect
|
|
454
|
+
if (previewRafRef.current === null) {
|
|
455
|
+
previewRafRef.current = requestAnimationFrame(flushPreview)
|
|
456
|
+
}
|
|
525
457
|
}
|
|
526
458
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
459
|
+
const finishDrag = (event: PointerEvent) => {
|
|
460
|
+
if (!pointerDownRef.current) return
|
|
461
|
+
if (pointerIdRef.current !== null && event.pointerId !== pointerIdRef.current) return
|
|
462
|
+
|
|
463
|
+
if (
|
|
464
|
+
isBoxSelectPointerSuppressed(event) ||
|
|
465
|
+
(useViewer.getState().inputDragging && !ownsInputDraggingRef.current)
|
|
466
|
+
) {
|
|
467
|
+
markBoxSelectHandled()
|
|
468
|
+
resetDrag()
|
|
469
|
+
return
|
|
470
|
+
}
|
|
535
471
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
472
|
+
if (isDraggingRef.current) {
|
|
473
|
+
event.preventDefault()
|
|
474
|
+
event.stopPropagation()
|
|
475
|
+
markBoxSelectHandled()
|
|
540
476
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
477
|
+
const rect = normalizeScreenRect(
|
|
478
|
+
startClientXRef.current,
|
|
479
|
+
startClientYRef.current,
|
|
480
|
+
event.clientX,
|
|
481
|
+
event.clientY,
|
|
482
|
+
)
|
|
483
|
+
const clampedRect = intersectScreenRects(
|
|
484
|
+
rect,
|
|
485
|
+
screenRectFromDomRect(canvas.getBoundingClientRect()),
|
|
486
|
+
)
|
|
487
|
+
const ids = clampedRect ? collectNodeIdsInScreenRect(clampedRect, camera, canvas) : []
|
|
488
|
+
commitBoxSelection(ids, event)
|
|
544
489
|
}
|
|
545
490
|
|
|
546
|
-
|
|
491
|
+
try {
|
|
492
|
+
canvas.releasePointerCapture(event.pointerId)
|
|
493
|
+
} catch {}
|
|
547
494
|
|
|
548
|
-
|
|
495
|
+
resetDrag()
|
|
496
|
+
}
|
|
549
497
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
498
|
+
const onCanvasPointerDown = (event: PointerEvent) => {
|
|
499
|
+
if (event.button !== 0) return
|
|
500
|
+
if (spaceDownRef.current) return
|
|
501
|
+
if (isBoxSelectPointerSuppressed(event)) return
|
|
502
|
+
|
|
503
|
+
const viewer = useViewer.getState()
|
|
504
|
+
if (viewer.cameraDragging || viewer.inputDragging) return
|
|
505
|
+
|
|
506
|
+
pointerDownRef.current = true
|
|
507
|
+
isDraggingRef.current = false
|
|
508
|
+
pointerIdRef.current = event.pointerId
|
|
509
|
+
startClientXRef.current = event.clientX
|
|
510
|
+
startClientYRef.current = event.clientY
|
|
511
|
+
currentClientXRef.current = event.clientX
|
|
512
|
+
currentClientYRef.current = event.clientY
|
|
513
|
+
syncPreviewSelectedIds([])
|
|
514
|
+
}
|
|
557
515
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
startPoint.current,
|
|
563
|
-
currentPoint.current,
|
|
564
|
-
gridY.current,
|
|
565
|
-
)
|
|
516
|
+
const onPointerCancel = (event: PointerEvent) => {
|
|
517
|
+
if (pointerIdRef.current !== null && event.pointerId !== pointerIdRef.current) return
|
|
518
|
+
resetDrag()
|
|
519
|
+
}
|
|
566
520
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
nextGridPosition[1] !== previousGridPosition.current[1])
|
|
572
|
-
) {
|
|
573
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
574
|
-
}
|
|
575
|
-
previousGridPosition.current = nextGridPosition
|
|
521
|
+
canvas.addEventListener('pointerdown', onCanvasPointerDown)
|
|
522
|
+
window.addEventListener('pointermove', updateDrag, { passive: false })
|
|
523
|
+
window.addEventListener('pointerup', finishDrag)
|
|
524
|
+
window.addEventListener('pointercancel', onPointerCancel)
|
|
576
525
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
syncPreviewSelectedIds(collectNodeIdsInBounds(bounds))
|
|
584
|
-
}
|
|
526
|
+
return () => {
|
|
527
|
+
canvas.removeEventListener('pointerdown', onCanvasPointerDown)
|
|
528
|
+
window.removeEventListener('pointermove', updateDrag)
|
|
529
|
+
window.removeEventListener('pointerup', finishDrag)
|
|
530
|
+
window.removeEventListener('pointercancel', onPointerCancel)
|
|
531
|
+
resetDrag()
|
|
585
532
|
}
|
|
533
|
+
}, [camera, gl, resetDrag, syncPreviewSelectedIds])
|
|
586
534
|
|
|
587
|
-
|
|
535
|
+
useEffect(() => {
|
|
588
536
|
return () => {
|
|
589
|
-
|
|
537
|
+
clearBoxSelectHandled()
|
|
538
|
+
resetDrag()
|
|
590
539
|
}
|
|
591
|
-
}, [])
|
|
540
|
+
}, [resetDrag])
|
|
592
541
|
|
|
593
|
-
return
|
|
594
|
-
<group>
|
|
595
|
-
{/* Cursor indicator */}
|
|
596
|
-
<CursorSphere ref={cursorRef} tooltipContent={BOX_SELECT_TOOLTIP} />
|
|
597
|
-
|
|
598
|
-
{/* Selection rectangle fill */}
|
|
599
|
-
<mesh
|
|
600
|
-
layers={EDITOR_LAYER}
|
|
601
|
-
ref={rectFillRef}
|
|
602
|
-
renderOrder={1}
|
|
603
|
-
rotation={[-Math.PI / 2, 0, 0]}
|
|
604
|
-
visible={false}
|
|
605
|
-
>
|
|
606
|
-
<planeGeometry args={[1, 1]} />
|
|
607
|
-
<meshBasicMaterial
|
|
608
|
-
color={BOX_SELECT_ACCENT_COLOR}
|
|
609
|
-
depthTest={false}
|
|
610
|
-
depthWrite={false}
|
|
611
|
-
opacity={0.14}
|
|
612
|
-
side={DoubleSide}
|
|
613
|
-
transparent
|
|
614
|
-
/>
|
|
615
|
-
</mesh>
|
|
616
|
-
|
|
617
|
-
{/* Outline (LineLoop added as primitive — allocated once in ref) */}
|
|
618
|
-
<primitive object={outlineRef.current} />
|
|
619
|
-
</group>
|
|
620
|
-
)
|
|
542
|
+
return null
|
|
621
543
|
}
|