@pascal-app/editor 0.9.1 → 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 +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- 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 +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- 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 +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- 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 +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- 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 +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- 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-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- 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-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- 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 +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- 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/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- 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 +1 -1
- 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 +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- 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 +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- 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/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- 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 +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- 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 +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -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 +44 -3
- 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/tab-bar.tsx +42 -28
- 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 +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- 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 +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- 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-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- 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/planar-cursor-placement.test.ts +57 -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 +6 -6
- 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 +73 -8
- package/src/lib/scene.ts +28 -5
- 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 +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- 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/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 +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -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 +11 -3
- 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-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
bboxCornerAnchors,
|
|
5
|
+
collectAlignmentAnchors,
|
|
6
|
+
pauseSceneHistory,
|
|
7
|
+
pauseSpaceDetection,
|
|
8
|
+
resolveAlignment,
|
|
9
|
+
resumeSceneHistory,
|
|
10
|
+
resumeSpaceDetection,
|
|
11
|
+
useLiveNodeOverrides,
|
|
12
|
+
useLiveTransforms,
|
|
13
|
+
useScene,
|
|
14
|
+
} from '@pascal-app/core'
|
|
15
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
16
|
+
import { type Camera, Plane, type Raycaster, Vector2, Vector3 } from 'three'
|
|
17
|
+
import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
18
|
+
import { isHistoryShortcut } from '../../lib/history'
|
|
19
|
+
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
20
|
+
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
21
|
+
import useEditor, {
|
|
22
|
+
isAlignmentGuideActive,
|
|
23
|
+
isGridSnapActive,
|
|
24
|
+
isMagneticSnapActive,
|
|
25
|
+
} from '../../store/use-editor'
|
|
26
|
+
import useInteractionScope from '../../store/use-interaction-scope'
|
|
27
|
+
import { useFloorplanGroupDrag } from '../editor-2d/floorplan-group-move'
|
|
28
|
+
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
29
|
+
import { startGroupPickUp } from './group-actions'
|
|
30
|
+
import {
|
|
31
|
+
classifyParticipant,
|
|
32
|
+
collectParticipants,
|
|
33
|
+
computeGroupBox,
|
|
34
|
+
expandToComponent,
|
|
35
|
+
levelFrame,
|
|
36
|
+
participantExtents,
|
|
37
|
+
rotateGroupSnapshots,
|
|
38
|
+
translateGroupPatches,
|
|
39
|
+
type Vec2,
|
|
40
|
+
} from './group-transform-shared'
|
|
41
|
+
import { swallowNextClick } from './handles/use-handle-drag'
|
|
42
|
+
|
|
43
|
+
// 3D sibling of the 2D floorplan group move (and successor of the removed
|
|
44
|
+
// group-move gizmo cross): pressing any selected element's body in a
|
|
45
|
+
// multi-selection and dragging past the threshold slides the whole selection
|
|
46
|
+
// on the ground plane; a plain click (no drag) enters the group pick-up,
|
|
47
|
+
// parity with the single-item click-to-move. Shares the group participant
|
|
48
|
+
// snapshot, welded junctions, snapping entry points, live override previews,
|
|
49
|
+
// and single-undo commit with the 2D session.
|
|
50
|
+
|
|
51
|
+
// Figma-style alignment-snap threshold (meters) — same pull distance as the
|
|
52
|
+
// single-node registry move.
|
|
53
|
+
const ALIGNMENT_THRESHOLD_M = 0.08
|
|
54
|
+
const DRAG_THRESHOLD_PX = 4
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Arm a 3D group move from a pointer-down on `nodeId`. Returns false
|
|
58
|
+
* (attaching nothing) unless the node is a transformable member of a
|
|
59
|
+
* multi-selection.
|
|
60
|
+
*/
|
|
61
|
+
export function armGroupMove3d(args: {
|
|
62
|
+
nodeId: AnyNodeId
|
|
63
|
+
clientX: number
|
|
64
|
+
clientY: number
|
|
65
|
+
pointerId: number
|
|
66
|
+
nativeEvent: PointerEvent
|
|
67
|
+
camera: Camera
|
|
68
|
+
raycaster: Raycaster
|
|
69
|
+
domElement: HTMLCanvasElement
|
|
70
|
+
}): boolean {
|
|
71
|
+
const { nodeId, clientX, clientY, pointerId, camera, raycaster, domElement } = args
|
|
72
|
+
const { selectedIds, levelId } = useViewer.getState().selection
|
|
73
|
+
if (selectedIds.length < 2 || !selectedIds.includes(nodeId)) return false
|
|
74
|
+
const sceneNodes = useScene.getState().nodes
|
|
75
|
+
if (classifyParticipant(sceneNodes[nodeId], levelId, sceneNodes) === null) return false
|
|
76
|
+
|
|
77
|
+
suppressBoxSelectForPointer(args.nativeEvent)
|
|
78
|
+
|
|
79
|
+
const ndc = new Vector2()
|
|
80
|
+
const setNDC = (x: number, y: number) => {
|
|
81
|
+
const rect = domElement.getBoundingClientRect()
|
|
82
|
+
ndc.set(((x - rect.left) / rect.width) * 2 - 1, -((y - rect.top) / rect.height) * 2 + 1)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type Session = {
|
|
86
|
+
starts: ReturnType<typeof collectParticipants>['starts']
|
|
87
|
+
links: ReturnType<typeof collectParticipants>['links']
|
|
88
|
+
affectedIds: AnyNodeId[]
|
|
89
|
+
candidates: ReturnType<typeof collectAlignmentAnchors>
|
|
90
|
+
restAnchors: ReturnType<typeof bboxCornerAnchors>
|
|
91
|
+
restCenter: Vec2
|
|
92
|
+
plane: Plane
|
|
93
|
+
startLocal: Vector3
|
|
94
|
+
frameInv: ReturnType<typeof levelFrame>['inverse']
|
|
95
|
+
lastDelta: Vec2 | null
|
|
96
|
+
}
|
|
97
|
+
let session: Session | null = null
|
|
98
|
+
|
|
99
|
+
const engage = (): Session | null => {
|
|
100
|
+
const nodes = useScene.getState().nodes
|
|
101
|
+
const participantIds = selectedIds.filter(
|
|
102
|
+
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
103
|
+
)
|
|
104
|
+
// Move the full connected wall/fence component, mirroring the 2D session.
|
|
105
|
+
const fullIds = expandToComponent(participantIds, nodes, levelId)
|
|
106
|
+
const { starts, links } = collectParticipants(fullIds, nodes, levelId)
|
|
107
|
+
if (starts.length === 0) return null
|
|
108
|
+
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
109
|
+
|
|
110
|
+
// Horizontal drag plane at the group's base; placements live in the level
|
|
111
|
+
// frame, so world-space plane hits convert through it (a rotated building
|
|
112
|
+
// would otherwise drift off-axis from the cursor).
|
|
113
|
+
const restBox = computeGroupBox(fullIds)
|
|
114
|
+
if (!restBox) return null
|
|
115
|
+
const plane = new Plane(new Vector3(0, 1, 0), -restBox.min.y)
|
|
116
|
+
const { inverse: frameInv } = levelFrame(levelId)
|
|
117
|
+
|
|
118
|
+
setNDC(clientX, clientY)
|
|
119
|
+
raycaster.setFromCamera(ndc, camera)
|
|
120
|
+
const hit = new Vector3()
|
|
121
|
+
if (!raycaster.ray.intersectPlane(plane, hit)) return null
|
|
122
|
+
const startLocal = hit.clone().applyMatrix4(frameInv)
|
|
123
|
+
|
|
124
|
+
// Alignment candidates — anchors of everything OUTSIDE the moving set,
|
|
125
|
+
// gathered once at drag start. The group aligns as one rigid footprint:
|
|
126
|
+
// its bbox corners + center are the moving anchors.
|
|
127
|
+
const movingIdSet = new Set<string>(affectedIds)
|
|
128
|
+
const staticNodes: Record<string, AnyNode> = {}
|
|
129
|
+
for (const [nid, n] of Object.entries(nodes)) {
|
|
130
|
+
if (n && !movingIdSet.has(nid)) staticNodes[nid] = n
|
|
131
|
+
}
|
|
132
|
+
const candidates = collectAlignmentAnchors(staticNodes, '', levelId)
|
|
133
|
+
const boxMin = restBox.min.clone().applyMatrix4(frameInv)
|
|
134
|
+
const boxMax = restBox.max.clone().applyMatrix4(frameInv)
|
|
135
|
+
const restAnchors = bboxCornerAnchors(
|
|
136
|
+
'group-move',
|
|
137
|
+
Math.min(boxMin.x, boxMax.x),
|
|
138
|
+
Math.min(boxMin.z, boxMax.z),
|
|
139
|
+
Math.max(boxMin.x, boxMax.x),
|
|
140
|
+
Math.max(boxMin.z, boxMax.z),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for (const id of affectedIds) {
|
|
144
|
+
useLiveTransforms.getState().clear(id)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
domElement.style.cursor = 'grabbing'
|
|
148
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
149
|
+
swallowNextClick()
|
|
150
|
+
useViewer.getState().setInputDragging(true)
|
|
151
|
+
pauseSceneHistory(useScene)
|
|
152
|
+
useInteractionScope.getState().begin({
|
|
153
|
+
kind: 'handle-drag',
|
|
154
|
+
nodeId,
|
|
155
|
+
handle: GROUP_MOVE_DRAG_LABEL,
|
|
156
|
+
})
|
|
157
|
+
// Rotation pivot for mid-drag R/T — the participant DATA extents' center.
|
|
158
|
+
const ext = participantExtents(starts)
|
|
159
|
+
const restCenter: Vec2 = ext ? [(ext.minX + ext.maxX) / 2, (ext.minZ + ext.maxZ) / 2] : [0, 0]
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
starts,
|
|
163
|
+
links,
|
|
164
|
+
affectedIds,
|
|
165
|
+
candidates,
|
|
166
|
+
restAnchors,
|
|
167
|
+
restCenter,
|
|
168
|
+
plane,
|
|
169
|
+
startLocal,
|
|
170
|
+
frameInv,
|
|
171
|
+
lastDelta: null,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const applyMove = (e: PointerEvent, s: Session) => {
|
|
176
|
+
setNDC(e.clientX, e.clientY)
|
|
177
|
+
raycaster.setFromCamera(ndc, camera)
|
|
178
|
+
const moveHit = new Vector3()
|
|
179
|
+
if (!raycaster.ray.intersectPlane(s.plane, moveHit)) return
|
|
180
|
+
const moveLocal = moveHit.applyMatrix4(s.frameInv)
|
|
181
|
+
|
|
182
|
+
// Snap the slide DELTA to the active grid step so the selection's internal
|
|
183
|
+
// layout stays intact. Mode-driven: the `handle-drag` scope resolves to
|
|
184
|
+
// the item snap context, so Shift cycles the mode mid-drag.
|
|
185
|
+
const step = useEditor.getState().gridSnapStep
|
|
186
|
+
const snap = isGridSnapActive() && step > 0
|
|
187
|
+
let dx = snap
|
|
188
|
+
? Math.round((moveLocal.x - s.startLocal.x) / step) * step
|
|
189
|
+
: moveLocal.x - s.startLocal.x
|
|
190
|
+
let dz = snap
|
|
191
|
+
? Math.round((moveLocal.z - s.startLocal.z) / step) * step
|
|
192
|
+
: moveLocal.z - s.startLocal.z
|
|
193
|
+
|
|
194
|
+
// Figma-style alignment layered on top: guides display in every mode
|
|
195
|
+
// except Off; the magnetic pull applies only in 'lines' mode.
|
|
196
|
+
if (isAlignmentGuideActive() && s.candidates.length > 0 && s.restAnchors.length > 0) {
|
|
197
|
+
const result = resolveAlignment({
|
|
198
|
+
moving: s.restAnchors.map((a) => ({ ...a, x: a.x + dx, z: a.z + dz })),
|
|
199
|
+
candidates: s.candidates,
|
|
200
|
+
threshold: ALIGNMENT_THRESHOLD_M,
|
|
201
|
+
})
|
|
202
|
+
if (result.snap && isMagneticSnapActive()) {
|
|
203
|
+
dx += result.snap.dx
|
|
204
|
+
dz += result.snap.dz
|
|
205
|
+
}
|
|
206
|
+
useAlignmentGuides.getState().set(result.guides)
|
|
207
|
+
} else {
|
|
208
|
+
useAlignmentGuides.getState().clear()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
applyDelta(s, dx, dz)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const applyDelta = (s: Session, dx: number, dz: number) => {
|
|
215
|
+
// Ticker on each delta change — parity with the single-node move SFX.
|
|
216
|
+
if (!s.lastDelta || s.lastDelta[0] !== dx || s.lastDelta[1] !== dz) {
|
|
217
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
218
|
+
s.lastDelta = [dx, dz]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const entries = translateGroupPatches(s.starts, s.links, dx, dz)
|
|
222
|
+
const patchById = new Map(entries)
|
|
223
|
+
const liveTransforms = useLiveTransforms.getState()
|
|
224
|
+
for (const start of s.starts) {
|
|
225
|
+
if (start.kind === 'scalar') {
|
|
226
|
+
const patch = patchById.get(start.id)
|
|
227
|
+
if (patch) {
|
|
228
|
+
liveTransforms.set(start.id, {
|
|
229
|
+
position: patch.position as [number, number, number],
|
|
230
|
+
rotation: start.rotation,
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
useScene.getState().markDirty(start.id)
|
|
235
|
+
}
|
|
236
|
+
for (const l of s.links) {
|
|
237
|
+
useScene.getState().markDirty(l.id)
|
|
238
|
+
}
|
|
239
|
+
useLiveNodeOverrides.getState().setMany(entries)
|
|
240
|
+
// The 2D dashed group bbox rides the same delta in split view.
|
|
241
|
+
useFloorplanGroupDrag.getState().set([dx, dz])
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Mid-drag R/T: rotate the SNAPSHOTS around the rest pivot and re-apply the
|
|
245
|
+
// current delta — the carried group turns exactly like the idle keyboard
|
|
246
|
+
// rotate, and the commit stays a single updateNodes.
|
|
247
|
+
const rotateSession = (s: Session, direction: 1 | -1) => {
|
|
248
|
+
const rotated = rotateGroupSnapshots(
|
|
249
|
+
s.starts,
|
|
250
|
+
s.links,
|
|
251
|
+
{ x: s.restCenter[0], z: s.restCenter[1] },
|
|
252
|
+
-direction * (Math.PI / 4),
|
|
253
|
+
)
|
|
254
|
+
s.starts = rotated.starts
|
|
255
|
+
s.links = rotated.links
|
|
256
|
+
const ext = participantExtents(rotated.starts)
|
|
257
|
+
if (ext) {
|
|
258
|
+
s.restAnchors = bboxCornerAnchors('group-move', ext.minX, ext.minZ, ext.maxX, ext.maxZ)
|
|
259
|
+
}
|
|
260
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
261
|
+
applyDelta(s, s.lastDelta?.[0] ?? 0, s.lastDelta?.[1] ?? 0)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const clearLivePreviews = (s: Session) => {
|
|
265
|
+
const overrides = useLiveNodeOverrides.getState()
|
|
266
|
+
const liveTransforms = useLiveTransforms.getState()
|
|
267
|
+
for (const id of s.affectedIds) {
|
|
268
|
+
overrides.clear(id)
|
|
269
|
+
liveTransforms.clear(id)
|
|
270
|
+
useScene.getState().markDirty(id)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const removeListeners = () => {
|
|
275
|
+
window.removeEventListener('pointermove', onMove)
|
|
276
|
+
window.removeEventListener('pointerup', onUp, true)
|
|
277
|
+
window.removeEventListener('pointercancel', onPointerCancel)
|
|
278
|
+
window.removeEventListener('keydown', onKeyDown, true)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// History resume is NOT here — it pairs one-to-one with the
|
|
282
|
+
// `pauseSceneHistory` in `engage()`, on the commit and cancel paths.
|
|
283
|
+
const teardown = () => {
|
|
284
|
+
removeListeners()
|
|
285
|
+
if (domElement.style.cursor === 'grabbing') domElement.style.cursor = ''
|
|
286
|
+
useAlignmentGuides.getState().clear()
|
|
287
|
+
// Deferred so the canvas pointerup later in this same dispatch still sees
|
|
288
|
+
// the drag as active (see onUp).
|
|
289
|
+
setTimeout(() => useViewer.getState().setInputDragging(false), 0)
|
|
290
|
+
useInteractionScope
|
|
291
|
+
.getState()
|
|
292
|
+
.endIf((sc) => sc.kind === 'handle-drag' && sc.handle === GROUP_MOVE_DRAG_LABEL)
|
|
293
|
+
useFloorplanGroupDrag.getState().set(null)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const onMove = (e: PointerEvent) => {
|
|
297
|
+
if (e.pointerId !== pointerId) return
|
|
298
|
+
if (!session) {
|
|
299
|
+
if (Math.hypot(e.clientX - clientX, e.clientY - clientY) < DRAG_THRESHOLD_PX) return
|
|
300
|
+
session = engage()
|
|
301
|
+
if (!session) {
|
|
302
|
+
removeListeners()
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
applyMove(e, session)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Registered in CAPTURE phase so this runs before the canvas handlers:
|
|
310
|
+
// `use-node-events` synthesizes a selection click on EVERY pointerup,
|
|
311
|
+
// suppressed only while `inputDragging` is set — so the gesture must keep
|
|
312
|
+
// it raised through this event's dispatch (released on a 0ms timer) or the
|
|
313
|
+
// release re-routes selection to whatever sits under the cursor. Plain
|
|
314
|
+
// stopPropagation would ALSO kill the window bubble listeners that clear
|
|
315
|
+
// the box-select pointer suppression, leaving the marquee dead.
|
|
316
|
+
const onUp = (e: PointerEvent) => {
|
|
317
|
+
if (e.pointerId !== pointerId) return
|
|
318
|
+
if (!session) {
|
|
319
|
+
// Plain click — enter the group pick-up, parity with the single-item
|
|
320
|
+
// click-to-move. Eat the click so the selection manager's click
|
|
321
|
+
// handling doesn't collapse the multi-selection underneath it.
|
|
322
|
+
removeListeners()
|
|
323
|
+
swallowNextClick()
|
|
324
|
+
useViewer.getState().setInputDragging(true)
|
|
325
|
+
setTimeout(() => useViewer.getState().setInputDragging(false), 0)
|
|
326
|
+
startGroupPickUp()
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
swallowNextClick()
|
|
330
|
+
sfxEmitter.emit('sfx:item-place')
|
|
331
|
+
const overrides = useLiveNodeOverrides.getState()
|
|
332
|
+
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = []
|
|
333
|
+
for (const id of session.affectedIds) {
|
|
334
|
+
const patch = overrides.get(id)
|
|
335
|
+
if (patch) updates.push({ id, data: patch as Partial<AnyNode> })
|
|
336
|
+
}
|
|
337
|
+
// Resume before the commit so the single batched `updateNodes` is the one
|
|
338
|
+
// tracked set — collapsing the whole group move into one undo step.
|
|
339
|
+
// Group transforms move existing structure rigidly — the wall-driven room
|
|
340
|
+
// auto-detection must not re-create floors/ceilings for the walls' new
|
|
341
|
+
// positions (that belongs to wall building/editing). Paused around the
|
|
342
|
+
// commit; the sync rolls its baseline forward for paused changes.
|
|
343
|
+
pauseSpaceDetection()
|
|
344
|
+
resumeSceneHistory(useScene)
|
|
345
|
+
if (updates.length > 0) useScene.getState().updateNodes(updates)
|
|
346
|
+
resumeSpaceDetection()
|
|
347
|
+
clearLivePreviews(session)
|
|
348
|
+
session = null
|
|
349
|
+
teardown()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const cancel = () => {
|
|
353
|
+
if (session) {
|
|
354
|
+
clearLivePreviews(session)
|
|
355
|
+
resumeSceneHistory(useScene)
|
|
356
|
+
session = null
|
|
357
|
+
}
|
|
358
|
+
teardown()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const onPointerCancel = (e: PointerEvent) => {
|
|
362
|
+
if (e.pointerId !== pointerId) return
|
|
363
|
+
cancel()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Capture phase so the drag's Escape / R / T win over the global
|
|
367
|
+
// `use-keyboard` arms, which would otherwise act on stale store state
|
|
368
|
+
// (or clear the multi-selection) mid-session.
|
|
369
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
370
|
+
const key = e.key.toLowerCase()
|
|
371
|
+
if ((key === 'r' || key === 't') && !e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey) {
|
|
372
|
+
if (!session) return
|
|
373
|
+
e.preventDefault()
|
|
374
|
+
e.stopPropagation()
|
|
375
|
+
rotateSession(session, key === 'r' ? 1 : -1)
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
379
|
+
// Deleting mid-move: revert the session first, then let the global
|
|
380
|
+
// Delete arm remove the selection — no dangling carry.
|
|
381
|
+
cancel()
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
// ⌘Z mid-move cancels like Escape — never a history jump under a live pointer.
|
|
385
|
+
if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
|
|
386
|
+
e.preventDefault()
|
|
387
|
+
e.stopPropagation()
|
|
388
|
+
swallowNextClick()
|
|
389
|
+
cancel()
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
window.addEventListener('pointermove', onMove)
|
|
393
|
+
window.addEventListener('pointerup', onUp, true)
|
|
394
|
+
window.addEventListener('pointercancel', onPointerCancel)
|
|
395
|
+
window.addEventListener('keydown', onKeyDown, true)
|
|
396
|
+
return true
|
|
397
|
+
}
|