@ifc-lite/renderer 1.43.0 → 1.44.0
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/dist/camera-animation.d.ts +20 -32
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +71 -358
- package/dist/camera-animation.js.map +1 -1
- package/dist/camera-controls.d.ts +11 -51
- package/dist/camera-controls.d.ts.map +1 -1
- package/dist/camera-controls.js +140 -12
- package/dist/camera-controls.js.map +1 -1
- package/dist/camera-first-person.d.ts +52 -0
- package/dist/camera-first-person.d.ts.map +1 -0
- package/dist/camera-first-person.js +119 -0
- package/dist/camera-first-person.js.map +1 -0
- package/dist/camera-framing.d.ts +86 -0
- package/dist/camera-framing.d.ts.map +1 -0
- package/dist/camera-framing.js +230 -0
- package/dist/camera-framing.js.map +1 -0
- package/dist/camera-guards.d.ts +148 -0
- package/dist/camera-guards.d.ts.map +1 -0
- package/dist/camera-guards.js +150 -0
- package/dist/camera-guards.js.map +1 -0
- package/dist/camera-matrices.d.ts +14 -0
- package/dist/camera-matrices.d.ts.map +1 -0
- package/dist/camera-matrices.js +146 -0
- package/dist/camera-matrices.js.map +1 -0
- package/dist/camera-preset-view.d.ts +56 -0
- package/dist/camera-preset-view.d.ts.map +1 -0
- package/dist/camera-preset-view.js +180 -0
- package/dist/camera-preset-view.js.map +1 -0
- package/dist/camera-projection.d.ts +1 -1
- package/dist/camera-projection.d.ts.map +1 -1
- package/dist/camera-projection.js +83 -30
- package/dist/camera-projection.js.map +1 -1
- package/dist/camera-state.d.ts +62 -0
- package/dist/camera-state.d.ts.map +1 -0
- package/dist/camera-state.js +5 -0
- package/dist/camera-state.js.map +1 -0
- package/dist/camera.d.ts +22 -11
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +75 -120
- package/dist/camera.js.map +1 -1
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +23 -1
- package/dist/environment.js.map +1 -1
- package/dist/index.d.ts +154 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +294 -45
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +49 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +122 -67
- package/dist/math.js.map +1 -1
- package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
- package/dist/renderer-symbolic-overlays.js +20 -25
- package/dist/renderer-symbolic-overlays.js.map +1 -1
- package/dist/scene-geometry.d.ts +32 -0
- package/dist/scene-geometry.d.ts.map +1 -1
- package/dist/scene-geometry.js +66 -0
- package/dist/scene-geometry.js.map +1 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +22 -97
- package/dist/scene.js.map +1 -1
- package/dist/section-2d-lift.d.ts +105 -0
- package/dist/section-2d-lift.d.ts.map +1 -0
- package/dist/section-2d-lift.js +162 -0
- package/dist/section-2d-lift.js.map +1 -0
- package/dist/section-2d-line-buffer.d.ts +58 -0
- package/dist/section-2d-line-buffer.d.ts.map +1 -0
- package/dist/section-2d-line-buffer.js +109 -0
- package/dist/section-2d-line-buffer.js.map +1 -0
- package/dist/section-2d-overlay.d.ts +28 -77
- package/dist/section-2d-overlay.d.ts.map +1 -1
- package/dist/section-2d-overlay.js +161 -593
- package/dist/section-2d-overlay.js.map +1 -1
- package/dist/section-plane-basis.d.ts +22 -3
- package/dist/section-plane-basis.d.ts.map +1 -1
- package/dist/section-plane-basis.js +51 -10
- package/dist/section-plane-basis.js.map +1 -1
- package/dist/section-plane.d.ts.map +1 -1
- package/dist/section-plane.js +10 -1
- package/dist/section-plane.js.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.d.ts +102 -0
- package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -0
- package/dist/shaders/section-2d-overlay.wgsl.js +281 -0
- package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -0
- package/package.json +3 -2
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Camera animation system handling tweened transitions
|
|
3
|
-
*
|
|
2
|
+
* Camera animation system handling tweened transitions and inertia/momentum,
|
|
3
|
+
* plus the thin wrappers that apply a pose picked by `camera-framing.ts` (free
|
|
4
|
+
* framing) or `camera-preset-view.ts` (the ViewCube's named directions).
|
|
4
5
|
* Extracted from Camera class using composition pattern.
|
|
6
|
+
*
|
|
7
|
+
* The tween's own failure story is the NaN *latch*: `velocity` accumulates in
|
|
8
|
+
* place and the loop spends a channel only while `Math.abs(v) > minVelocity`,
|
|
9
|
+
* which is false for NaN forever, so one non-finite gesture delta kills that
|
|
10
|
+
* channel for the session rather than for a frame (#2441/#2473). Framing's
|
|
11
|
+
* failure story is a different input class entirely and lives in
|
|
12
|
+
* `camera-framing.ts` with the guards that reject it.
|
|
5
13
|
*/
|
|
6
14
|
import type { Vec3 } from './types.js';
|
|
7
|
-
import type { CameraInternalState } from './camera-
|
|
15
|
+
import type { CameraInternalState } from './camera-state.js';
|
|
8
16
|
import type { CameraControls } from './camera-controls.js';
|
|
9
17
|
import type { CameraProjection } from './camera-projection.js';
|
|
18
|
+
import { type FramingBounds } from './camera-framing.js';
|
|
10
19
|
/**
|
|
11
20
|
* Manages camera animations: tweened transitions between positions,
|
|
12
|
-
* inertia/momentum after user interaction, preset view switching
|
|
13
|
-
* with rotation cycling
|
|
21
|
+
* inertia/momentum after user interaction, and preset view switching
|
|
22
|
+
* with rotation cycling.
|
|
14
23
|
*/
|
|
15
24
|
export declare class CameraAnimator {
|
|
16
25
|
private readonly state;
|
|
@@ -31,8 +40,6 @@ export declare class CameraAnimator {
|
|
|
31
40
|
private animationStartOrthoSize;
|
|
32
41
|
private animationEndOrthoSize;
|
|
33
42
|
private animationEasing;
|
|
34
|
-
private isFirstPersonMode;
|
|
35
|
-
private walkVelocity;
|
|
36
43
|
private lastPresetView;
|
|
37
44
|
private presetViewRotation;
|
|
38
45
|
constructor(state: CameraInternalState, updateMatrices: () => void, controls: CameraControls, projection: CameraProjection);
|
|
@@ -59,6 +66,12 @@ export declare class CameraAnimator {
|
|
|
59
66
|
*/
|
|
60
67
|
frameBounds(min: Vec3, max: Vec3, duration?: number): Promise<void>;
|
|
61
68
|
zoomExtent(min: Vec3, max: Vec3, duration?: number): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Set preset view with explicit bounds (Y-up coordinate system)
|
|
71
|
+
* Clicking the same view again rotates 90 degrees around the view axis
|
|
72
|
+
* @param buildingRotation Optional building rotation in radians (from IfcSite placement)
|
|
73
|
+
*/
|
|
74
|
+
setPresetView(view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right', bounds?: FramingBounds, buildingRotation?: number): void;
|
|
62
75
|
/**
|
|
63
76
|
* Animate camera to position and target
|
|
64
77
|
*/
|
|
@@ -71,31 +84,6 @@ export declare class CameraAnimator {
|
|
|
71
84
|
* Easing function: easeOutCubic
|
|
72
85
|
*/
|
|
73
86
|
private easeOutCubic;
|
|
74
|
-
/**
|
|
75
|
-
* Set first-person mode
|
|
76
|
-
*/
|
|
77
|
-
enableFirstPersonMode(enabled: boolean): void;
|
|
78
|
-
/**
|
|
79
|
-
* Walk on the horizontal XZ plane (Y-up coordinate system).
|
|
80
|
-
* Forward/backward moves in the camera's horizontal facing direction.
|
|
81
|
-
* Left/right strafes perpendicular. Y position stays fixed (walking on ground).
|
|
82
|
-
* Speed scales with scene size. Movement uses smooth acceleration to avoid
|
|
83
|
-
* abrupt jumps — velocity ramps up over successive frames.
|
|
84
|
-
*/
|
|
85
|
-
moveFirstPerson(forward: number, right: number, _up: number): void;
|
|
86
|
-
/**
|
|
87
|
-
* Set preset view with explicit bounds (Y-up coordinate system)
|
|
88
|
-
* Clicking the same view again rotates 90 degrees around the view axis
|
|
89
|
-
* @param buildingRotation Optional building rotation in radians (from IfcSite placement)
|
|
90
|
-
*/
|
|
91
|
-
setPresetView(view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right', bounds?: {
|
|
92
|
-
min: Vec3;
|
|
93
|
-
max: Vec3;
|
|
94
|
-
}, buildingRotation?: number): void;
|
|
95
|
-
/**
|
|
96
|
-
* Get current bounds estimate (simplified - in production would use scene bounds)
|
|
97
|
-
*/
|
|
98
|
-
private getCurrentBounds;
|
|
99
87
|
/**
|
|
100
88
|
* Reset velocity (stop inertia)
|
|
101
89
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"camera-animation.d.ts","sourceRoot":"","sources":["../src/camera-animation.ts"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"camera-animation.d.ts","sourceRoot":"","sources":["../src/camera-animation.ts"],"names":[],"mappings":"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"}
|
package/dist/camera-animation.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
import { areFiniteNumbers, usableOrthoSize } from './camera-guards.js';
|
|
5
|
+
import { frameBoundsTarget, framePointTarget, zoomExtentTarget, } from './camera-framing.js';
|
|
6
|
+
import { presetViewTarget, resolvePresetBounds } from './camera-preset-view.js';
|
|
4
7
|
/**
|
|
5
8
|
* Manages camera animations: tweened transitions between positions,
|
|
6
|
-
* inertia/momentum after user interaction, preset view switching
|
|
7
|
-
* with rotation cycling
|
|
9
|
+
* inertia/momentum after user interaction, and preset view switching
|
|
10
|
+
* with rotation cycling.
|
|
8
11
|
*/
|
|
9
12
|
export class CameraAnimator {
|
|
10
13
|
state;
|
|
@@ -27,9 +30,6 @@ export class CameraAnimator {
|
|
|
27
30
|
animationStartOrthoSize = null;
|
|
28
31
|
animationEndOrthoSize = null;
|
|
29
32
|
animationEasing = null;
|
|
30
|
-
// First-person mode
|
|
31
|
-
isFirstPersonMode = false;
|
|
32
|
-
walkVelocity = { x: 0, z: 0 };
|
|
33
33
|
// Track preset view for rotation cycling (clicking same view rotates 90 degrees)
|
|
34
34
|
lastPresetView = null;
|
|
35
35
|
presetViewRotation = 0; // 0, 1, 2, 3 = 0, 90, 180, 270 degrees
|
|
@@ -40,15 +40,29 @@ export class CameraAnimator {
|
|
|
40
40
|
this.projection = projection;
|
|
41
41
|
}
|
|
42
42
|
// --- Velocity management (called by Camera class) ---
|
|
43
|
+
// Every one of these accumulates **in place**, and the inertia loop spends a
|
|
44
|
+
// velocity only while `Math.abs(v) > minVelocity` — false for NaN. So a
|
|
45
|
+
// single non-finite gesture argument does not cost one frame of inertia: it
|
|
46
|
+
// latches the channel dead for the rest of the session, and the value never
|
|
47
|
+
// decays back under the threshold either. That is the same latch
|
|
48
|
+
// `moveFirstPerson`'s `walkVelocity` had (#2441), reached from the argument
|
|
49
|
+
// side rather than the pose side (#2473). Skip the contribution instead;
|
|
50
|
+
// the gesture itself has already been rejected by the same test downstream.
|
|
43
51
|
addOrbitVelocity(deltaX, deltaY) {
|
|
52
|
+
if (!areFiniteNumbers(deltaX, deltaY))
|
|
53
|
+
return;
|
|
44
54
|
this.velocity.orbit.x += deltaX * 0.001;
|
|
45
55
|
this.velocity.orbit.y += deltaY * 0.001;
|
|
46
56
|
}
|
|
47
57
|
addPanVelocity(deltaX, deltaY, panSpeed) {
|
|
58
|
+
if (!areFiniteNumbers(deltaX, deltaY, panSpeed))
|
|
59
|
+
return;
|
|
48
60
|
this.velocity.pan.x += deltaX * panSpeed * 0.1;
|
|
49
61
|
this.velocity.pan.y += deltaY * panSpeed * 0.1;
|
|
50
62
|
}
|
|
51
63
|
addZoomVelocity(normalizedDelta) {
|
|
64
|
+
if (!Number.isFinite(normalizedDelta))
|
|
65
|
+
return;
|
|
52
66
|
this.velocity.zoom += normalizedDelta * 0.1;
|
|
53
67
|
}
|
|
54
68
|
/**
|
|
@@ -79,9 +93,16 @@ export class CameraAnimator {
|
|
|
79
93
|
this.state.camera.target.x = this.animationStartTarget.x + (this.animationEndTarget.x - this.animationStartTarget.x) * t;
|
|
80
94
|
this.state.camera.target.y = this.animationStartTarget.y + (this.animationEndTarget.y - this.animationStartTarget.y) * t;
|
|
81
95
|
this.state.camera.target.z = this.animationStartTarget.z + (this.animationEndTarget.z - this.animationStartTarget.z) * t;
|
|
82
|
-
// Interpolate orthoSize if animating orthographic zoom
|
|
96
|
+
// Interpolate orthoSize if animating orthographic zoom.
|
|
97
|
+
// The animator is the second writer that bypasses `Camera.setOrthoSize`
|
|
98
|
+
// (#2461): the read-site backstop in `updateCameraMatrices` keeps the
|
|
99
|
+
// projection matrix finite but not the state, and `getOrthoSize()`
|
|
100
|
+
// reads the state — which is what a saved viewpoint persists. Keep the
|
|
101
|
+
// previous half-height when the interpolation yields nothing usable.
|
|
83
102
|
if (this.animationStartOrthoSize !== null && this.animationEndOrthoSize !== null) {
|
|
84
|
-
|
|
103
|
+
const next = usableOrthoSize(this.animationStartOrthoSize + (this.animationEndOrthoSize - this.animationStartOrthoSize) * t);
|
|
104
|
+
if (next !== null)
|
|
105
|
+
this.state.orthoSize = next;
|
|
85
106
|
}
|
|
86
107
|
// Interpolate up vector if animating with up
|
|
87
108
|
if (this.animationStartUp && this.animationEndUp) {
|
|
@@ -118,7 +139,9 @@ export class CameraAnimator {
|
|
|
118
139
|
this.state.camera.up.z = this.animationEndUp.z;
|
|
119
140
|
}
|
|
120
141
|
if (this.animationEndOrthoSize !== null) {
|
|
121
|
-
|
|
142
|
+
const next = usableOrthoSize(this.animationEndOrthoSize);
|
|
143
|
+
if (next !== null)
|
|
144
|
+
this.state.orthoSize = next;
|
|
122
145
|
}
|
|
123
146
|
this.updateMatrices();
|
|
124
147
|
this.animationStartTime = 0;
|
|
@@ -155,153 +178,63 @@ export class CameraAnimator {
|
|
|
155
178
|
}
|
|
156
179
|
return isAnimating;
|
|
157
180
|
}
|
|
181
|
+
// --- Framing (pose picked in `camera-framing.ts`, applied by the tween) ---
|
|
182
|
+
//
|
|
183
|
+
// These wrappers exist on the animator because they must return the tween's
|
|
184
|
+
// promise, and the promise machinery *is* tween state. They null-check the
|
|
185
|
+
// picked target and do nothing else — in particular they do NOT re-validate
|
|
186
|
+
// the input. Each framing input is guarded exactly once, in the pure module;
|
|
187
|
+
// a second copy here would leave neither copy load-bearing, so a reverted
|
|
188
|
+
// guard would still look green.
|
|
158
189
|
/**
|
|
159
190
|
* Frame/center view on a point (keeps current distance and direction)
|
|
160
191
|
* Standard CAD "Frame Selection" behavior
|
|
161
192
|
*/
|
|
162
193
|
async framePoint(point, duration = 300) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
z: this.state.camera.position.z - this.state.camera.target.z,
|
|
168
|
-
};
|
|
169
|
-
// New position: point + current offset
|
|
170
|
-
const endPos = {
|
|
171
|
-
x: point.x + dir.x,
|
|
172
|
-
y: point.y + dir.y,
|
|
173
|
-
z: point.z + dir.z,
|
|
174
|
-
};
|
|
175
|
-
return this.animateTo(endPos, point, duration);
|
|
194
|
+
const fit = framePointTarget(this.state, point);
|
|
195
|
+
if (!fit)
|
|
196
|
+
return;
|
|
197
|
+
return this.animateTo(fit.position, fit.target, duration, fit.orthoSize);
|
|
176
198
|
}
|
|
177
199
|
/**
|
|
178
200
|
* Frame selection - zoom to fit bounds while keeping current view direction
|
|
179
201
|
* This is what "Frame Selection" should do - zoom to fill screen
|
|
180
202
|
*/
|
|
181
203
|
async frameBounds(min, max, duration = 300) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
};
|
|
187
|
-
const size = {
|
|
188
|
-
x: max.x - min.x,
|
|
189
|
-
y: max.y - min.y,
|
|
190
|
-
z: max.z - min.z,
|
|
191
|
-
};
|
|
192
|
-
const maxSize = Math.max(size.x, size.y, size.z);
|
|
193
|
-
if (maxSize < 1e-6) {
|
|
194
|
-
// Very small or zero size - just center on it
|
|
195
|
-
return this.framePoint(center, duration);
|
|
196
|
-
}
|
|
197
|
-
// Calculate required distance based on FOV to fit bounds
|
|
198
|
-
const fovFactor = Math.tan(this.state.camera.fov / 2);
|
|
199
|
-
const distance = (maxSize / 2) / fovFactor * 1.2; // 1.2x padding for nice framing
|
|
200
|
-
// Get current viewing direction from view matrix (more reliable than position-target)
|
|
201
|
-
// View matrix forward is -Z axis in view space
|
|
202
|
-
const viewMatrix = this.state.viewMatrix.m;
|
|
203
|
-
// Extract forward direction from view matrix (negative Z column, normalized)
|
|
204
|
-
let dir = {
|
|
205
|
-
x: -viewMatrix[8], // -m[2][0] (forward X)
|
|
206
|
-
y: -viewMatrix[9], // -m[2][1] (forward Y)
|
|
207
|
-
z: -viewMatrix[10], // -m[2][2] (forward Z)
|
|
208
|
-
};
|
|
209
|
-
const dirLen = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
210
|
-
// Normalize direction
|
|
211
|
-
if (dirLen > 1e-6) {
|
|
212
|
-
dir.x /= dirLen;
|
|
213
|
-
dir.y /= dirLen;
|
|
214
|
-
dir.z /= dirLen;
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
// Fallback: use position-target if view matrix is invalid
|
|
218
|
-
dir = {
|
|
219
|
-
x: this.state.camera.position.x - this.state.camera.target.x,
|
|
220
|
-
y: this.state.camera.position.y - this.state.camera.target.y,
|
|
221
|
-
z: this.state.camera.position.z - this.state.camera.target.z,
|
|
222
|
-
};
|
|
223
|
-
const fallbackLen = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
224
|
-
if (fallbackLen > 1e-6) {
|
|
225
|
-
dir.x /= fallbackLen;
|
|
226
|
-
dir.y /= fallbackLen;
|
|
227
|
-
dir.z /= fallbackLen;
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
// Last resort: southeast isometric
|
|
231
|
-
dir.x = 0.6;
|
|
232
|
-
dir.y = 0.5;
|
|
233
|
-
dir.z = 0.6;
|
|
234
|
-
const len = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
235
|
-
dir.x /= len;
|
|
236
|
-
dir.y /= len;
|
|
237
|
-
dir.z /= len;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
// New position: center + direction * distance
|
|
241
|
-
const endPos = {
|
|
242
|
-
x: center.x + dir.x * distance,
|
|
243
|
-
y: center.y + dir.y * distance,
|
|
244
|
-
z: center.z + dir.z * distance,
|
|
245
|
-
};
|
|
246
|
-
// Calculate orthoSize for orthographic mode so zoom level resets properly
|
|
247
|
-
const aspect = this.state.camera.aspect || 1;
|
|
248
|
-
const endOrthoSize = this.state.projectionMode === 'orthographic'
|
|
249
|
-
? Math.max(0.01, maxSize / 2, maxSize / 2 / aspect) * 1.2
|
|
250
|
-
: undefined;
|
|
251
|
-
return this.animateTo(endPos, center, duration, endOrthoSize);
|
|
204
|
+
const fit = frameBoundsTarget(this.state, min, max);
|
|
205
|
+
if (!fit)
|
|
206
|
+
return;
|
|
207
|
+
return this.animateTo(fit.position, fit.target, duration, fit.orthoSize);
|
|
252
208
|
}
|
|
253
209
|
async zoomExtent(min, max, duration = 300) {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
z: (min.z + max.z) / 2,
|
|
258
|
-
};
|
|
259
|
-
const size = {
|
|
260
|
-
x: max.x - min.x,
|
|
261
|
-
y: max.y - min.y,
|
|
262
|
-
z: max.z - min.z,
|
|
263
|
-
};
|
|
264
|
-
const maxSize = Math.max(size.x, size.y, size.z);
|
|
265
|
-
// Calculate required distance based on FOV
|
|
266
|
-
const fovFactor = Math.tan(this.state.camera.fov / 2);
|
|
267
|
-
const distance = (maxSize / 2) / fovFactor * 1.5; // 1.5x for padding
|
|
210
|
+
const fit = zoomExtentTarget(this.state, min, max);
|
|
211
|
+
if (!fit)
|
|
212
|
+
return;
|
|
268
213
|
// Update near/far planes dynamically
|
|
269
|
-
this.projection.updateNearFarPlanes(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
214
|
+
this.projection.updateNearFarPlanes(fit.fitDistance);
|
|
215
|
+
return this.animateTo(fit.position, fit.target, duration, fit.orthoSize);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Set preset view with explicit bounds (Y-up coordinate system)
|
|
219
|
+
* Clicking the same view again rotates 90 degrees around the view axis
|
|
220
|
+
* @param buildingRotation Optional building rotation in radians (from IfcSite placement)
|
|
221
|
+
*/
|
|
222
|
+
setPresetView(view, bounds, buildingRotation) {
|
|
223
|
+
// Resolved and validated before the rotation cycles, so a rejected preset
|
|
224
|
+
// leaves the ViewCube's cycle position where it was.
|
|
225
|
+
const useBounds = resolvePresetBounds(this.state, bounds);
|
|
226
|
+
if (!useBounds)
|
|
227
|
+
return;
|
|
228
|
+
// Check if clicking the same view again - cycle rotation
|
|
229
|
+
if (this.lastPresetView === view) {
|
|
230
|
+
this.presetViewRotation = (this.presetViewRotation + 1) % 4;
|
|
282
231
|
}
|
|
283
232
|
else {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
dir.y = 0.5;
|
|
287
|
-
dir.z = 0.6;
|
|
288
|
-
const len = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
289
|
-
dir.x /= len;
|
|
290
|
-
dir.y /= len;
|
|
291
|
-
dir.z /= len;
|
|
233
|
+
this.lastPresetView = view;
|
|
234
|
+
this.presetViewRotation = 0;
|
|
292
235
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
x: center.x + dir.x * distance,
|
|
296
|
-
y: center.y + dir.y * distance,
|
|
297
|
-
z: center.z + dir.z * distance,
|
|
298
|
-
};
|
|
299
|
-
// Calculate orthoSize for orthographic mode so zoom level resets properly
|
|
300
|
-
const aspect = this.state.camera.aspect || 1;
|
|
301
|
-
const endOrthoSize = this.state.projectionMode === 'orthographic'
|
|
302
|
-
? Math.max(0.01, maxSize / 2, maxSize / 2 / aspect) * 1.5
|
|
303
|
-
: undefined;
|
|
304
|
-
return this.animateTo(endPos, center, duration, endOrthoSize);
|
|
236
|
+
const fit = presetViewTarget(this.state, view, useBounds, this.presetViewRotation, buildingRotation);
|
|
237
|
+
this.animateToWithUp(fit.position, fit.target, fit.up, 300);
|
|
305
238
|
}
|
|
306
239
|
/**
|
|
307
240
|
* Animate camera to position and target
|
|
@@ -377,226 +310,6 @@ export class CameraAnimator {
|
|
|
377
310
|
easeOutCubic(t) {
|
|
378
311
|
return 1 - Math.pow(1 - t, 3);
|
|
379
312
|
}
|
|
380
|
-
/**
|
|
381
|
-
* Set first-person mode
|
|
382
|
-
*/
|
|
383
|
-
enableFirstPersonMode(enabled) {
|
|
384
|
-
this.isFirstPersonMode = enabled;
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Walk on the horizontal XZ plane (Y-up coordinate system).
|
|
388
|
-
* Forward/backward moves in the camera's horizontal facing direction.
|
|
389
|
-
* Left/right strafes perpendicular. Y position stays fixed (walking on ground).
|
|
390
|
-
* Speed scales with scene size. Movement uses smooth acceleration to avoid
|
|
391
|
-
* abrupt jumps — velocity ramps up over successive frames.
|
|
392
|
-
*/
|
|
393
|
-
moveFirstPerson(forward, right, _up) {
|
|
394
|
-
// Camera forward direction projected onto XZ plane
|
|
395
|
-
const dir = {
|
|
396
|
-
x: this.state.camera.target.x - this.state.camera.position.x,
|
|
397
|
-
z: this.state.camera.target.z - this.state.camera.position.z,
|
|
398
|
-
};
|
|
399
|
-
const horizLen = Math.sqrt(dir.x * dir.x + dir.z * dir.z);
|
|
400
|
-
if (horizLen < 1e-10)
|
|
401
|
-
return;
|
|
402
|
-
// Normalized horizontal forward and right vectors
|
|
403
|
-
const fwdX = dir.x / horizLen;
|
|
404
|
-
const fwdZ = dir.z / horizLen;
|
|
405
|
-
const rightX = -fwdZ;
|
|
406
|
-
const rightZ = fwdX;
|
|
407
|
-
// Target velocity from input (forward/right can be -2..2 with sprint)
|
|
408
|
-
const targetVelX = fwdX * forward + rightX * right;
|
|
409
|
-
const targetVelZ = fwdZ * forward + rightZ * right;
|
|
410
|
-
// Smooth acceleration: lerp current walk velocity toward target
|
|
411
|
-
this.walkVelocity.x += (targetVelX - this.walkVelocity.x) * 0.15;
|
|
412
|
-
this.walkVelocity.z += (targetVelZ - this.walkVelocity.z) * 0.15;
|
|
413
|
-
// Speed proportional to scene size (use camera-target distance as proxy)
|
|
414
|
-
const camDir = {
|
|
415
|
-
x: this.state.camera.position.x - this.state.camera.target.x,
|
|
416
|
-
y: this.state.camera.position.y - this.state.camera.target.y,
|
|
417
|
-
z: this.state.camera.position.z - this.state.camera.target.z,
|
|
418
|
-
};
|
|
419
|
-
const distance = Math.sqrt(camDir.x * camDir.x + camDir.y * camDir.y + camDir.z * camDir.z);
|
|
420
|
-
const speed = Math.max(0.02, distance * 0.004);
|
|
421
|
-
// Apply smoothed velocity
|
|
422
|
-
const offsetX = this.walkVelocity.x * speed;
|
|
423
|
-
const offsetZ = this.walkVelocity.z * speed;
|
|
424
|
-
// Move both position and target by the same offset (preserves view direction)
|
|
425
|
-
this.state.camera.position.x += offsetX;
|
|
426
|
-
this.state.camera.position.z += offsetZ;
|
|
427
|
-
this.state.camera.target.x += offsetX;
|
|
428
|
-
this.state.camera.target.z += offsetZ;
|
|
429
|
-
this.updateMatrices();
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Set preset view with explicit bounds (Y-up coordinate system)
|
|
433
|
-
* Clicking the same view again rotates 90 degrees around the view axis
|
|
434
|
-
* @param buildingRotation Optional building rotation in radians (from IfcSite placement)
|
|
435
|
-
*/
|
|
436
|
-
setPresetView(view, bounds, buildingRotation) {
|
|
437
|
-
const useBounds = bounds || this.getCurrentBounds();
|
|
438
|
-
if (!useBounds) {
|
|
439
|
-
console.warn('[Camera] No bounds available for setPresetView');
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
// Check if clicking the same view again - cycle rotation
|
|
443
|
-
if (this.lastPresetView === view) {
|
|
444
|
-
this.presetViewRotation = (this.presetViewRotation + 1) % 4;
|
|
445
|
-
}
|
|
446
|
-
else {
|
|
447
|
-
this.lastPresetView = view;
|
|
448
|
-
this.presetViewRotation = 0;
|
|
449
|
-
}
|
|
450
|
-
const center = {
|
|
451
|
-
x: (useBounds.min.x + useBounds.max.x) / 2,
|
|
452
|
-
y: (useBounds.min.y + useBounds.max.y) / 2,
|
|
453
|
-
z: (useBounds.min.z + useBounds.max.z) / 2,
|
|
454
|
-
};
|
|
455
|
-
const size = {
|
|
456
|
-
x: useBounds.max.x - useBounds.min.x,
|
|
457
|
-
y: useBounds.max.y - useBounds.min.y,
|
|
458
|
-
z: useBounds.max.z - useBounds.min.z,
|
|
459
|
-
};
|
|
460
|
-
const maxSize = Math.max(size.x, size.y, size.z);
|
|
461
|
-
// Calculate distance based on FOV for proper fit
|
|
462
|
-
const fovFactor = Math.tan(this.state.camera.fov / 2);
|
|
463
|
-
const distance = (maxSize / 2) / fovFactor * 1.5; // 1.5x for padding
|
|
464
|
-
let endPos;
|
|
465
|
-
const endTarget = center;
|
|
466
|
-
// WebGL uses Y-up coordinate system internally
|
|
467
|
-
// We set both position AND up vector for proper orthogonal views
|
|
468
|
-
let upVector = { x: 0, y: 1, z: 0 }; // Default Y-up
|
|
469
|
-
// Up vector rotation options for top/bottom views (rotate around Y axis)
|
|
470
|
-
// 0: -Z, 1: -X, 2: +Z, 3: +X
|
|
471
|
-
const topUpVectors = [
|
|
472
|
-
{ x: 0, y: 0, z: -1 }, // 0 degrees - North up
|
|
473
|
-
{ x: -1, y: 0, z: 0 }, // 90 degrees - West up
|
|
474
|
-
{ x: 0, y: 0, z: 1 }, // 180 degrees - South up
|
|
475
|
-
{ x: 1, y: 0, z: 0 }, // 270 degrees - East up
|
|
476
|
-
];
|
|
477
|
-
const bottomUpVectors = [
|
|
478
|
-
{ x: 0, y: 0, z: 1 }, // 0 degrees - South up
|
|
479
|
-
{ x: 1, y: 0, z: 0 }, // 90 degrees - East up
|
|
480
|
-
{ x: 0, y: 0, z: -1 }, // 180 degrees - North up
|
|
481
|
-
{ x: -1, y: 0, z: 0 }, // 270 degrees - West up
|
|
482
|
-
];
|
|
483
|
-
// Apply building rotation if present (rotate around Y axis)
|
|
484
|
-
const cosR = buildingRotation !== undefined && buildingRotation !== 0 ? Math.cos(buildingRotation) : 1.0;
|
|
485
|
-
const sinR = buildingRotation !== undefined && buildingRotation !== 0 ? Math.sin(buildingRotation) : 0.0;
|
|
486
|
-
switch (view) {
|
|
487
|
-
case 'top': {
|
|
488
|
-
// Top view: position camera *just barely* off the +Y pole so the
|
|
489
|
-
// subsequent orbit math has a well-defined polar tangent (no pole
|
|
490
|
-
// singularity). camera.up stays world Y throughout — screen-up is
|
|
491
|
-
// then determined by lookAt projecting (0,1,0) onto perp(look),
|
|
492
|
-
// which falls along the horizontal component of `-look`.
|
|
493
|
-
//
|
|
494
|
-
// The 4 rotation cycles select which compass direction appears at
|
|
495
|
-
// the top of the screen by varying the small horizontal offset
|
|
496
|
-
// (theta in the spherical math).
|
|
497
|
-
// rotation 0 → camera slightly to +Z of center → screen-up = +Z
|
|
498
|
-
// rotation 1 → camera slightly to +X → screen-up = +X
|
|
499
|
-
// rotation 2 → camera slightly to -Z → screen-up = -Z
|
|
500
|
-
// rotation 3 → camera slightly to -X → screen-up = -X
|
|
501
|
-
// Building rotation is applied as the same Y-axis rotation that
|
|
502
|
-
// setPresetView would have used to remap the legacy up vector.
|
|
503
|
-
const poleOffset = Math.sin(0.01) * distance; // ~0.6° tilt
|
|
504
|
-
const verticalOffset = Math.cos(0.01) * distance;
|
|
505
|
-
const thetaPerRotation = [0, Math.PI / 2, Math.PI, -Math.PI / 2];
|
|
506
|
-
const thetaWorld = thetaPerRotation[this.presetViewRotation] + (buildingRotation ?? 0);
|
|
507
|
-
endPos = {
|
|
508
|
-
x: center.x + poleOffset * Math.sin(thetaWorld),
|
|
509
|
-
y: center.y + verticalOffset,
|
|
510
|
-
z: center.z + poleOffset * Math.cos(thetaWorld),
|
|
511
|
-
};
|
|
512
|
-
upVector = { x: 0, y: 1, z: 0 };
|
|
513
|
-
break;
|
|
514
|
-
}
|
|
515
|
-
case 'bottom': {
|
|
516
|
-
// Bottom view: mirror of top — phi = π − MIN_PHI.
|
|
517
|
-
const poleOffset = Math.sin(0.01) * distance;
|
|
518
|
-
const verticalOffset = Math.cos(0.01) * distance;
|
|
519
|
-
const thetaPerRotation = [Math.PI, Math.PI / 2, 0, -Math.PI / 2];
|
|
520
|
-
const thetaWorld = thetaPerRotation[this.presetViewRotation] + (buildingRotation ?? 0);
|
|
521
|
-
endPos = {
|
|
522
|
-
x: center.x + poleOffset * Math.sin(thetaWorld),
|
|
523
|
-
y: center.y - verticalOffset,
|
|
524
|
-
z: center.z + poleOffset * Math.cos(thetaWorld),
|
|
525
|
-
};
|
|
526
|
-
upVector = { x: 0, y: 1, z: 0 };
|
|
527
|
-
break;
|
|
528
|
-
}
|
|
529
|
-
case 'front':
|
|
530
|
-
// Front view: from +Z looking at model
|
|
531
|
-
// Rotate camera position around Y axis by building rotation
|
|
532
|
-
// Standard rotation: x' = x*cos - z*sin, z' = x*sin + z*cos
|
|
533
|
-
// For +Z direction (0,0,1): x' = -sin, z' = cos
|
|
534
|
-
// But we need to look at building's front, so use negative rotation
|
|
535
|
-
endPos = {
|
|
536
|
-
x: center.x + sinR * distance,
|
|
537
|
-
y: center.y,
|
|
538
|
-
z: center.z + cosR * distance,
|
|
539
|
-
};
|
|
540
|
-
upVector = { x: 0, y: 1, z: 0 }; // Y-up
|
|
541
|
-
break;
|
|
542
|
-
case 'back':
|
|
543
|
-
// Back view: from -Z looking at model
|
|
544
|
-
// For -Z direction (0,0,-1) rotated: x' = sin, z' = -cos
|
|
545
|
-
endPos = {
|
|
546
|
-
x: center.x - sinR * distance,
|
|
547
|
-
y: center.y,
|
|
548
|
-
z: center.z - cosR * distance,
|
|
549
|
-
};
|
|
550
|
-
upVector = { x: 0, y: 1, z: 0 }; // Y-up
|
|
551
|
-
break;
|
|
552
|
-
case 'left':
|
|
553
|
-
// Left view: from -X looking at model
|
|
554
|
-
// For -X direction (-1,0,0) rotated: x' = -cos, z' = sin
|
|
555
|
-
endPos = {
|
|
556
|
-
x: center.x - cosR * distance,
|
|
557
|
-
y: center.y,
|
|
558
|
-
z: center.z + sinR * distance,
|
|
559
|
-
};
|
|
560
|
-
upVector = { x: 0, y: 1, z: 0 }; // Y-up
|
|
561
|
-
break;
|
|
562
|
-
case 'right':
|
|
563
|
-
// Right view: from +X looking at model
|
|
564
|
-
// For +X direction (1,0,0) rotated: x' = cos, z' = -sin
|
|
565
|
-
endPos = {
|
|
566
|
-
x: center.x + cosR * distance,
|
|
567
|
-
y: center.y,
|
|
568
|
-
z: center.z - sinR * distance,
|
|
569
|
-
};
|
|
570
|
-
upVector = { x: 0, y: 1, z: 0 }; // Y-up
|
|
571
|
-
break;
|
|
572
|
-
}
|
|
573
|
-
this.animateToWithUp(endPos, endTarget, upVector, 300);
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Get current bounds estimate (simplified - in production would use scene bounds)
|
|
577
|
-
*/
|
|
578
|
-
getCurrentBounds() {
|
|
579
|
-
// Estimate bounds from camera distance
|
|
580
|
-
const dir = {
|
|
581
|
-
x: this.state.camera.position.x - this.state.camera.target.x,
|
|
582
|
-
y: this.state.camera.position.y - this.state.camera.target.y,
|
|
583
|
-
z: this.state.camera.position.z - this.state.camera.target.z,
|
|
584
|
-
};
|
|
585
|
-
const distance = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
586
|
-
const size = distance / 2;
|
|
587
|
-
return {
|
|
588
|
-
min: {
|
|
589
|
-
x: this.state.camera.target.x - size,
|
|
590
|
-
y: this.state.camera.target.y - size,
|
|
591
|
-
z: this.state.camera.target.z - size,
|
|
592
|
-
},
|
|
593
|
-
max: {
|
|
594
|
-
x: this.state.camera.target.x + size,
|
|
595
|
-
y: this.state.camera.target.y + size,
|
|
596
|
-
z: this.state.camera.target.z + size,
|
|
597
|
-
},
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
313
|
/**
|
|
601
314
|
* Reset velocity (stop inertia)
|
|
602
315
|
*/
|