@pascal-app/editor 0.9.2 → 1.0.0-beta.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 +9 -7
- package/src/components/editor/bake-exporter.tsx +3 -1
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +307 -212
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
- package/src/components/editor/first-person/build-collider-world.ts +20 -0
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +295 -132
- package/src/components/editor/floating-action-menu.tsx +59 -65
- package/src/components/editor/floorplan-background-selection.test.ts +16 -2
- package/src/components/editor/floorplan-background-selection.ts +4 -18
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +931 -861
- package/src/components/editor/group-actions.ts +155 -15
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-rotate-handle.tsx +1 -1
- package/src/components/editor/group-selection-box-3d.tsx +1 -1
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +17 -7
- package/src/components/editor/index.tsx +42 -9
- package/src/components/editor/measurement-pill.tsx +6 -1
- package/src/components/editor/node-arrow-handles.tsx +51 -15
- package/src/components/editor/quick-measurement-card.tsx +9 -3
- package/src/components/editor/quick-measurement-hud.tsx +7 -1
- package/src/components/editor/selection-manager.tsx +3 -1
- package/src/components/editor/site-edge-labels.tsx +24 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
- package/src/components/editor/thumbnail-generator.tsx +66 -294
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
- package/src/components/editor/wall-measurement-label.tsx +8 -16
- package/src/components/editor/wall-move-side-handles.tsx +193 -18
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
- package/src/components/tools/fence/fence-drafting.ts +27 -0
- package/src/components/tools/item/use-draft-node.ts +131 -72
- package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
- package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
- package/src/components/tools/roof/roof-tool.tsx +16 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/site/site-boundary-editor.tsx +13 -6
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-tool.tsx +98 -47
- package/src/components/tools/tool-manager.tsx +67 -9
- package/src/components/tools/wall/wall-drafting.test.ts +118 -1
- package/src/components/tools/wall/wall-drafting.ts +128 -9
- package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
- package/src/components/tools/zone/zone-tool.tsx +16 -0
- package/src/components/ui/action-menu/control-modes.tsx +4 -1
- package/src/components/ui/action-menu/measurement-control.tsx +107 -28
- package/src/components/ui/command-palette/editor-commands.tsx +14 -1
- package/src/components/ui/controls/material-paint-panel.tsx +7 -1
- package/src/components/ui/controls/material-picker.tsx +86 -7
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +73 -19
- package/src/components/ui/helpers/helper-manager.tsx +46 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/panel-manager.tsx +14 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -2
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +20 -655
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +53 -30
- package/src/index.tsx +137 -4
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +8 -1
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/direct-manipulation.test.ts +83 -0
- package/src/lib/direct-manipulation.ts +9 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +265 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +745 -131
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +2 -3
- package/src/lib/fresh-planar-placement.test.ts +44 -1
- package/src/lib/fresh-planar-placement.ts +21 -4
- package/src/lib/glb-export.test.ts +116 -1
- package/src/lib/glb-export.ts +204 -7
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +22 -0
- package/src/lib/interaction/overlay-policy.test.ts +2 -1
- package/src/lib/interaction/scope.ts +40 -8
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/measurements.test.ts +5 -0
- package/src/lib/measurements.ts +12 -1
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/scene-clipboard.test.ts +130 -1
- package/src/lib/scene-clipboard.ts +237 -13
- package/src/lib/selection-routing.test.ts +16 -1
- package/src/lib/selection-routing.ts +5 -0
- package/src/lib/sfx-bus.ts +33 -2
- package/src/lib/sfx-player.test.ts +148 -6
- package/src/lib/sfx-player.ts +156 -8
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/snapping-mode.test.ts +29 -2
- package/src/lib/snapping-mode.ts +15 -3
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +57 -85
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +184 -15
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-fence-curve-draft.ts +38 -8
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +54 -4
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +23 -3
- package/src/store/use-interaction-scope.ts +8 -0
- package/src/store/use-measurement-draft.test.ts +29 -0
- package/src/store/use-measurement-draft.ts +33 -6
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-stair-build-preview.ts +12 -2
- package/src/lib/floorplan/selection-tool.ts +0 -271
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AnyNodeId,
|
|
5
5
|
type CameraControlEvent,
|
|
6
6
|
type CameraControlFitSceneEvent,
|
|
7
|
+
type CameraPose,
|
|
7
8
|
emitter,
|
|
8
9
|
sceneRegistry,
|
|
9
10
|
useScene,
|
|
@@ -20,13 +21,25 @@ import {
|
|
|
20
21
|
Spherical,
|
|
21
22
|
Vector3,
|
|
22
23
|
} from 'three'
|
|
24
|
+
import {
|
|
25
|
+
type CameraPoseApplicationPlan,
|
|
26
|
+
normalizeCameraPose,
|
|
27
|
+
planCameraPoseApplication,
|
|
28
|
+
publishInitialCameraPose,
|
|
29
|
+
releaseCameraPoseEventSuppression,
|
|
30
|
+
stepCameraPoseInterpolation,
|
|
31
|
+
withCameraPoseDistance,
|
|
32
|
+
} from '../../lib/camera-pose'
|
|
23
33
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
34
|
+
import { editorOwnsOneFingerDrag } from '../../lib/touch-gesture-priority'
|
|
35
|
+
import { publishCameraPose } from '../../store/camera-pose-store'
|
|
24
36
|
import useEditor from '../../store/use-editor'
|
|
25
37
|
import {
|
|
26
38
|
useActiveHandleDrag,
|
|
27
39
|
useEndpointReshape,
|
|
28
40
|
useMovingNode,
|
|
29
41
|
} from '../../store/use-interaction-scope'
|
|
42
|
+
import { createCameraDraggingLifecycle } from './camera-dragging-lifecycle'
|
|
30
43
|
|
|
31
44
|
const currentTarget = new Vector3()
|
|
32
45
|
const tempBox = new Box3()
|
|
@@ -35,14 +48,11 @@ const tempDelta = new Vector3()
|
|
|
35
48
|
const tempPosition = new Vector3()
|
|
36
49
|
const tempSize = new Vector3()
|
|
37
50
|
const tempTarget = new Vector3()
|
|
38
|
-
const
|
|
39
|
-
const
|
|
51
|
+
const transitionFreezePosition = new Vector3()
|
|
52
|
+
const transitionFreezeTarget = new Vector3()
|
|
40
53
|
const keyboardPanSpherical = new Spherical()
|
|
41
54
|
const DEFAULT_MAX_POLAR_ANGLE = Math.PI / 2 - 0.1
|
|
42
55
|
const DEBUG_MAX_POLAR_ANGLE = Math.PI - 0.05
|
|
43
|
-
const NAVIGATION_SYNC_POSITION_EPSILON = 0.001
|
|
44
|
-
const NAVIGATION_SYNC_AZIMUTH_EPSILON = 0.0005
|
|
45
|
-
const NAVIGATION_SYNC_VIEW_WIDTH_EPSILON = 0.001
|
|
46
56
|
const KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND = 0.65
|
|
47
57
|
const KEYBOARD_PAN_MIN_SPEED = 2
|
|
48
58
|
const KEYBOARD_PAN_MAX_SPEED = 55
|
|
@@ -52,14 +62,6 @@ type CameraPoseSnapshot = {
|
|
|
52
62
|
position: [number, number, number]
|
|
53
63
|
target: [number, number, number]
|
|
54
64
|
}
|
|
55
|
-
type NavigationCameraPoseSnapshot = {
|
|
56
|
-
target: [number, number, number]
|
|
57
|
-
azimuth: number
|
|
58
|
-
viewWidth: number
|
|
59
|
-
}
|
|
60
|
-
type PendingNavigationCameraPoseSnapshot = NavigationCameraPoseSnapshot & {
|
|
61
|
-
publishOnComplete: boolean
|
|
62
|
-
}
|
|
63
65
|
type CameraViewWidthUpdate =
|
|
64
66
|
| { type: 'distance'; distance: number; viewWidth: number }
|
|
65
67
|
| { type: 'zoom'; viewWidth: number; zoom: number }
|
|
@@ -97,6 +99,21 @@ function restoreCameraPose(control: CameraControlsImpl, pose: CameraPoseSnapshot
|
|
|
97
99
|
)
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
function freezeCameraControlTransition(control: CameraControlsImpl) {
|
|
103
|
+
// `stop()` snaps to the transition endpoint, so replace it with the current pose instead.
|
|
104
|
+
control.getPosition(transitionFreezePosition, false)
|
|
105
|
+
control.getTarget(transitionFreezeTarget, false)
|
|
106
|
+
void control.setLookAt(
|
|
107
|
+
transitionFreezePosition.x,
|
|
108
|
+
transitionFreezePosition.y,
|
|
109
|
+
transitionFreezePosition.z,
|
|
110
|
+
transitionFreezeTarget.x,
|
|
111
|
+
transitionFreezeTarget.y,
|
|
112
|
+
transitionFreezeTarget.z,
|
|
113
|
+
false,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
function isEditableKeyboardTarget(target: EventTarget | null) {
|
|
101
118
|
return (
|
|
102
119
|
target instanceof HTMLInputElement ||
|
|
@@ -141,6 +158,10 @@ function isKeyboardPanKey(code: string): boolean {
|
|
|
141
158
|
return code === 'KeyW' || code === 'KeyA' || code === 'KeyS' || code === 'KeyD'
|
|
142
159
|
}
|
|
143
160
|
|
|
161
|
+
function hasKeyboardPanInput(state: KeyboardPanState): boolean {
|
|
162
|
+
return state.forward || state.backward || state.left || state.right
|
|
163
|
+
}
|
|
164
|
+
|
|
144
165
|
type CameraViewportSize = {
|
|
145
166
|
width: number
|
|
146
167
|
height: number
|
|
@@ -171,14 +192,6 @@ function getCameraViewWidth(camera: Camera, distance: number, size: CameraViewpo
|
|
|
171
192
|
return Math.max(0.001, distance)
|
|
172
193
|
}
|
|
173
194
|
|
|
174
|
-
function getAngleDeltaRadians(a: number, b: number) {
|
|
175
|
-
return Math.atan2(Math.sin(a - b), Math.cos(a - b))
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function nearestEquivalentRadians(angle: number, reference: number) {
|
|
179
|
-
return reference + getAngleDeltaRadians(angle, reference)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
195
|
function clampFinite(value: number, min: number, max: number) {
|
|
183
196
|
const resolvedMin = Number.isFinite(min) ? min : Number.NEGATIVE_INFINITY
|
|
184
197
|
const resolvedMax = Number.isFinite(max) ? max : Number.POSITIVE_INFINITY
|
|
@@ -203,21 +216,6 @@ function clampCameraControlZoom(control: CameraControlsImpl, zoom: number) {
|
|
|
203
216
|
)
|
|
204
217
|
}
|
|
205
218
|
|
|
206
|
-
function isCameraAtNavigationPose(
|
|
207
|
-
pose: NavigationCameraPoseSnapshot,
|
|
208
|
-
target: Vector3,
|
|
209
|
-
azimuth: number,
|
|
210
|
-
viewWidth: number,
|
|
211
|
-
) {
|
|
212
|
-
return (
|
|
213
|
-
Math.abs(pose.target[0] - target.x) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
214
|
-
Math.abs(pose.target[1] - target.y) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
215
|
-
Math.abs(pose.target[2] - target.z) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
216
|
-
Math.abs(getAngleDeltaRadians(pose.azimuth, azimuth)) < NAVIGATION_SYNC_AZIMUTH_EPSILON &&
|
|
217
|
-
Math.abs(pose.viewWidth - viewWidth) < NAVIGATION_SYNC_VIEW_WIDTH_EPSILON
|
|
218
|
-
)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
219
|
function getCameraDistanceForViewWidth(
|
|
222
220
|
camera: Camera,
|
|
223
221
|
viewWidth: number,
|
|
@@ -272,14 +270,18 @@ function resolveCameraViewWidthUpdate(
|
|
|
272
270
|
return { type: 'none', viewWidth }
|
|
273
271
|
}
|
|
274
272
|
|
|
275
|
-
function applyCameraViewWidth(
|
|
273
|
+
function applyCameraViewWidth(
|
|
274
|
+
control: CameraControlsImpl,
|
|
275
|
+
update: CameraViewWidthUpdate,
|
|
276
|
+
enableTransition = true,
|
|
277
|
+
) {
|
|
276
278
|
if (update.type === 'distance') {
|
|
277
|
-
control.dollyTo(update.distance,
|
|
279
|
+
control.dollyTo(update.distance, enableTransition)
|
|
278
280
|
return
|
|
279
281
|
}
|
|
280
282
|
|
|
281
283
|
if (update.type === 'zoom') {
|
|
282
|
-
control.zoomTo(update.zoom,
|
|
284
|
+
control.zoomTo(update.zoom, enableTransition)
|
|
283
285
|
}
|
|
284
286
|
}
|
|
285
287
|
|
|
@@ -347,6 +349,13 @@ function useFirstPersonCameraPoseRestore(
|
|
|
347
349
|
|
|
348
350
|
export const CustomCameraControls = () => {
|
|
349
351
|
const controls = useRef<CameraControlsImpl | null>(null)
|
|
352
|
+
const pendingAppliedPose = useRef<CameraPoseApplicationPlan | null>(null)
|
|
353
|
+
const activePoseInterpolation = useRef<{
|
|
354
|
+
camera: Camera
|
|
355
|
+
control: CameraControlsImpl
|
|
356
|
+
plan: CameraPoseApplicationPlan
|
|
357
|
+
} | null>(null)
|
|
358
|
+
const suppressPoseEvents = useRef(false)
|
|
350
359
|
const keyboardPanKeys = useRef<KeyboardPanState>({
|
|
351
360
|
forward: false,
|
|
352
361
|
backward: false,
|
|
@@ -356,7 +365,6 @@ export const CustomCameraControls = () => {
|
|
|
356
365
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
357
366
|
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
358
367
|
const allowUndergroundCamera = useEditor((s) => s.allowUndergroundCamera)
|
|
359
|
-
const isFloorplanOpen = useEditor((s) => s.isFloorplanOpen)
|
|
360
368
|
const selection = useViewer((s) => s.selection)
|
|
361
369
|
const cameraMode = useViewer((state) => state.cameraMode)
|
|
362
370
|
const isRestoringFirstPersonPose = useFirstPersonCameraPoseRestore(
|
|
@@ -366,24 +374,21 @@ export const CustomCameraControls = () => {
|
|
|
366
374
|
)
|
|
367
375
|
const currentLevelId = selection.levelId
|
|
368
376
|
const firstLoad = useRef(true)
|
|
369
|
-
const lastPublishedNavigationSync = useRef<NavigationCameraPoseSnapshot | null>(null)
|
|
370
|
-
const pendingFloorplanNavigationPose = useRef<PendingNavigationCameraPoseSnapshot | null>(null)
|
|
371
|
-
const lastApplied2dNavigationRevision = useRef(0)
|
|
372
|
-
const savedSmoothTimeRef = useRef<number | null>(null)
|
|
373
377
|
const maxPolarAngle =
|
|
374
378
|
!isPreviewMode && allowUndergroundCamera ? DEBUG_MAX_POLAR_ANGLE : DEFAULT_MAX_POLAR_ANGLE
|
|
375
|
-
const clearPendingFloorplanNavigationPose = useCallback(() => {
|
|
376
|
-
pendingFloorplanNavigationPose.current = null
|
|
377
|
-
if (savedSmoothTimeRef.current !== null && controls.current) {
|
|
378
|
-
controls.current.smoothTime = savedSmoothTimeRef.current
|
|
379
|
-
savedSmoothTimeRef.current = null
|
|
380
|
-
}
|
|
381
|
-
}, [])
|
|
382
379
|
|
|
383
380
|
const camera = useThree((state) => state.camera)
|
|
384
381
|
const gl = useThree((state) => state.gl)
|
|
385
382
|
const raycaster = useThree((state) => state.raycaster)
|
|
386
383
|
const viewportSize = useThree((state) => state.size)
|
|
384
|
+
const cameraDraggingLifecycle = useMemo(
|
|
385
|
+
() =>
|
|
386
|
+
createCameraDraggingLifecycle({
|
|
387
|
+
setDragging: (dragging) => useViewer.getState().setCameraDragging(dragging),
|
|
388
|
+
}),
|
|
389
|
+
[],
|
|
390
|
+
)
|
|
391
|
+
useEffect(() => () => cameraDraggingLifecycle.end(), [cameraDraggingLifecycle])
|
|
387
392
|
useEffect(() => {
|
|
388
393
|
camera.layers.enable(EDITOR_LAYER)
|
|
389
394
|
camera.layers.enable(GRID_LAYER)
|
|
@@ -391,6 +396,122 @@ export const CustomCameraControls = () => {
|
|
|
391
396
|
raycaster.layers.enable(ZONE_LAYER)
|
|
392
397
|
}, [camera, raycaster])
|
|
393
398
|
|
|
399
|
+
const freezeActivePoseInterpolation = useCallback(() => {
|
|
400
|
+
const active = activePoseInterpolation.current
|
|
401
|
+
if (!active) return
|
|
402
|
+
|
|
403
|
+
activePoseInterpolation.current = null
|
|
404
|
+
freezeCameraControlTransition(active.control)
|
|
405
|
+
}, [])
|
|
406
|
+
|
|
407
|
+
const cancelPoseApplication = useCallback(() => {
|
|
408
|
+
pendingAppliedPose.current = null
|
|
409
|
+
try {
|
|
410
|
+
freezeActivePoseInterpolation()
|
|
411
|
+
} finally {
|
|
412
|
+
suppressPoseEvents.current = false
|
|
413
|
+
}
|
|
414
|
+
}, [freezeActivePoseInterpolation])
|
|
415
|
+
|
|
416
|
+
const beginLocalCameraInteraction = useCallback(
|
|
417
|
+
({ dragging = true }: { dragging?: boolean } = {}) => {
|
|
418
|
+
cancelPoseApplication()
|
|
419
|
+
if (dragging) cameraDraggingLifecycle.begin()
|
|
420
|
+
emitter.emit('camera-controls:interaction-start', undefined)
|
|
421
|
+
},
|
|
422
|
+
[cameraDraggingLifecycle, cancelPoseApplication],
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
const applyPendingPose = useCallback(() => {
|
|
426
|
+
if (isFirstPersonMode) {
|
|
427
|
+
cancelPoseApplication()
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const plan = pendingAppliedPose.current
|
|
432
|
+
const control = controls.current
|
|
433
|
+
|
|
434
|
+
const active = activePoseInterpolation.current
|
|
435
|
+
if (active && (active.camera !== camera || active.control !== control)) {
|
|
436
|
+
try {
|
|
437
|
+
freezeActivePoseInterpolation()
|
|
438
|
+
} finally {
|
|
439
|
+
if (!plan) suppressPoseEvents.current = false
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!(plan && control)) return
|
|
444
|
+
|
|
445
|
+
const cameraMatchesProjection =
|
|
446
|
+
(plan.pose.projection === 'perspective' && isPerspectiveCamera(camera)) ||
|
|
447
|
+
(plan.pose.projection === 'orthographic' && isOrthographicCamera(camera))
|
|
448
|
+
if (!cameraMatchesProjection) return
|
|
449
|
+
|
|
450
|
+
pendingAppliedPose.current = null
|
|
451
|
+
if (plan.perspectiveFov !== null && isPerspectiveCamera(camera)) {
|
|
452
|
+
camera.fov = plan.perspectiveFov
|
|
453
|
+
camera.updateProjectionMatrix()
|
|
454
|
+
}
|
|
455
|
+
let appliedPlan = plan
|
|
456
|
+
if (plan.pose.viewWidth !== undefined) {
|
|
457
|
+
const viewWidthUpdate = resolveCameraViewWidthUpdate(
|
|
458
|
+
control,
|
|
459
|
+
camera,
|
|
460
|
+
plan.pose.viewWidth,
|
|
461
|
+
viewportSize,
|
|
462
|
+
)
|
|
463
|
+
if (viewWidthUpdate.type === 'distance') {
|
|
464
|
+
appliedPlan = {
|
|
465
|
+
...plan,
|
|
466
|
+
pose: withCameraPoseDistance(plan.pose, viewWidthUpdate.distance),
|
|
467
|
+
}
|
|
468
|
+
} else if (viewWidthUpdate.type === 'zoom') {
|
|
469
|
+
applyCameraViewWidth(control, viewWidthUpdate, false)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
activePoseInterpolation.current = { camera, control, plan: appliedPlan }
|
|
474
|
+
}, [
|
|
475
|
+
camera,
|
|
476
|
+
cancelPoseApplication,
|
|
477
|
+
freezeActivePoseInterpolation,
|
|
478
|
+
isFirstPersonMode,
|
|
479
|
+
viewportSize,
|
|
480
|
+
])
|
|
481
|
+
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
applyPendingPose()
|
|
484
|
+
}, [applyPendingPose])
|
|
485
|
+
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
const handleAppliedPose = (pose: CameraPose) => {
|
|
488
|
+
if (isFirstPersonMode) return
|
|
489
|
+
|
|
490
|
+
const plan = planCameraPoseApplication(pose)
|
|
491
|
+
if (!plan) return
|
|
492
|
+
|
|
493
|
+
pendingAppliedPose.current = plan
|
|
494
|
+
suppressPoseEvents.current = true
|
|
495
|
+
|
|
496
|
+
if (useViewer.getState().cameraMode !== plan.pose.projection) {
|
|
497
|
+
freezeActivePoseInterpolation()
|
|
498
|
+
useViewer.getState().setCameraMode(plan.pose.projection)
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
applyPendingPose()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
emitter.on('camera-controls:apply-pose', handleAppliedPose)
|
|
506
|
+
emitter.on('camera-controls:cancel-pose', cancelPoseApplication)
|
|
507
|
+
return () => {
|
|
508
|
+
emitter.off('camera-controls:apply-pose', handleAppliedPose)
|
|
509
|
+
emitter.off('camera-controls:cancel-pose', cancelPoseApplication)
|
|
510
|
+
}
|
|
511
|
+
}, [applyPendingPose, cancelPoseApplication, freezeActivePoseInterpolation, isFirstPersonMode])
|
|
512
|
+
|
|
513
|
+
useEffect(() => cancelPoseApplication, [cancelPoseApplication])
|
|
514
|
+
|
|
394
515
|
useEffect(() => {
|
|
395
516
|
// Dev-only: deterministic camera poses for screenshot/automation tooling.
|
|
396
517
|
// A getter, not a snapshot — drei recreates the impl when the default
|
|
@@ -417,19 +538,11 @@ export const CustomCameraControls = () => {
|
|
|
417
538
|
if (!controls.current) return
|
|
418
539
|
if (firstLoad.current) {
|
|
419
540
|
firstLoad.current = false
|
|
420
|
-
clearPendingFloorplanNavigationPose()
|
|
421
541
|
controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
|
|
422
542
|
}
|
|
423
543
|
controls.current.getTarget(currentTarget)
|
|
424
|
-
clearPendingFloorplanNavigationPose()
|
|
425
544
|
controls.current.moveTo(currentTarget.x, targetY, currentTarget.z, true)
|
|
426
|
-
}, [
|
|
427
|
-
clearPendingFloorplanNavigationPose,
|
|
428
|
-
currentLevelId,
|
|
429
|
-
isPreviewMode,
|
|
430
|
-
isFirstPersonMode,
|
|
431
|
-
isRestoringFirstPersonPose,
|
|
432
|
-
])
|
|
545
|
+
}, [currentLevelId, isPreviewMode, isFirstPersonMode, isRestoringFirstPersonPose])
|
|
433
546
|
|
|
434
547
|
useEffect(() => {
|
|
435
548
|
if (isFirstPersonMode || !controls.current) return
|
|
@@ -457,7 +570,6 @@ export const CustomCameraControls = () => {
|
|
|
457
570
|
controls.current.getTarget(tempTarget)
|
|
458
571
|
tempDelta.copy(tempCenter).sub(tempTarget)
|
|
459
572
|
|
|
460
|
-
clearPendingFloorplanNavigationPose()
|
|
461
573
|
controls.current.setLookAt(
|
|
462
574
|
tempPosition.x + tempDelta.x,
|
|
463
575
|
tempPosition.y + tempDelta.y,
|
|
@@ -468,122 +580,88 @@ export const CustomCameraControls = () => {
|
|
|
468
580
|
true,
|
|
469
581
|
)
|
|
470
582
|
},
|
|
471
|
-
[
|
|
583
|
+
[isPreviewMode, isFirstPersonMode],
|
|
472
584
|
)
|
|
473
585
|
|
|
474
|
-
|
|
475
|
-
if (isFirstPersonMode) return
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
target: [...pose.target],
|
|
494
|
-
azimuth: targetAzimuth,
|
|
495
|
-
viewWidth: viewWidthUpdate.viewWidth,
|
|
496
|
-
publishOnComplete:
|
|
497
|
-
Math.abs(viewWidthUpdate.viewWidth - pose.viewWidth) >=
|
|
498
|
-
NAVIGATION_SYNC_VIEW_WIDTH_EPSILON,
|
|
499
|
-
}
|
|
500
|
-
// Match 3D settle time to 2D exponential decay (τ=90ms). SmoothDamp's
|
|
501
|
-
// effective time constant is smoothTime/2, so smoothTime=0.18 gives
|
|
502
|
-
// τ≈90ms and visual convergence in ~350-400ms, matching the 2D panel.
|
|
503
|
-
if (savedSmoothTimeRef.current === null) {
|
|
504
|
-
savedSmoothTimeRef.current = control.smoothTime
|
|
505
|
-
}
|
|
506
|
-
control.smoothTime = 0.18
|
|
507
|
-
control.moveTo(pose.target[0], pose.target[1], pose.target[2], true)
|
|
508
|
-
control.rotateTo(targetAzimuth, control.polarAngle, true)
|
|
509
|
-
applyCameraViewWidth(control, viewWidthUpdate)
|
|
586
|
+
const publishCurrentPose = useCallback(() => {
|
|
587
|
+
if (isFirstPersonMode || suppressPoseEvents.current || !controls.current) return
|
|
588
|
+
|
|
589
|
+
controls.current.getPosition(tempPosition, false)
|
|
590
|
+
controls.current.getTarget(tempTarget, false)
|
|
591
|
+
const targetDistance = tempPosition.distanceTo(tempTarget)
|
|
592
|
+
const projection = isPerspectiveCamera(camera)
|
|
593
|
+
? 'perspective'
|
|
594
|
+
: isOrthographicCamera(camera)
|
|
595
|
+
? 'orthographic'
|
|
596
|
+
: null
|
|
597
|
+
if (!projection) return
|
|
598
|
+
|
|
599
|
+
const pose = normalizeCameraPose({
|
|
600
|
+
position: [tempPosition.x, tempPosition.y, tempPosition.z],
|
|
601
|
+
target: [tempTarget.x, tempTarget.y, tempTarget.z],
|
|
602
|
+
projection,
|
|
603
|
+
viewWidth: getCameraViewWidth(camera, targetDistance, viewportSize),
|
|
604
|
+
...(isPerspectiveCamera(camera) ? { fov: camera.fov } : {}),
|
|
510
605
|
})
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const publishCurrentNavigationPose = useCallback(() => {
|
|
514
|
-
if (isFirstPersonMode || !controls.current) return
|
|
515
|
-
|
|
516
|
-
controls.current.getTarget(syncTarget, false)
|
|
517
|
-
controls.current.getSpherical(syncSpherical, false)
|
|
518
|
-
const viewWidth = getCameraViewWidth(camera, syncSpherical.radius, viewportSize)
|
|
519
|
-
|
|
520
|
-
const pendingFloorplanPose = pendingFloorplanNavigationPose.current
|
|
521
|
-
if (pendingFloorplanPose) {
|
|
522
|
-
// The camera is still damping toward a 2D-originated pose; do not echo
|
|
523
|
-
// intermediate 3D poses back into the floorplan.
|
|
524
|
-
if (
|
|
525
|
-
isCameraAtNavigationPose(pendingFloorplanPose, syncTarget, syncSpherical.theta, viewWidth)
|
|
526
|
-
) {
|
|
527
|
-
lastPublishedNavigationSync.current = pendingFloorplanPose
|
|
528
|
-
clearPendingFloorplanNavigationPose()
|
|
529
|
-
if (pendingFloorplanPose.publishOnComplete) {
|
|
530
|
-
useEditor.getState().publishNavigationSyncPose({
|
|
531
|
-
source: '3d',
|
|
532
|
-
target: [
|
|
533
|
-
pendingFloorplanPose.target[0],
|
|
534
|
-
pendingFloorplanPose.target[1],
|
|
535
|
-
pendingFloorplanPose.target[2],
|
|
536
|
-
],
|
|
537
|
-
azimuth: pendingFloorplanPose.azimuth,
|
|
538
|
-
viewWidth: pendingFloorplanPose.viewWidth,
|
|
539
|
-
})
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const previous = lastPublishedNavigationSync.current
|
|
546
|
-
if (
|
|
547
|
-
previous &&
|
|
548
|
-
Math.abs(previous.target[0] - syncTarget.x) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
549
|
-
Math.abs(previous.target[1] - syncTarget.y) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
550
|
-
Math.abs(previous.target[2] - syncTarget.z) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
551
|
-
Math.abs(getAngleDeltaRadians(previous.azimuth, syncSpherical.theta)) <
|
|
552
|
-
NAVIGATION_SYNC_AZIMUTH_EPSILON &&
|
|
553
|
-
Math.abs(previous.viewWidth - viewWidth) < NAVIGATION_SYNC_VIEW_WIDTH_EPSILON
|
|
554
|
-
) {
|
|
555
|
-
return
|
|
606
|
+
if (pose) {
|
|
607
|
+
publishCameraPose(pose)
|
|
556
608
|
}
|
|
609
|
+
}, [camera, isFirstPersonMode, viewportSize])
|
|
557
610
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
viewWidth,
|
|
562
|
-
}
|
|
563
|
-
useEditor.getState().publishNavigationSyncPose({
|
|
564
|
-
source: '3d',
|
|
565
|
-
target: [syncTarget.x, syncTarget.y, syncTarget.z],
|
|
566
|
-
azimuth: syncSpherical.theta,
|
|
567
|
-
viewWidth,
|
|
568
|
-
})
|
|
569
|
-
}, [camera, clearPendingFloorplanNavigationPose, isFirstPersonMode, viewportSize])
|
|
611
|
+
const handleCameraUpdate = useCallback(() => {
|
|
612
|
+
publishCurrentPose()
|
|
613
|
+
}, [publishCurrentPose])
|
|
570
614
|
|
|
571
615
|
useEffect(() => {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const frame = requestAnimationFrame(() => {
|
|
575
|
-
lastPublishedNavigationSync.current = null
|
|
576
|
-
publishCurrentNavigationPose()
|
|
577
|
-
})
|
|
578
|
-
|
|
579
|
-
return () => {
|
|
580
|
-
cancelAnimationFrame(frame)
|
|
581
|
-
}
|
|
582
|
-
}, [currentLevelId, isFirstPersonMode, isFloorplanOpen, publishCurrentNavigationPose])
|
|
616
|
+
publishInitialCameraPose(publishCurrentPose)
|
|
617
|
+
}, [publishCurrentPose])
|
|
583
618
|
|
|
584
619
|
useFrame((_, delta) => {
|
|
585
620
|
if (isFirstPersonMode || !controls.current) return
|
|
586
621
|
|
|
622
|
+
const activePose = activePoseInterpolation.current
|
|
623
|
+
if (activePose) {
|
|
624
|
+
const control = controls.current
|
|
625
|
+
if (activePose.camera !== camera || activePose.control !== control) {
|
|
626
|
+
try {
|
|
627
|
+
freezeActivePoseInterpolation()
|
|
628
|
+
} finally {
|
|
629
|
+
if (!pendingAppliedPose.current) suppressPoseEvents.current = false
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
control.getPosition(tempPosition, false)
|
|
633
|
+
control.getTarget(tempTarget, false)
|
|
634
|
+
const step = stepCameraPoseInterpolation(
|
|
635
|
+
[tempPosition.x, tempPosition.y, tempPosition.z],
|
|
636
|
+
[tempTarget.x, tempTarget.y, tempTarget.z],
|
|
637
|
+
activePose.plan.pose,
|
|
638
|
+
delta,
|
|
639
|
+
)
|
|
640
|
+
try {
|
|
641
|
+
// Transition-enabled calls retain one rest listener each, so this frame loop owns smoothing.
|
|
642
|
+
void control.setLookAt(
|
|
643
|
+
step.position[0],
|
|
644
|
+
step.position[1],
|
|
645
|
+
step.position[2],
|
|
646
|
+
step.target[0],
|
|
647
|
+
step.target[1],
|
|
648
|
+
step.target[2],
|
|
649
|
+
false,
|
|
650
|
+
)
|
|
651
|
+
control.update(0)
|
|
652
|
+
} catch {
|
|
653
|
+
if (activePoseInterpolation.current === activePose) {
|
|
654
|
+
activePoseInterpolation.current = null
|
|
655
|
+
releaseCameraPoseEventSuppression(suppressPoseEvents, publishCurrentPose)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (step.settled && activePoseInterpolation.current === activePose) {
|
|
659
|
+
activePoseInterpolation.current = null
|
|
660
|
+
releaseCameraPoseEventSuppression(suppressPoseEvents, publishCurrentPose)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
587
665
|
const panKeys = keyboardPanKeys.current
|
|
588
666
|
const horizontal = (panKeys.right ? 1 : 0) - (panKeys.left ? 1 : 0)
|
|
589
667
|
const vertical = (panKeys.forward ? 1 : 0) - (panKeys.backward ? 1 : 0)
|
|
@@ -599,10 +677,9 @@ export const CustomCameraControls = () => {
|
|
|
599
677
|
)
|
|
600
678
|
const step = (speed * Math.min(delta, 0.05)) / Math.hypot(horizontal, vertical)
|
|
601
679
|
|
|
602
|
-
clearPendingFloorplanNavigationPose()
|
|
603
680
|
if (horizontal !== 0) control.truck(horizontal * step, 0, true)
|
|
604
681
|
if (vertical !== 0) control.forward(vertical * step, true)
|
|
605
|
-
})
|
|
682
|
+
}, 0)
|
|
606
683
|
|
|
607
684
|
// Configure mouse buttons based on control mode and camera mode
|
|
608
685
|
const mouseButtons = useMemo(() => {
|
|
@@ -622,11 +699,11 @@ export const CustomCameraControls = () => {
|
|
|
622
699
|
|
|
623
700
|
// Touch gestures (mobile / trackpad).
|
|
624
701
|
// - One finger drag → rotate by default (much easier on a phone), but
|
|
625
|
-
// falls back to NONE while the
|
|
626
|
-
// placing/moving something
|
|
627
|
-
// so the editor's pointer handlers (place tool,
|
|
628
|
-
// drag-to-move endpoint, marquee selection drag
|
|
629
|
-
// keep priority over the camera.
|
|
702
|
+
// falls back to NONE while the editor owns the drag —
|
|
703
|
+
// placing/moving something, box-select, or a brush mode
|
|
704
|
+
// — so the editor's pointer handlers (place tool,
|
|
705
|
+
// drag-to-move endpoint, marquee selection drag, paint
|
|
706
|
+
// and sculpt strokes) keep priority over the camera.
|
|
630
707
|
// In preview mode it's TOUCH_TRUCK (pan), matching
|
|
631
708
|
// preview's left = SCREEN_PAN.
|
|
632
709
|
// - Two finger pinch → zoom + pan together (TOUCH_DOLLY_TRUCK for
|
|
@@ -641,9 +718,16 @@ export const CustomCameraControls = () => {
|
|
|
641
718
|
const endpointReshape = useEndpointReshape()
|
|
642
719
|
const activeHandleDrag = useActiveHandleDrag()
|
|
643
720
|
const isBoxSelectActive = mode === 'select' && selectionTool === 'marquee'
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
721
|
+
// The mode term lives in `editorOwnsOneFingerDrag` rather than in this OR: a
|
|
722
|
+
// brush mode owns the drag the way an armed tool does, but none of the
|
|
723
|
+
// transient terms notice it (entering paint or sculpt *clears* `tool`, and its
|
|
724
|
+
// scope is `painting`/`sculpting`, not `handle-drag`).
|
|
725
|
+
const isInteracting = editorOwnsOneFingerDrag({
|
|
726
|
+
mode,
|
|
727
|
+
activeGesture: Boolean(
|
|
728
|
+
tool || movingNode || endpointReshape || activeHandleDrag || isBoxSelectActive,
|
|
729
|
+
),
|
|
730
|
+
})
|
|
647
731
|
const touches = useMemo(() => {
|
|
648
732
|
const twoFingerAction =
|
|
649
733
|
cameraMode === 'orthographic'
|
|
@@ -750,8 +834,8 @@ export const CustomCameraControls = () => {
|
|
|
750
834
|
!(event.metaKey || event.ctrlKey || event.altKey) &&
|
|
751
835
|
!isEditableKeyboardTarget(event.target)
|
|
752
836
|
) {
|
|
753
|
-
setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
|
|
754
|
-
|
|
837
|
+
const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
|
|
838
|
+
if (changed) beginLocalCameraInteraction()
|
|
755
839
|
event.preventDefault()
|
|
756
840
|
event.stopPropagation()
|
|
757
841
|
}
|
|
@@ -783,6 +867,9 @@ export const CustomCameraControls = () => {
|
|
|
783
867
|
if (isKeyboardPanKey(event.code)) {
|
|
784
868
|
const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, false)
|
|
785
869
|
if (changed) {
|
|
870
|
+
if (!hasKeyboardPanInput(keyboardPanKeys.current)) {
|
|
871
|
+
cameraDraggingLifecycle.end()
|
|
872
|
+
}
|
|
786
873
|
event.preventDefault()
|
|
787
874
|
event.stopPropagation()
|
|
788
875
|
}
|
|
@@ -814,7 +901,6 @@ export const CustomCameraControls = () => {
|
|
|
814
901
|
|
|
815
902
|
const onPointerDown = (event: PointerEvent) => {
|
|
816
903
|
if (!(event.target instanceof Node) || !gl.domElement.contains(event.target)) return
|
|
817
|
-
clearPendingFloorplanNavigationPose()
|
|
818
904
|
if (event.button !== 1 && !(event.button === 0 && keyState.space)) return
|
|
819
905
|
|
|
820
906
|
panPointerId = event.pointerId
|
|
@@ -823,7 +909,8 @@ export const CustomCameraControls = () => {
|
|
|
823
909
|
}
|
|
824
910
|
|
|
825
911
|
const onWheel = () => {
|
|
826
|
-
|
|
912
|
+
beginLocalCameraInteraction()
|
|
913
|
+
cameraDraggingLifecycle.scheduleEnd()
|
|
827
914
|
}
|
|
828
915
|
|
|
829
916
|
const onPointerUp = (event: PointerEvent) => {
|
|
@@ -842,6 +929,7 @@ export const CustomCameraControls = () => {
|
|
|
842
929
|
panPointerId = null
|
|
843
930
|
panPointerButton = null
|
|
844
931
|
clearNavigationCursor()
|
|
932
|
+
cameraDraggingLifecycle.end()
|
|
845
933
|
updateConfig()
|
|
846
934
|
}
|
|
847
935
|
|
|
@@ -851,7 +939,7 @@ export const CustomCameraControls = () => {
|
|
|
851
939
|
window.addEventListener('pointerup', onPointerUp, true)
|
|
852
940
|
window.addEventListener('pointercancel', onPointerUp, true)
|
|
853
941
|
window.addEventListener('blur', onBlur)
|
|
854
|
-
gl.domElement.addEventListener('wheel', onWheel, { passive: true })
|
|
942
|
+
gl.domElement.addEventListener('wheel', onWheel, { capture: true, passive: true })
|
|
855
943
|
updateConfig()
|
|
856
944
|
|
|
857
945
|
return () => {
|
|
@@ -861,29 +949,30 @@ export const CustomCameraControls = () => {
|
|
|
861
949
|
window.removeEventListener('pointerup', onPointerUp, true)
|
|
862
950
|
window.removeEventListener('pointercancel', onPointerUp, true)
|
|
863
951
|
window.removeEventListener('blur', onBlur)
|
|
864
|
-
gl.domElement.removeEventListener('wheel', onWheel)
|
|
952
|
+
gl.domElement.removeEventListener('wheel', onWheel, true)
|
|
865
953
|
clearKeyboardPanKeys()
|
|
866
954
|
clearNavigationCursor()
|
|
955
|
+
cameraDraggingLifecycle.end()
|
|
867
956
|
}
|
|
868
|
-
}, [
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
const control = controls.current
|
|
877
|
-
if (!control) return
|
|
957
|
+
}, [
|
|
958
|
+
beginLocalCameraInteraction,
|
|
959
|
+
cameraDraggingLifecycle,
|
|
960
|
+
cameraMode,
|
|
961
|
+
gl,
|
|
962
|
+
isPreviewMode,
|
|
963
|
+
isFirstPersonMode,
|
|
964
|
+
])
|
|
878
965
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
966
|
+
// `controlstart` fires only for user pointer interactions. Pointerdowns
|
|
967
|
+
// mapped to ACTION.NONE must not flag the camera as dragging because no
|
|
968
|
+
// rest/sleep event follows to clear the flag.
|
|
969
|
+
const handleControlStart = useCallback(() => {
|
|
970
|
+
beginLocalCameraInteraction({
|
|
971
|
+
dragging: controls.current
|
|
972
|
+
? controls.current.currentAction !== CameraControlsImpl.ACTION.NONE
|
|
973
|
+
: false,
|
|
974
|
+
})
|
|
975
|
+
}, [beginLocalCameraInteraction])
|
|
887
976
|
|
|
888
977
|
// Preview mode: auto-navigate camera to selected node (viewer behavior)
|
|
889
978
|
const previewTargetNodeId = isPreviewMode
|
|
@@ -1090,7 +1179,6 @@ export const CustomCameraControls = () => {
|
|
|
1090
1179
|
if (!node?.camera) return
|
|
1091
1180
|
const { position, target } = node.camera
|
|
1092
1181
|
|
|
1093
|
-
clearPendingFloorplanNavigationPose()
|
|
1094
1182
|
controls.current.setLookAt(
|
|
1095
1183
|
position[0],
|
|
1096
1184
|
position[1],
|
|
@@ -1111,7 +1199,6 @@ export const CustomCameraControls = () => {
|
|
|
1111
1199
|
// Otherwise, go to top view (0°)
|
|
1112
1200
|
const targetAngle = currentPolarAngle < 0.1 ? Math.PI / 4 : 0
|
|
1113
1201
|
|
|
1114
|
-
clearPendingFloorplanNavigationPose()
|
|
1115
1202
|
controls.current.rotatePolarTo(targetAngle, true)
|
|
1116
1203
|
}
|
|
1117
1204
|
|
|
@@ -1124,7 +1211,6 @@ export const CustomCameraControls = () => {
|
|
|
1124
1211
|
const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
|
|
1125
1212
|
const target = rounded - Math.PI / 2
|
|
1126
1213
|
|
|
1127
|
-
clearPendingFloorplanNavigationPose()
|
|
1128
1214
|
controls.current.rotateTo(target, currentPolar, true)
|
|
1129
1215
|
}
|
|
1130
1216
|
|
|
@@ -1137,7 +1223,6 @@ export const CustomCameraControls = () => {
|
|
|
1137
1223
|
const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
|
|
1138
1224
|
const target = rounded + Math.PI / 2
|
|
1139
1225
|
|
|
1140
|
-
clearPendingFloorplanNavigationPose()
|
|
1141
1226
|
controls.current.rotateTo(target, currentPolar, true)
|
|
1142
1227
|
}
|
|
1143
1228
|
|
|
@@ -1149,7 +1234,6 @@ export const CustomCameraControls = () => {
|
|
|
1149
1234
|
if (isFirstPersonMode || !controls.current || isPreviewMode) return
|
|
1150
1235
|
if (!bounds) {
|
|
1151
1236
|
// Restore default framing pose when no bounds were computed.
|
|
1152
|
-
clearPendingFloorplanNavigationPose()
|
|
1153
1237
|
controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
|
|
1154
1238
|
return
|
|
1155
1239
|
}
|
|
@@ -1160,7 +1244,6 @@ export const CustomCameraControls = () => {
|
|
|
1160
1244
|
const maxExtent = Math.max(w, d)
|
|
1161
1245
|
const distance = Math.max(maxExtent * 1.4, 15)
|
|
1162
1246
|
const height = Math.max(maxExtent * 0.8, 10)
|
|
1163
|
-
clearPendingFloorplanNavigationPose()
|
|
1164
1247
|
controls.current.setLookAt(cx + distance * 0.7, height, cz + distance * 0.7, cx, 0, cz, true)
|
|
1165
1248
|
}
|
|
1166
1249
|
|
|
@@ -1181,15 +1264,25 @@ export const CustomCameraControls = () => {
|
|
|
1181
1264
|
emitter.off('camera-controls:orbit-ccw', handleOrbitCCW)
|
|
1182
1265
|
emitter.off('camera-controls:fit-scene', handleFitScene)
|
|
1183
1266
|
}
|
|
1184
|
-
}, [
|
|
1267
|
+
}, [focusNode, isPreviewMode, isFirstPersonMode])
|
|
1185
1268
|
|
|
1186
1269
|
const onTransitionStart = useCallback(() => {
|
|
1187
|
-
|
|
1188
|
-
}, [])
|
|
1270
|
+
cameraDraggingLifecycle.begin()
|
|
1271
|
+
}, [cameraDraggingLifecycle])
|
|
1189
1272
|
|
|
1190
1273
|
const onRest = useCallback(() => {
|
|
1191
|
-
|
|
1192
|
-
}, [])
|
|
1274
|
+
cameraDraggingLifecycle.end()
|
|
1275
|
+
}, [cameraDraggingLifecycle])
|
|
1276
|
+
|
|
1277
|
+
const onControlEnd = useCallback(() => {
|
|
1278
|
+
// A mapped-button tap with zero camera movement never wakes the
|
|
1279
|
+
// controls, so no rest/sleep follows — clear the dragging flag on
|
|
1280
|
+
// release. While damping is still settling (`active`), rest/sleep
|
|
1281
|
+
// clears it instead.
|
|
1282
|
+
if (!controls.current?.active) {
|
|
1283
|
+
cameraDraggingLifecycle.end()
|
|
1284
|
+
}
|
|
1285
|
+
}, [cameraDraggingLifecycle])
|
|
1193
1286
|
|
|
1194
1287
|
// Preset capture mode frames a single subtree (often a 0.3–2m preset),
|
|
1195
1288
|
// so the default 2m minDistance prevents the user from getting close
|
|
@@ -1211,7 +1304,9 @@ export const CustomCameraControls = () => {
|
|
|
1211
1304
|
minDistance={minDistance}
|
|
1212
1305
|
minPolarAngle={0}
|
|
1213
1306
|
mouseButtons={mouseButtons}
|
|
1214
|
-
|
|
1307
|
+
onControlEnd={onControlEnd}
|
|
1308
|
+
onControlStart={handleControlStart}
|
|
1309
|
+
onUpdate={handleCameraUpdate}
|
|
1215
1310
|
onRest={onRest}
|
|
1216
1311
|
onSleep={onRest}
|
|
1217
1312
|
onTransitionStart={onTransitionStart}
|