@ifc-lite/renderer 1.6.1 → 1.8.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/README.md +40 -0
- package/dist/camera-animation.d.ts +108 -0
- package/dist/camera-animation.d.ts.map +1 -0
- package/dist/camera-animation.js +606 -0
- package/dist/camera-animation.js.map +1 -0
- package/dist/camera-controls.d.ts +75 -0
- package/dist/camera-controls.d.ts.map +1 -0
- package/dist/camera-controls.js +239 -0
- package/dist/camera-controls.js.map +1 -0
- package/dist/camera-projection.d.ts +51 -0
- package/dist/camera-projection.d.ts.map +1 -0
- package/dist/camera-projection.js +147 -0
- package/dist/camera-projection.js.map +1 -0
- package/dist/camera.d.ts +33 -45
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +128 -815
- package/dist/camera.js.map +1 -1
- package/dist/geometry-manager.d.ts +99 -0
- package/dist/geometry-manager.d.ts.map +1 -0
- package/dist/geometry-manager.js +387 -0
- package/dist/geometry-manager.js.map +1 -0
- package/dist/index.d.ts +7 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +62 -658
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +6 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +20 -0
- package/dist/math.js.map +1 -1
- package/dist/picking-manager.d.ts +31 -0
- package/dist/picking-manager.d.ts.map +1 -0
- package/dist/picking-manager.js +140 -0
- package/dist/picking-manager.js.map +1 -0
- package/dist/pipeline.d.ts +2 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +42 -0
- package/dist/pipeline.js.map +1 -1
- package/dist/raycast-engine.d.ts +76 -0
- package/dist/raycast-engine.d.ts.map +1 -0
- package/dist/raycast-engine.js +255 -0
- package/dist/raycast-engine.js.map +1 -0
- package/dist/scene.d.ts +26 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +134 -25
- package/dist/scene.js.map +1 -1
- package/package.json +4 -4
package/dist/camera.js
CHANGED
|
@@ -2,68 +2,57 @@
|
|
|
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
4
|
import { MathUtils } from './math.js';
|
|
5
|
+
import { CameraControls } from './camera-controls.js';
|
|
6
|
+
import { CameraAnimator } from './camera-animation.js';
|
|
7
|
+
import { CameraProjection } from './camera-projection.js';
|
|
5
8
|
export class Camera {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// Inertia system
|
|
11
|
-
velocity = { orbit: { x: 0, y: 0 }, pan: { x: 0, y: 0 }, zoom: 0 };
|
|
12
|
-
damping = 0.92; // Inertia factor (0-1), higher = more damping
|
|
13
|
-
minVelocity = 0.001; // Minimum velocity threshold
|
|
14
|
-
// Animation system
|
|
15
|
-
animationStartTime = 0;
|
|
16
|
-
animationDuration = 0;
|
|
17
|
-
animationStartPos = null;
|
|
18
|
-
animationStartTarget = null;
|
|
19
|
-
animationEndPos = null;
|
|
20
|
-
animationEndTarget = null;
|
|
21
|
-
animationStartUp = null;
|
|
22
|
-
animationEndUp = null;
|
|
23
|
-
animationEasing = null;
|
|
24
|
-
// First-person mode
|
|
25
|
-
isFirstPersonMode = false;
|
|
26
|
-
firstPersonSpeed = 0.1;
|
|
27
|
-
// Dynamic orbit pivot (for orbiting around selected element or cursor point)
|
|
28
|
-
orbitPivot = null;
|
|
29
|
-
// Track preset view for rotation cycling (clicking same view rotates 90°)
|
|
30
|
-
lastPresetView = null;
|
|
31
|
-
presetViewRotation = 0; // 0, 1, 2, 3 = 0°, 90°, 180°, 270°
|
|
9
|
+
state;
|
|
10
|
+
controls;
|
|
11
|
+
animator;
|
|
12
|
+
projection;
|
|
32
13
|
constructor() {
|
|
33
14
|
// Geometry is converted from IFC Z-up to WebGL Y-up during import
|
|
34
|
-
this.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
15
|
+
this.state = {
|
|
16
|
+
camera: {
|
|
17
|
+
position: { x: 50, y: 50, z: 100 },
|
|
18
|
+
target: { x: 0, y: 0, z: 0 },
|
|
19
|
+
up: { x: 0, y: 1, z: 0 }, // Y-up (standard WebGL)
|
|
20
|
+
fov: Math.PI / 4,
|
|
21
|
+
aspect: 1,
|
|
22
|
+
near: 0.1,
|
|
23
|
+
far: 100000, // Increased default far plane for large models
|
|
24
|
+
},
|
|
25
|
+
viewMatrix: MathUtils.identity(),
|
|
26
|
+
projMatrix: MathUtils.identity(),
|
|
27
|
+
viewProjMatrix: MathUtils.identity(),
|
|
28
|
+
projectionMode: 'perspective',
|
|
29
|
+
orthoSize: 50, // Default half-height in world units
|
|
42
30
|
};
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
31
|
+
const updateMatrices = () => this.updateMatrices();
|
|
32
|
+
this.controls = new CameraControls(this.state, updateMatrices);
|
|
33
|
+
this.projection = new CameraProjection(this.state, updateMatrices);
|
|
34
|
+
this.animator = new CameraAnimator(this.state, updateMatrices, this.controls, this.projection);
|
|
46
35
|
this.updateMatrices();
|
|
47
36
|
}
|
|
48
37
|
/**
|
|
49
38
|
* Set camera aspect ratio
|
|
50
39
|
*/
|
|
51
40
|
setAspect(aspect) {
|
|
52
|
-
this.camera.aspect = aspect;
|
|
41
|
+
this.state.camera.aspect = aspect;
|
|
53
42
|
this.updateMatrices();
|
|
54
43
|
}
|
|
55
44
|
/**
|
|
56
45
|
* Set camera position
|
|
57
46
|
*/
|
|
58
47
|
setPosition(x, y, z) {
|
|
59
|
-
this.camera.position = { x, y, z };
|
|
48
|
+
this.state.camera.position = { x, y, z };
|
|
60
49
|
this.updateMatrices();
|
|
61
50
|
}
|
|
62
51
|
/**
|
|
63
52
|
* Set camera target
|
|
64
53
|
*/
|
|
65
54
|
setTarget(x, y, z) {
|
|
66
|
-
this.camera.target = { x, y, z };
|
|
55
|
+
this.state.camera.target = { x, y, z };
|
|
67
56
|
this.updateMatrices();
|
|
68
57
|
}
|
|
69
58
|
/**
|
|
@@ -71,130 +60,41 @@ export class Camera {
|
|
|
71
60
|
* When set, orbit() will rotate around this point instead of the camera target
|
|
72
61
|
*/
|
|
73
62
|
setOrbitPivot(pivot) {
|
|
74
|
-
this.
|
|
63
|
+
this.controls.setOrbitPivot(pivot);
|
|
75
64
|
}
|
|
76
65
|
/**
|
|
77
66
|
* Get current orbit pivot (returns temporary pivot if set, otherwise target)
|
|
78
67
|
*/
|
|
79
68
|
getOrbitPivot() {
|
|
80
|
-
return this.
|
|
69
|
+
return this.controls.getOrbitPivot();
|
|
81
70
|
}
|
|
82
71
|
/**
|
|
83
72
|
* Check if a temporary orbit pivot is set
|
|
84
73
|
*/
|
|
85
74
|
hasOrbitPivot() {
|
|
86
|
-
return this.
|
|
75
|
+
return this.controls.hasOrbitPivot();
|
|
87
76
|
}
|
|
88
77
|
/**
|
|
89
78
|
* Orbit around target or pivot (Y-up coordinate system)
|
|
90
79
|
* If an orbit pivot is set, orbits around that point and moves target along
|
|
91
80
|
*/
|
|
92
81
|
orbit(deltaX, deltaY, addVelocity = false) {
|
|
93
|
-
|
|
94
|
-
this.
|
|
95
|
-
// Reset preset view tracking when user orbits
|
|
96
|
-
this.lastPresetView = null;
|
|
97
|
-
this.presetViewRotation = 0;
|
|
98
|
-
// Invert controls: mouse movement direction = model rotation direction
|
|
99
|
-
const dx = -deltaX * 0.01;
|
|
100
|
-
const dy = -deltaY * 0.01;
|
|
101
|
-
// Use orbit pivot if set, otherwise use target
|
|
102
|
-
const pivotPoint = this.orbitPivot || this.camera.target;
|
|
103
|
-
const dir = {
|
|
104
|
-
x: this.camera.position.x - pivotPoint.x,
|
|
105
|
-
y: this.camera.position.y - pivotPoint.y,
|
|
106
|
-
z: this.camera.position.z - pivotPoint.z,
|
|
107
|
-
};
|
|
108
|
-
const distance = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
109
|
-
if (distance < 1e-6)
|
|
110
|
-
return;
|
|
111
|
-
// Y-up coordinate system using standard spherical coordinates
|
|
112
|
-
// theta: horizontal rotation around Y axis
|
|
113
|
-
// phi: vertical angle from Y axis (0 = top, PI = bottom)
|
|
114
|
-
let currentPhi = Math.acos(Math.max(-1, Math.min(1, dir.y / distance)));
|
|
115
|
-
// When at poles (top/bottom view), use a stable theta based on current direction
|
|
116
|
-
// to avoid gimbal lock issues
|
|
117
|
-
let theta;
|
|
118
|
-
const sinPhi = Math.sin(currentPhi);
|
|
119
|
-
if (sinPhi > 0.05) {
|
|
120
|
-
// Normal case - calculate theta from horizontal position
|
|
121
|
-
theta = Math.atan2(dir.x, dir.z);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
// At a pole - determine which one and push away
|
|
125
|
-
theta = 0; // Default theta when at pole
|
|
126
|
-
if (currentPhi < Math.PI / 2) {
|
|
127
|
-
// Top pole (phi ≈ 0) - push down
|
|
128
|
-
currentPhi = 0.15;
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
// Bottom pole (phi ≈ π) - push up
|
|
132
|
-
currentPhi = Math.PI - 0.15;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
theta += dx;
|
|
136
|
-
const phi = currentPhi + dy;
|
|
137
|
-
// Clamp phi to prevent gimbal lock (stay away from exact poles)
|
|
138
|
-
const phiClamped = Math.max(0.15, Math.min(Math.PI - 0.15, phi));
|
|
139
|
-
// Calculate new camera position around pivot
|
|
140
|
-
const newPosX = pivotPoint.x + distance * Math.sin(phiClamped) * Math.sin(theta);
|
|
141
|
-
const newPosY = pivotPoint.y + distance * Math.cos(phiClamped);
|
|
142
|
-
const newPosZ = pivotPoint.z + distance * Math.sin(phiClamped) * Math.cos(theta);
|
|
143
|
-
// Update camera position
|
|
144
|
-
this.camera.position.x = newPosX;
|
|
145
|
-
this.camera.position.y = newPosY;
|
|
146
|
-
this.camera.position.z = newPosZ;
|
|
82
|
+
this.animator.resetPresetTracking();
|
|
83
|
+
this.controls.orbit(deltaX, deltaY);
|
|
147
84
|
if (addVelocity) {
|
|
148
|
-
this.
|
|
149
|
-
this.velocity.orbit.y += deltaY * 0.001;
|
|
85
|
+
this.animator.addOrbitVelocity(deltaX, deltaY);
|
|
150
86
|
}
|
|
151
|
-
this.updateMatrices();
|
|
152
87
|
}
|
|
153
88
|
/**
|
|
154
89
|
* Pan camera (Y-up coordinate system)
|
|
155
90
|
*/
|
|
156
91
|
pan(deltaX, deltaY, addVelocity = false) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
z: this.camera.position.z - this.camera.target.z,
|
|
161
|
-
};
|
|
162
|
-
const distance = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
163
|
-
// Right vector: cross product of direction and up (0,1,0)
|
|
164
|
-
const right = {
|
|
165
|
-
x: -dir.z,
|
|
166
|
-
y: 0,
|
|
167
|
-
z: dir.x,
|
|
168
|
-
};
|
|
169
|
-
const rightLen = Math.sqrt(right.x * right.x + right.z * right.z);
|
|
170
|
-
if (rightLen > 1e-10) {
|
|
171
|
-
right.x /= rightLen;
|
|
172
|
-
right.z /= rightLen;
|
|
173
|
-
}
|
|
174
|
-
// Up vector: cross product of right and direction
|
|
175
|
-
const up = {
|
|
176
|
-
x: (right.z * dir.y - right.y * dir.z),
|
|
177
|
-
y: (right.x * dir.z - right.z * dir.x),
|
|
178
|
-
z: (right.y * dir.x - right.x * dir.y),
|
|
179
|
-
};
|
|
180
|
-
const upLen = Math.sqrt(up.x * up.x + up.y * up.y + up.z * up.z);
|
|
181
|
-
if (upLen > 1e-10) {
|
|
182
|
-
up.x /= upLen;
|
|
183
|
-
up.y /= upLen;
|
|
184
|
-
up.z /= upLen;
|
|
185
|
-
}
|
|
186
|
-
const panSpeed = distance * 0.001;
|
|
187
|
-
this.camera.target.x += (right.x * deltaX + up.x * deltaY) * panSpeed;
|
|
188
|
-
this.camera.target.y += (right.y * deltaX + up.y * deltaY) * panSpeed;
|
|
189
|
-
this.camera.target.z += (right.z * deltaX + up.z * deltaY) * panSpeed;
|
|
190
|
-
this.camera.position.x += (right.x * deltaX + up.x * deltaY) * panSpeed;
|
|
191
|
-
this.camera.position.y += (right.y * deltaX + up.y * deltaY) * panSpeed;
|
|
192
|
-
this.camera.position.z += (right.z * deltaX + up.z * deltaY) * panSpeed;
|
|
92
|
+
// Pan speed depends on distance; compute before pan (pan preserves distance)
|
|
93
|
+
const panSpeed = this.getDistance() * 0.001;
|
|
94
|
+
this.controls.pan(deltaX, deltaY);
|
|
193
95
|
if (addVelocity) {
|
|
194
|
-
this.
|
|
195
|
-
this.velocity.pan.y += deltaY * panSpeed * 0.1;
|
|
96
|
+
this.animator.addPanVelocity(deltaX, deltaY, panSpeed);
|
|
196
97
|
}
|
|
197
|
-
this.updateMatrices();
|
|
198
98
|
}
|
|
199
99
|
/**
|
|
200
100
|
* Zoom camera towards mouse position
|
|
@@ -206,74 +106,11 @@ export class Camera {
|
|
|
206
106
|
* @param canvasHeight - Canvas height
|
|
207
107
|
*/
|
|
208
108
|
zoom(delta, addVelocity = false, mouseX, mouseY, canvasWidth, canvasHeight) {
|
|
209
|
-
|
|
210
|
-
x: this.camera.position.x - this.camera.target.x,
|
|
211
|
-
y: this.camera.position.y - this.camera.target.y,
|
|
212
|
-
z: this.camera.position.z - this.camera.target.z,
|
|
213
|
-
};
|
|
214
|
-
const distance = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
215
|
-
// Normalize delta (wheel events can have large values)
|
|
216
|
-
const normalizedDelta = Math.sign(delta) * Math.min(Math.abs(delta) * 0.001, 0.1);
|
|
217
|
-
const zoomFactor = 1 + normalizedDelta;
|
|
218
|
-
// If mouse position provided, zoom towards that point
|
|
219
|
-
if (mouseX !== undefined && mouseY !== undefined && canvasWidth && canvasHeight) {
|
|
220
|
-
// Convert mouse to normalized device coordinates (-1 to 1)
|
|
221
|
-
const ndcX = (mouseX / canvasWidth) * 2 - 1;
|
|
222
|
-
const ndcY = 1 - (mouseY / canvasHeight) * 2; // Flip Y
|
|
223
|
-
// Calculate offset from center in world space
|
|
224
|
-
// Use the camera's right and up vectors
|
|
225
|
-
const forward = {
|
|
226
|
-
x: -dir.x / distance,
|
|
227
|
-
y: -dir.y / distance,
|
|
228
|
-
z: -dir.z / distance,
|
|
229
|
-
};
|
|
230
|
-
// Right = forward × up
|
|
231
|
-
const up = this.camera.up;
|
|
232
|
-
const right = {
|
|
233
|
-
x: forward.y * up.z - forward.z * up.y,
|
|
234
|
-
y: forward.z * up.x - forward.x * up.z,
|
|
235
|
-
z: forward.x * up.y - forward.y * up.x,
|
|
236
|
-
};
|
|
237
|
-
const rightLen = Math.sqrt(right.x * right.x + right.y * right.y + right.z * right.z);
|
|
238
|
-
if (rightLen > 1e-10) {
|
|
239
|
-
right.x /= rightLen;
|
|
240
|
-
right.y /= rightLen;
|
|
241
|
-
right.z /= rightLen;
|
|
242
|
-
}
|
|
243
|
-
// Actual up = right × forward
|
|
244
|
-
const actualUp = {
|
|
245
|
-
x: right.y * forward.z - right.z * forward.y,
|
|
246
|
-
y: right.z * forward.x - right.x * forward.z,
|
|
247
|
-
z: right.x * forward.y - right.y * forward.x,
|
|
248
|
-
};
|
|
249
|
-
// Calculate view frustum size at target distance
|
|
250
|
-
const halfHeight = distance * Math.tan(this.camera.fov / 2);
|
|
251
|
-
const halfWidth = halfHeight * this.camera.aspect;
|
|
252
|
-
// World offset from center towards mouse position
|
|
253
|
-
const worldOffsetX = ndcX * halfWidth;
|
|
254
|
-
const worldOffsetY = ndcY * halfHeight;
|
|
255
|
-
// Point in world space that mouse is pointing at (on the target plane)
|
|
256
|
-
const mouseWorldPoint = {
|
|
257
|
-
x: this.camera.target.x + right.x * worldOffsetX + actualUp.x * worldOffsetY,
|
|
258
|
-
y: this.camera.target.y + right.y * worldOffsetX + actualUp.y * worldOffsetY,
|
|
259
|
-
z: this.camera.target.z + right.z * worldOffsetX + actualUp.z * worldOffsetY,
|
|
260
|
-
};
|
|
261
|
-
// Move both camera and target towards mouse point while zooming
|
|
262
|
-
const moveAmount = (1 - zoomFactor); // Negative when zooming in
|
|
263
|
-
this.camera.target.x += (mouseWorldPoint.x - this.camera.target.x) * moveAmount;
|
|
264
|
-
this.camera.target.y += (mouseWorldPoint.y - this.camera.target.y) * moveAmount;
|
|
265
|
-
this.camera.target.z += (mouseWorldPoint.z - this.camera.target.z) * moveAmount;
|
|
266
|
-
}
|
|
267
|
-
// Apply zoom (scale distance)
|
|
268
|
-
const newDistance = Math.max(0.1, distance * zoomFactor);
|
|
269
|
-
const scale = newDistance / distance;
|
|
270
|
-
this.camera.position.x = this.camera.target.x + dir.x * scale;
|
|
271
|
-
this.camera.position.y = this.camera.target.y + dir.y * scale;
|
|
272
|
-
this.camera.position.z = this.camera.target.z + dir.z * scale;
|
|
109
|
+
this.controls.zoom(delta, mouseX, mouseY, canvasWidth, canvasHeight);
|
|
273
110
|
if (addVelocity) {
|
|
274
|
-
|
|
111
|
+
const normalizedDelta = Math.sign(delta) * Math.min(Math.abs(delta) * 0.001, 0.1);
|
|
112
|
+
this.animator.addZoomVelocity(normalizedDelta);
|
|
275
113
|
}
|
|
276
|
-
this.updateMatrices();
|
|
277
114
|
}
|
|
278
115
|
/**
|
|
279
116
|
* Fit view to bounding box
|
|
@@ -281,619 +118,114 @@ export class Camera {
|
|
|
281
118
|
* Y-up coordinate system: Y is vertical
|
|
282
119
|
*/
|
|
283
120
|
fitToBounds(min, max) {
|
|
284
|
-
|
|
285
|
-
x: (min.x + max.x) / 2,
|
|
286
|
-
y: (min.y + max.y) / 2,
|
|
287
|
-
z: (min.z + max.z) / 2,
|
|
288
|
-
};
|
|
289
|
-
const size = {
|
|
290
|
-
x: max.x - min.x,
|
|
291
|
-
y: max.y - min.y,
|
|
292
|
-
z: max.z - min.z,
|
|
293
|
-
};
|
|
294
|
-
const maxSize = Math.max(size.x, size.y, size.z);
|
|
295
|
-
const distance = maxSize * 2.0;
|
|
296
|
-
this.camera.target = center;
|
|
297
|
-
// Southeast isometric view for Y-up:
|
|
298
|
-
// Position camera above and to the front-right of the model
|
|
299
|
-
this.camera.position = {
|
|
300
|
-
x: center.x + distance * 0.6, // Right
|
|
301
|
-
y: center.y + distance * 0.5, // Above
|
|
302
|
-
z: center.z + distance * 0.6, // Front
|
|
303
|
-
};
|
|
304
|
-
// Dynamic near/far plane calculation to prevent Z-fighting
|
|
305
|
-
// Keep near/far ratio under 10000:1 for better depth precision
|
|
306
|
-
const optimalNear = Math.max(0.01, distance * 0.001); // 0.1% of distance
|
|
307
|
-
const optimalFar = distance * 10; // 10x distance for safety margin
|
|
308
|
-
// Ensure ratio is reasonable (max 10000:1)
|
|
309
|
-
const maxRatio = 10000;
|
|
310
|
-
if (optimalFar / optimalNear > maxRatio) {
|
|
311
|
-
// Adjust far plane to maintain ratio
|
|
312
|
-
this.camera.far = optimalNear * maxRatio;
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
this.camera.far = optimalFar;
|
|
316
|
-
}
|
|
317
|
-
this.camera.near = optimalNear;
|
|
318
|
-
this.updateMatrices();
|
|
121
|
+
this.projection.fitToBounds(min, max);
|
|
319
122
|
}
|
|
320
123
|
/**
|
|
321
124
|
* Update camera animation and inertia
|
|
322
125
|
* Returns true if camera is still animating
|
|
323
126
|
*/
|
|
324
|
-
update(
|
|
325
|
-
|
|
326
|
-
void _deltaTime;
|
|
327
|
-
let isAnimating = false;
|
|
328
|
-
// Handle animation
|
|
329
|
-
if (this.animationStartTime > 0 && this.animationDuration > 0) {
|
|
330
|
-
const elapsed = Date.now() - this.animationStartTime;
|
|
331
|
-
const progress = Math.min(elapsed / this.animationDuration, 1);
|
|
332
|
-
if (progress < 1 && this.animationStartPos && this.animationEndPos &&
|
|
333
|
-
this.animationStartTarget && this.animationEndTarget && this.animationEasing) {
|
|
334
|
-
const t = this.animationEasing(progress);
|
|
335
|
-
this.camera.position.x = this.animationStartPos.x + (this.animationEndPos.x - this.animationStartPos.x) * t;
|
|
336
|
-
this.camera.position.y = this.animationStartPos.y + (this.animationEndPos.y - this.animationStartPos.y) * t;
|
|
337
|
-
this.camera.position.z = this.animationStartPos.z + (this.animationEndPos.z - this.animationStartPos.z) * t;
|
|
338
|
-
this.camera.target.x = this.animationStartTarget.x + (this.animationEndTarget.x - this.animationStartTarget.x) * t;
|
|
339
|
-
this.camera.target.y = this.animationStartTarget.y + (this.animationEndTarget.y - this.animationStartTarget.y) * t;
|
|
340
|
-
this.camera.target.z = this.animationStartTarget.z + (this.animationEndTarget.z - this.animationStartTarget.z) * t;
|
|
341
|
-
// Interpolate up vector if animating with up
|
|
342
|
-
if (this.animationStartUp && this.animationEndUp) {
|
|
343
|
-
// SLERP-like interpolation for up vector (normalized lerp)
|
|
344
|
-
let upX = this.animationStartUp.x + (this.animationEndUp.x - this.animationStartUp.x) * t;
|
|
345
|
-
let upY = this.animationStartUp.y + (this.animationEndUp.y - this.animationStartUp.y) * t;
|
|
346
|
-
let upZ = this.animationStartUp.z + (this.animationEndUp.z - this.animationStartUp.z) * t;
|
|
347
|
-
// Normalize
|
|
348
|
-
const len = Math.sqrt(upX * upX + upY * upY + upZ * upZ);
|
|
349
|
-
if (len > 0.0001) {
|
|
350
|
-
this.camera.up.x = upX / len;
|
|
351
|
-
this.camera.up.y = upY / len;
|
|
352
|
-
this.camera.up.z = upZ / len;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
this.updateMatrices();
|
|
356
|
-
isAnimating = true;
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
// Animation complete - set final values
|
|
360
|
-
if (this.animationEndPos) {
|
|
361
|
-
this.camera.position.x = this.animationEndPos.x;
|
|
362
|
-
this.camera.position.y = this.animationEndPos.y;
|
|
363
|
-
this.camera.position.z = this.animationEndPos.z;
|
|
364
|
-
}
|
|
365
|
-
if (this.animationEndTarget) {
|
|
366
|
-
this.camera.target.x = this.animationEndTarget.x;
|
|
367
|
-
this.camera.target.y = this.animationEndTarget.y;
|
|
368
|
-
this.camera.target.z = this.animationEndTarget.z;
|
|
369
|
-
}
|
|
370
|
-
if (this.animationEndUp) {
|
|
371
|
-
this.camera.up.x = this.animationEndUp.x;
|
|
372
|
-
this.camera.up.y = this.animationEndUp.y;
|
|
373
|
-
this.camera.up.z = this.animationEndUp.z;
|
|
374
|
-
}
|
|
375
|
-
this.updateMatrices();
|
|
376
|
-
this.animationStartTime = 0;
|
|
377
|
-
this.animationDuration = 0;
|
|
378
|
-
this.animationStartPos = null;
|
|
379
|
-
this.animationEndPos = null;
|
|
380
|
-
this.animationStartTarget = null;
|
|
381
|
-
this.animationEndTarget = null;
|
|
382
|
-
this.animationStartUp = null;
|
|
383
|
-
this.animationEndUp = null;
|
|
384
|
-
this.animationEasing = null;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
// Apply inertia
|
|
388
|
-
if (Math.abs(this.velocity.orbit.x) > this.minVelocity || Math.abs(this.velocity.orbit.y) > this.minVelocity) {
|
|
389
|
-
this.orbit(this.velocity.orbit.x * 100, this.velocity.orbit.y * 100, false);
|
|
390
|
-
this.velocity.orbit.x *= this.damping;
|
|
391
|
-
this.velocity.orbit.y *= this.damping;
|
|
392
|
-
isAnimating = true;
|
|
393
|
-
}
|
|
394
|
-
if (Math.abs(this.velocity.pan.x) > this.minVelocity || Math.abs(this.velocity.pan.y) > this.minVelocity) {
|
|
395
|
-
this.pan(this.velocity.pan.x * 1000, this.velocity.pan.y * 1000, false);
|
|
396
|
-
this.velocity.pan.x *= this.damping;
|
|
397
|
-
this.velocity.pan.y *= this.damping;
|
|
398
|
-
isAnimating = true;
|
|
399
|
-
}
|
|
400
|
-
if (Math.abs(this.velocity.zoom) > this.minVelocity) {
|
|
401
|
-
this.zoom(this.velocity.zoom * 1000, false);
|
|
402
|
-
this.velocity.zoom *= this.damping;
|
|
403
|
-
isAnimating = true;
|
|
404
|
-
}
|
|
405
|
-
return isAnimating;
|
|
127
|
+
update(deltaTime) {
|
|
128
|
+
return this.animator.update(deltaTime);
|
|
406
129
|
}
|
|
407
130
|
/**
|
|
408
131
|
* Animate camera to fit bounds (southeast isometric view)
|
|
409
132
|
* Y-up coordinate system
|
|
410
133
|
*/
|
|
411
134
|
async zoomToFit(min, max, duration = 500) {
|
|
412
|
-
|
|
413
|
-
x: (min.x + max.x) / 2,
|
|
414
|
-
y: (min.y + max.y) / 2,
|
|
415
|
-
z: (min.z + max.z) / 2,
|
|
416
|
-
};
|
|
417
|
-
const size = {
|
|
418
|
-
x: max.x - min.x,
|
|
419
|
-
y: max.y - min.y,
|
|
420
|
-
z: max.z - min.z,
|
|
421
|
-
};
|
|
422
|
-
const maxSize = Math.max(size.x, size.y, size.z);
|
|
423
|
-
const distance = maxSize * 2.0;
|
|
424
|
-
const endTarget = center;
|
|
425
|
-
// Southeast isometric view for Y-up (same as fitToBounds)
|
|
426
|
-
const endPos = {
|
|
427
|
-
x: center.x + distance * 0.6,
|
|
428
|
-
y: center.y + distance * 0.5,
|
|
429
|
-
z: center.z + distance * 0.6,
|
|
430
|
-
};
|
|
431
|
-
return this.animateTo(endPos, endTarget, duration);
|
|
135
|
+
return this.animator.zoomToFit(min, max, duration);
|
|
432
136
|
}
|
|
433
|
-
/**
|
|
434
|
-
* Zoom to fit bounds WITHOUT changing view direction
|
|
435
|
-
* Just centers on bounds and adjusts distance to fit
|
|
436
|
-
*/
|
|
437
137
|
/**
|
|
438
138
|
* Frame/center view on a point (keeps current distance and direction)
|
|
439
139
|
* Standard CAD "Frame Selection" behavior
|
|
440
140
|
*/
|
|
441
141
|
async framePoint(point, duration = 300) {
|
|
442
|
-
|
|
443
|
-
const dir = {
|
|
444
|
-
x: this.camera.position.x - this.camera.target.x,
|
|
445
|
-
y: this.camera.position.y - this.camera.target.y,
|
|
446
|
-
z: this.camera.position.z - this.camera.target.z,
|
|
447
|
-
};
|
|
448
|
-
// New position: point + current offset
|
|
449
|
-
const endPos = {
|
|
450
|
-
x: point.x + dir.x,
|
|
451
|
-
y: point.y + dir.y,
|
|
452
|
-
z: point.z + dir.z,
|
|
453
|
-
};
|
|
454
|
-
return this.animateTo(endPos, point, duration);
|
|
142
|
+
return this.animator.framePoint(point, duration);
|
|
455
143
|
}
|
|
456
144
|
/**
|
|
457
145
|
* Frame selection - zoom to fit bounds while keeping current view direction
|
|
458
146
|
* This is what "Frame Selection" should do - zoom to fill screen
|
|
459
147
|
*/
|
|
460
148
|
async frameBounds(min, max, duration = 300) {
|
|
461
|
-
|
|
462
|
-
x: (min.x + max.x) / 2,
|
|
463
|
-
y: (min.y + max.y) / 2,
|
|
464
|
-
z: (min.z + max.z) / 2,
|
|
465
|
-
};
|
|
466
|
-
const size = {
|
|
467
|
-
x: max.x - min.x,
|
|
468
|
-
y: max.y - min.y,
|
|
469
|
-
z: max.z - min.z,
|
|
470
|
-
};
|
|
471
|
-
const maxSize = Math.max(size.x, size.y, size.z);
|
|
472
|
-
if (maxSize < 1e-6) {
|
|
473
|
-
// Very small or zero size - just center on it
|
|
474
|
-
return this.framePoint(center, duration);
|
|
475
|
-
}
|
|
476
|
-
// Calculate required distance based on FOV to fit bounds
|
|
477
|
-
const fovFactor = Math.tan(this.camera.fov / 2);
|
|
478
|
-
const distance = (maxSize / 2) / fovFactor * 1.2; // 1.2x padding for nice framing
|
|
479
|
-
// Get current viewing direction from view matrix (more reliable than position-target)
|
|
480
|
-
// View matrix forward is -Z axis in view space
|
|
481
|
-
const viewMatrix = this.viewMatrix.m;
|
|
482
|
-
// Extract forward direction from view matrix (negative Z column, normalized)
|
|
483
|
-
let dir = {
|
|
484
|
-
x: -viewMatrix[8], // -m[2][0] (forward X)
|
|
485
|
-
y: -viewMatrix[9], // -m[2][1] (forward Y)
|
|
486
|
-
z: -viewMatrix[10], // -m[2][2] (forward Z)
|
|
487
|
-
};
|
|
488
|
-
const dirLen = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
489
|
-
// Normalize direction
|
|
490
|
-
if (dirLen > 1e-6) {
|
|
491
|
-
dir.x /= dirLen;
|
|
492
|
-
dir.y /= dirLen;
|
|
493
|
-
dir.z /= dirLen;
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
// Fallback: use position-target if view matrix is invalid
|
|
497
|
-
dir = {
|
|
498
|
-
x: this.camera.position.x - this.camera.target.x,
|
|
499
|
-
y: this.camera.position.y - this.camera.target.y,
|
|
500
|
-
z: this.camera.position.z - this.camera.target.z,
|
|
501
|
-
};
|
|
502
|
-
const fallbackLen = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
503
|
-
if (fallbackLen > 1e-6) {
|
|
504
|
-
dir.x /= fallbackLen;
|
|
505
|
-
dir.y /= fallbackLen;
|
|
506
|
-
dir.z /= fallbackLen;
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
// Last resort: southeast isometric
|
|
510
|
-
dir.x = 0.6;
|
|
511
|
-
dir.y = 0.5;
|
|
512
|
-
dir.z = 0.6;
|
|
513
|
-
const len = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
514
|
-
dir.x /= len;
|
|
515
|
-
dir.y /= len;
|
|
516
|
-
dir.z /= len;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
// New position: center + direction * distance
|
|
520
|
-
const endPos = {
|
|
521
|
-
x: center.x + dir.x * distance,
|
|
522
|
-
y: center.y + dir.y * distance,
|
|
523
|
-
z: center.z + dir.z * distance,
|
|
524
|
-
};
|
|
525
|
-
return this.animateTo(endPos, center, duration);
|
|
149
|
+
return this.animator.frameBounds(min, max, duration);
|
|
526
150
|
}
|
|
527
151
|
async zoomExtent(min, max, duration = 300) {
|
|
528
|
-
|
|
529
|
-
x: (min.x + max.x) / 2,
|
|
530
|
-
y: (min.y + max.y) / 2,
|
|
531
|
-
z: (min.z + max.z) / 2,
|
|
532
|
-
};
|
|
533
|
-
const size = {
|
|
534
|
-
x: max.x - min.x,
|
|
535
|
-
y: max.y - min.y,
|
|
536
|
-
z: max.z - min.z,
|
|
537
|
-
};
|
|
538
|
-
const maxSize = Math.max(size.x, size.y, size.z);
|
|
539
|
-
// Calculate required distance based on FOV
|
|
540
|
-
const fovFactor = Math.tan(this.camera.fov / 2);
|
|
541
|
-
const distance = (maxSize / 2) / fovFactor * 1.5; // 1.5x for padding
|
|
542
|
-
// Update near/far planes dynamically
|
|
543
|
-
this.updateNearFarPlanes(distance);
|
|
544
|
-
// Keep current viewing direction
|
|
545
|
-
const dir = {
|
|
546
|
-
x: this.camera.position.x - this.camera.target.x,
|
|
547
|
-
y: this.camera.position.y - this.camera.target.y,
|
|
548
|
-
z: this.camera.position.z - this.camera.target.z,
|
|
549
|
-
};
|
|
550
|
-
const currentDistance = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
551
|
-
// Normalize direction
|
|
552
|
-
if (currentDistance > 1e-10) {
|
|
553
|
-
dir.x /= currentDistance;
|
|
554
|
-
dir.y /= currentDistance;
|
|
555
|
-
dir.z /= currentDistance;
|
|
556
|
-
}
|
|
557
|
-
else {
|
|
558
|
-
// Fallback direction
|
|
559
|
-
dir.x = 0.6;
|
|
560
|
-
dir.y = 0.5;
|
|
561
|
-
dir.z = 0.6;
|
|
562
|
-
const len = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
563
|
-
dir.x /= len;
|
|
564
|
-
dir.y /= len;
|
|
565
|
-
dir.z /= len;
|
|
566
|
-
}
|
|
567
|
-
// New position: center + direction * distance
|
|
568
|
-
const endPos = {
|
|
569
|
-
x: center.x + dir.x * distance,
|
|
570
|
-
y: center.y + dir.y * distance,
|
|
571
|
-
z: center.z + dir.z * distance,
|
|
572
|
-
};
|
|
573
|
-
return this.animateTo(endPos, center, duration);
|
|
152
|
+
return this.animator.zoomExtent(min, max, duration);
|
|
574
153
|
}
|
|
575
154
|
/**
|
|
576
155
|
* Animate camera to position and target
|
|
577
156
|
*/
|
|
578
157
|
async animateTo(endPos, endTarget, duration = 500) {
|
|
579
|
-
|
|
580
|
-
this.animationStartTarget = { ...this.camera.target };
|
|
581
|
-
this.animationEndPos = endPos;
|
|
582
|
-
this.animationEndTarget = endTarget;
|
|
583
|
-
this.animationStartUp = null;
|
|
584
|
-
this.animationEndUp = null;
|
|
585
|
-
this.animationDuration = duration;
|
|
586
|
-
this.animationStartTime = Date.now();
|
|
587
|
-
this.animationEasing = this.easeOutCubic;
|
|
588
|
-
// Wait for animation to complete
|
|
589
|
-
return new Promise((resolve) => {
|
|
590
|
-
const checkAnimation = () => {
|
|
591
|
-
if (this.animationStartTime === 0) {
|
|
592
|
-
resolve();
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
requestAnimationFrame(checkAnimation);
|
|
596
|
-
}
|
|
597
|
-
};
|
|
598
|
-
checkAnimation();
|
|
599
|
-
});
|
|
158
|
+
return this.animator.animateTo(endPos, endTarget, duration);
|
|
600
159
|
}
|
|
601
160
|
/**
|
|
602
161
|
* Animate camera to position, target, and up vector (for orthogonal preset views)
|
|
603
162
|
*/
|
|
604
163
|
async animateToWithUp(endPos, endTarget, endUp, duration = 500) {
|
|
605
|
-
|
|
606
|
-
this.velocity.orbit.x = 0;
|
|
607
|
-
this.velocity.orbit.y = 0;
|
|
608
|
-
this.velocity.pan.x = 0;
|
|
609
|
-
this.velocity.pan.y = 0;
|
|
610
|
-
this.velocity.zoom = 0;
|
|
611
|
-
this.animationStartPos = { ...this.camera.position };
|
|
612
|
-
this.animationStartTarget = { ...this.camera.target };
|
|
613
|
-
this.animationStartUp = { ...this.camera.up };
|
|
614
|
-
this.animationEndPos = endPos;
|
|
615
|
-
this.animationEndTarget = endTarget;
|
|
616
|
-
this.animationEndUp = endUp;
|
|
617
|
-
this.animationDuration = duration;
|
|
618
|
-
this.animationStartTime = Date.now();
|
|
619
|
-
this.animationEasing = this.easeOutCubic;
|
|
620
|
-
// Wait for animation to complete
|
|
621
|
-
return new Promise((resolve) => {
|
|
622
|
-
const checkAnimation = () => {
|
|
623
|
-
if (this.animationStartTime === 0) {
|
|
624
|
-
resolve();
|
|
625
|
-
}
|
|
626
|
-
else {
|
|
627
|
-
requestAnimationFrame(checkAnimation);
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
checkAnimation();
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Easing function: easeOutCubic
|
|
635
|
-
*/
|
|
636
|
-
easeOutCubic(t) {
|
|
637
|
-
return 1 - Math.pow(1 - t, 3);
|
|
164
|
+
return this.animator.animateToWithUp(endPos, endTarget, endUp, duration);
|
|
638
165
|
}
|
|
639
166
|
/**
|
|
640
167
|
* Set first-person mode
|
|
641
168
|
*/
|
|
642
169
|
enableFirstPersonMode(enabled) {
|
|
643
|
-
this.
|
|
170
|
+
this.animator.enableFirstPersonMode(enabled);
|
|
644
171
|
}
|
|
645
172
|
/**
|
|
646
173
|
* Move in first-person mode (Y-up coordinate system)
|
|
647
174
|
*/
|
|
648
175
|
moveFirstPerson(forward, right, up) {
|
|
649
|
-
|
|
650
|
-
return;
|
|
651
|
-
const dir = {
|
|
652
|
-
x: this.camera.target.x - this.camera.position.x,
|
|
653
|
-
y: this.camera.target.y - this.camera.position.y,
|
|
654
|
-
z: this.camera.target.z - this.camera.position.z,
|
|
655
|
-
};
|
|
656
|
-
const len = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
657
|
-
if (len > 1e-10) {
|
|
658
|
-
dir.x /= len;
|
|
659
|
-
dir.y /= len;
|
|
660
|
-
dir.z /= len;
|
|
661
|
-
}
|
|
662
|
-
// Right vector: cross product of direction and up (0,1,0)
|
|
663
|
-
const rightVec = {
|
|
664
|
-
x: -dir.z,
|
|
665
|
-
y: 0,
|
|
666
|
-
z: dir.x,
|
|
667
|
-
};
|
|
668
|
-
const rightLen = Math.sqrt(rightVec.x * rightVec.x + rightVec.z * rightVec.z);
|
|
669
|
-
if (rightLen > 1e-10) {
|
|
670
|
-
rightVec.x /= rightLen;
|
|
671
|
-
rightVec.z /= rightLen;
|
|
672
|
-
}
|
|
673
|
-
// Up vector: cross product of right and direction
|
|
674
|
-
const upVec = {
|
|
675
|
-
x: (rightVec.z * dir.y - rightVec.y * dir.z),
|
|
676
|
-
y: (rightVec.x * dir.z - rightVec.z * dir.x),
|
|
677
|
-
z: (rightVec.y * dir.x - rightVec.x * dir.y),
|
|
678
|
-
};
|
|
679
|
-
const speed = this.firstPersonSpeed;
|
|
680
|
-
this.camera.position.x += (dir.x * forward + rightVec.x * right + upVec.x * up) * speed;
|
|
681
|
-
this.camera.position.y += (dir.y * forward + rightVec.y * right + upVec.y * up) * speed;
|
|
682
|
-
this.camera.position.z += (dir.z * forward + rightVec.z * right + upVec.z * up) * speed;
|
|
683
|
-
this.camera.target.x += (dir.x * forward + rightVec.x * right + upVec.x * up) * speed;
|
|
684
|
-
this.camera.target.y += (dir.y * forward + rightVec.y * right + upVec.y * up) * speed;
|
|
685
|
-
this.camera.target.z += (dir.z * forward + rightVec.z * right + upVec.z * up) * speed;
|
|
686
|
-
this.updateMatrices();
|
|
176
|
+
this.animator.moveFirstPerson(forward, right, up);
|
|
687
177
|
}
|
|
688
178
|
/**
|
|
689
179
|
* Set preset view with explicit bounds (Y-up coordinate system)
|
|
690
|
-
* Clicking the same view again rotates 90
|
|
180
|
+
* Clicking the same view again rotates 90 degrees around the view axis
|
|
691
181
|
* @param buildingRotation Optional building rotation in radians (from IfcSite placement)
|
|
692
182
|
*/
|
|
693
183
|
setPresetView(view, bounds, buildingRotation) {
|
|
694
|
-
|
|
695
|
-
if (!useBounds) {
|
|
696
|
-
console.warn('[Camera] No bounds available for setPresetView');
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
// Check if clicking the same view again - cycle rotation
|
|
700
|
-
if (this.lastPresetView === view) {
|
|
701
|
-
this.presetViewRotation = (this.presetViewRotation + 1) % 4;
|
|
702
|
-
}
|
|
703
|
-
else {
|
|
704
|
-
this.lastPresetView = view;
|
|
705
|
-
this.presetViewRotation = 0;
|
|
706
|
-
}
|
|
707
|
-
const center = {
|
|
708
|
-
x: (useBounds.min.x + useBounds.max.x) / 2,
|
|
709
|
-
y: (useBounds.min.y + useBounds.max.y) / 2,
|
|
710
|
-
z: (useBounds.min.z + useBounds.max.z) / 2,
|
|
711
|
-
};
|
|
712
|
-
const size = {
|
|
713
|
-
x: useBounds.max.x - useBounds.min.x,
|
|
714
|
-
y: useBounds.max.y - useBounds.min.y,
|
|
715
|
-
z: useBounds.max.z - useBounds.min.z,
|
|
716
|
-
};
|
|
717
|
-
const maxSize = Math.max(size.x, size.y, size.z);
|
|
718
|
-
// Calculate distance based on FOV for proper fit
|
|
719
|
-
const fovFactor = Math.tan(this.camera.fov / 2);
|
|
720
|
-
const distance = (maxSize / 2) / fovFactor * 1.5; // 1.5x for padding
|
|
721
|
-
let endPos;
|
|
722
|
-
const endTarget = center;
|
|
723
|
-
// WebGL uses Y-up coordinate system internally
|
|
724
|
-
// We set both position AND up vector for proper orthogonal views
|
|
725
|
-
let upVector = { x: 0, y: 1, z: 0 }; // Default Y-up
|
|
726
|
-
// Up vector rotation options for top/bottom views (rotate around Y axis)
|
|
727
|
-
// 0: -Z, 1: -X, 2: +Z, 3: +X
|
|
728
|
-
const topUpVectors = [
|
|
729
|
-
{ x: 0, y: 0, z: -1 }, // 0° - North up
|
|
730
|
-
{ x: -1, y: 0, z: 0 }, // 90° - West up
|
|
731
|
-
{ x: 0, y: 0, z: 1 }, // 180° - South up
|
|
732
|
-
{ x: 1, y: 0, z: 0 }, // 270° - East up
|
|
733
|
-
];
|
|
734
|
-
const bottomUpVectors = [
|
|
735
|
-
{ x: 0, y: 0, z: 1 }, // 0° - South up
|
|
736
|
-
{ x: 1, y: 0, z: 0 }, // 90° - East up
|
|
737
|
-
{ x: 0, y: 0, z: -1 }, // 180° - North up
|
|
738
|
-
{ x: -1, y: 0, z: 0 }, // 270° - West up
|
|
739
|
-
];
|
|
740
|
-
// Apply building rotation if present (rotate around Y axis)
|
|
741
|
-
const cosR = buildingRotation !== undefined && buildingRotation !== 0 ? Math.cos(buildingRotation) : 1.0;
|
|
742
|
-
const sinR = buildingRotation !== undefined && buildingRotation !== 0 ? Math.sin(buildingRotation) : 0.0;
|
|
743
|
-
switch (view) {
|
|
744
|
-
case 'top':
|
|
745
|
-
// Top view: looking straight down from above (+Y)
|
|
746
|
-
// Counter-rotate up vector by NEGATIVE building rotation to align with building axes
|
|
747
|
-
const topUp = topUpVectors[this.presetViewRotation];
|
|
748
|
-
endPos = { x: center.x, y: center.y + distance, z: center.z };
|
|
749
|
-
upVector = {
|
|
750
|
-
x: topUp.x * cosR + topUp.z * sinR,
|
|
751
|
-
y: topUp.y,
|
|
752
|
-
z: -topUp.x * sinR + topUp.z * cosR,
|
|
753
|
-
};
|
|
754
|
-
break;
|
|
755
|
-
case 'bottom':
|
|
756
|
-
// Bottom view: looking straight up from below (-Y)
|
|
757
|
-
// Counter-rotate up vector by NEGATIVE building rotation to align with building axes
|
|
758
|
-
const bottomUp = bottomUpVectors[this.presetViewRotation];
|
|
759
|
-
endPos = { x: center.x, y: center.y - distance, z: center.z };
|
|
760
|
-
upVector = {
|
|
761
|
-
x: bottomUp.x * cosR + bottomUp.z * sinR,
|
|
762
|
-
y: bottomUp.y,
|
|
763
|
-
z: -bottomUp.x * sinR + bottomUp.z * cosR,
|
|
764
|
-
};
|
|
765
|
-
break;
|
|
766
|
-
case 'front':
|
|
767
|
-
// Front view: from +Z looking at model
|
|
768
|
-
// Rotate camera position around Y axis by building rotation
|
|
769
|
-
// Standard rotation: x' = x*cos - z*sin, z' = x*sin + z*cos
|
|
770
|
-
// For +Z direction (0,0,1): x' = -sin, z' = cos
|
|
771
|
-
// But we need to look at building's front, so use negative rotation
|
|
772
|
-
endPos = {
|
|
773
|
-
x: center.x + sinR * distance,
|
|
774
|
-
y: center.y,
|
|
775
|
-
z: center.z + cosR * distance,
|
|
776
|
-
};
|
|
777
|
-
upVector = { x: 0, y: 1, z: 0 }; // Y-up
|
|
778
|
-
break;
|
|
779
|
-
case 'back':
|
|
780
|
-
// Back view: from -Z looking at model
|
|
781
|
-
// For -Z direction (0,0,-1) rotated: x' = sin, z' = -cos
|
|
782
|
-
endPos = {
|
|
783
|
-
x: center.x - sinR * distance,
|
|
784
|
-
y: center.y,
|
|
785
|
-
z: center.z - cosR * distance,
|
|
786
|
-
};
|
|
787
|
-
upVector = { x: 0, y: 1, z: 0 }; // Y-up
|
|
788
|
-
break;
|
|
789
|
-
case 'left':
|
|
790
|
-
// Left view: from -X looking at model
|
|
791
|
-
// For -X direction (-1,0,0) rotated: x' = -cos, z' = sin
|
|
792
|
-
endPos = {
|
|
793
|
-
x: center.x - cosR * distance,
|
|
794
|
-
y: center.y,
|
|
795
|
-
z: center.z + sinR * distance,
|
|
796
|
-
};
|
|
797
|
-
upVector = { x: 0, y: 1, z: 0 }; // Y-up
|
|
798
|
-
break;
|
|
799
|
-
case 'right':
|
|
800
|
-
// Right view: from +X looking at model
|
|
801
|
-
// For +X direction (1,0,0) rotated: x' = cos, z' = -sin
|
|
802
|
-
endPos = {
|
|
803
|
-
x: center.x + cosR * distance,
|
|
804
|
-
y: center.y,
|
|
805
|
-
z: center.z - sinR * distance,
|
|
806
|
-
};
|
|
807
|
-
upVector = { x: 0, y: 1, z: 0 }; // Y-up
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
this.animateToWithUp(endPos, endTarget, upVector, 300);
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* Get current bounds estimate (simplified - in production would use scene bounds)
|
|
814
|
-
*/
|
|
815
|
-
getCurrentBounds() {
|
|
816
|
-
// Estimate bounds from camera distance
|
|
817
|
-
const dir = {
|
|
818
|
-
x: this.camera.position.x - this.camera.target.x,
|
|
819
|
-
y: this.camera.position.y - this.camera.target.y,
|
|
820
|
-
z: this.camera.position.z - this.camera.target.z,
|
|
821
|
-
};
|
|
822
|
-
const distance = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
823
|
-
const size = distance / 2;
|
|
824
|
-
return {
|
|
825
|
-
min: {
|
|
826
|
-
x: this.camera.target.x - size,
|
|
827
|
-
y: this.camera.target.y - size,
|
|
828
|
-
z: this.camera.target.z - size,
|
|
829
|
-
},
|
|
830
|
-
max: {
|
|
831
|
-
x: this.camera.target.x + size,
|
|
832
|
-
y: this.camera.target.y + size,
|
|
833
|
-
z: this.camera.target.z + size,
|
|
834
|
-
},
|
|
835
|
-
};
|
|
184
|
+
this.animator.setPresetView(view, bounds, buildingRotation);
|
|
836
185
|
}
|
|
837
186
|
/**
|
|
838
187
|
* Reset velocity (stop inertia)
|
|
839
188
|
*/
|
|
840
189
|
stopInertia() {
|
|
841
|
-
this.
|
|
842
|
-
this.velocity.orbit.y = 0;
|
|
843
|
-
this.velocity.pan.x = 0;
|
|
844
|
-
this.velocity.pan.y = 0;
|
|
845
|
-
this.velocity.zoom = 0;
|
|
190
|
+
this.animator.stopInertia();
|
|
846
191
|
}
|
|
847
192
|
/**
|
|
848
193
|
* Reset camera state (clear orbit pivot, stop inertia, cancel animations)
|
|
849
194
|
* Called when loading a new model to ensure clean state
|
|
850
195
|
*/
|
|
851
196
|
reset() {
|
|
852
|
-
this.
|
|
853
|
-
this.
|
|
854
|
-
// Cancel any ongoing animations
|
|
855
|
-
this.animationStartTime = 0;
|
|
856
|
-
this.animationDuration = 0;
|
|
857
|
-
this.animationStartPos = null;
|
|
858
|
-
this.animationStartTarget = null;
|
|
859
|
-
this.animationEndPos = null;
|
|
860
|
-
this.animationEndTarget = null;
|
|
861
|
-
this.animationStartUp = null;
|
|
862
|
-
this.animationEndUp = null;
|
|
863
|
-
this.animationEasing = null;
|
|
864
|
-
// Reset preset view tracking
|
|
865
|
-
this.lastPresetView = null;
|
|
866
|
-
this.presetViewRotation = 0;
|
|
197
|
+
this.controls.clearOrbitPivot();
|
|
198
|
+
this.animator.reset();
|
|
867
199
|
}
|
|
868
200
|
getViewProjMatrix() {
|
|
869
|
-
return this.viewProjMatrix;
|
|
201
|
+
return this.state.viewProjMatrix;
|
|
870
202
|
}
|
|
871
203
|
getPosition() {
|
|
872
|
-
return { ...this.camera.position };
|
|
204
|
+
return { ...this.state.camera.position };
|
|
873
205
|
}
|
|
874
206
|
getTarget() {
|
|
875
|
-
return { ...this.camera.target };
|
|
207
|
+
return { ...this.state.camera.target };
|
|
876
208
|
}
|
|
877
209
|
/**
|
|
878
210
|
* Get camera up vector
|
|
879
211
|
*/
|
|
880
212
|
getUp() {
|
|
881
|
-
return { ...this.camera.up };
|
|
213
|
+
return { ...this.state.camera.up };
|
|
882
214
|
}
|
|
883
215
|
/**
|
|
884
216
|
* Get camera FOV in radians
|
|
885
217
|
*/
|
|
886
218
|
getFOV() {
|
|
887
|
-
return this.camera.fov;
|
|
219
|
+
return this.state.camera.fov;
|
|
888
220
|
}
|
|
889
221
|
/**
|
|
890
222
|
* Get distance from camera position to target
|
|
891
223
|
*/
|
|
892
224
|
getDistance() {
|
|
893
225
|
const dir = {
|
|
894
|
-
x: this.camera.position.x - this.camera.target.x,
|
|
895
|
-
y: this.camera.position.y - this.camera.target.y,
|
|
896
|
-
z: this.camera.position.z - this.camera.target.z,
|
|
226
|
+
x: this.state.camera.position.x - this.state.camera.target.x,
|
|
227
|
+
y: this.state.camera.position.y - this.state.camera.target.y,
|
|
228
|
+
z: this.state.camera.position.z - this.state.camera.target.z,
|
|
897
229
|
};
|
|
898
230
|
return Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
899
231
|
}
|
|
@@ -905,9 +237,9 @@ export class Camera {
|
|
|
905
237
|
*/
|
|
906
238
|
getRotation() {
|
|
907
239
|
const dir = {
|
|
908
|
-
x: this.camera.position.x - this.camera.target.x,
|
|
909
|
-
y: this.camera.position.y - this.camera.target.y,
|
|
910
|
-
z: this.camera.position.z - this.camera.target.z,
|
|
240
|
+
x: this.state.camera.position.x - this.state.camera.target.x,
|
|
241
|
+
y: this.state.camera.position.y - this.state.camera.target.y,
|
|
242
|
+
z: this.state.camera.position.z - this.state.camera.target.z,
|
|
911
243
|
};
|
|
912
244
|
const distance = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
913
245
|
if (distance < 1e-6)
|
|
@@ -916,9 +248,9 @@ export class Camera {
|
|
|
916
248
|
const elevation = Math.asin(Math.max(-1, Math.min(1, dir.y / distance))) * 180 / Math.PI;
|
|
917
249
|
// Calculate azimuth smoothly using up vector
|
|
918
250
|
// The up vector defines the "screen up" direction, which determines rotation
|
|
919
|
-
const upX = this.camera.up.x;
|
|
920
|
-
const upY = this.camera.up.y;
|
|
921
|
-
const upZ = this.camera.up.z;
|
|
251
|
+
const upX = this.state.camera.up.x;
|
|
252
|
+
const upY = this.state.camera.up.y;
|
|
253
|
+
const upZ = this.state.camera.up.z;
|
|
922
254
|
// Project up vector onto horizontal plane (XZ plane)
|
|
923
255
|
const upLen = Math.sqrt(upX * upX + upZ * upZ);
|
|
924
256
|
let azimuth;
|
|
@@ -936,13 +268,6 @@ export class Camera {
|
|
|
936
268
|
}
|
|
937
269
|
return { azimuth, elevation };
|
|
938
270
|
}
|
|
939
|
-
/**
|
|
940
|
-
* Project a world position to screen coordinates
|
|
941
|
-
* @param worldPos - Position in world space
|
|
942
|
-
* @param canvasWidth - Canvas width in pixels
|
|
943
|
-
* @param canvasHeight - Canvas height in pixels
|
|
944
|
-
* @returns Screen coordinates { x, y } or null if behind camera
|
|
945
|
-
*/
|
|
946
271
|
/**
|
|
947
272
|
* Unproject screen coordinates to a ray in world space
|
|
948
273
|
* @param screenX - X position in screen coordinates
|
|
@@ -952,83 +277,71 @@ export class Camera {
|
|
|
952
277
|
* @returns Ray origin and direction in world space
|
|
953
278
|
*/
|
|
954
279
|
unprojectToRay(screenX, screenY, canvasWidth, canvasHeight) {
|
|
955
|
-
|
|
956
|
-
// Direction is computed through the screen point
|
|
957
|
-
// Convert screen coords to NDC (-1 to 1)
|
|
958
|
-
const ndcX = (screenX / canvasWidth) * 2 - 1;
|
|
959
|
-
const ndcY = 1 - (screenY / canvasHeight) * 2; // Flip Y
|
|
960
|
-
// Invert the view-projection matrix
|
|
961
|
-
const invViewProj = MathUtils.invert(this.viewProjMatrix);
|
|
962
|
-
if (!invViewProj) {
|
|
963
|
-
// Fallback: return ray from camera position towards target
|
|
964
|
-
const dir = MathUtils.normalize({
|
|
965
|
-
x: this.camera.target.x - this.camera.position.x,
|
|
966
|
-
y: this.camera.target.y - this.camera.position.y,
|
|
967
|
-
z: this.camera.target.z - this.camera.position.z,
|
|
968
|
-
});
|
|
969
|
-
return { origin: { ...this.camera.position }, direction: dir };
|
|
970
|
-
}
|
|
971
|
-
// Unproject a point at some depth to get a point on the ray
|
|
972
|
-
// Using z=0.5 (midpoint in Reverse-Z: 1.0=near, 0.0=far) to get a finite point
|
|
973
|
-
const worldPoint = MathUtils.transformPoint(invViewProj, { x: ndcX, y: ndcY, z: 0.5 });
|
|
974
|
-
// Ray origin is camera position, direction is towards unprojected point
|
|
975
|
-
const origin = { ...this.camera.position };
|
|
976
|
-
const direction = MathUtils.normalize({
|
|
977
|
-
x: worldPoint.x - origin.x,
|
|
978
|
-
y: worldPoint.y - origin.y,
|
|
979
|
-
z: worldPoint.z - origin.z,
|
|
980
|
-
});
|
|
981
|
-
return { origin, direction };
|
|
280
|
+
return this.projection.unprojectToRay(screenX, screenY, canvasWidth, canvasHeight);
|
|
982
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Project a world position to screen coordinates
|
|
284
|
+
* @param worldPos - Position in world space
|
|
285
|
+
* @param canvasWidth - Canvas width in pixels
|
|
286
|
+
* @param canvasHeight - Canvas height in pixels
|
|
287
|
+
* @returns Screen coordinates { x, y } or null if behind camera
|
|
288
|
+
*/
|
|
983
289
|
projectToScreen(worldPos, canvasWidth, canvasHeight) {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const ndcY = clipY / clipW;
|
|
998
|
-
const ndcZ = clipZ / clipW;
|
|
999
|
-
// Check if outside clip volume
|
|
1000
|
-
if (ndcZ < -1 || ndcZ > 1) {
|
|
1001
|
-
return null;
|
|
290
|
+
return this.projection.projectToScreen(worldPos, canvasWidth, canvasHeight);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Set projection mode (perspective or orthographic)
|
|
294
|
+
* When switching to orthographic, calculates initial orthoSize from current view.
|
|
295
|
+
*/
|
|
296
|
+
setProjectionMode(mode) {
|
|
297
|
+
if (this.state.projectionMode === mode)
|
|
298
|
+
return;
|
|
299
|
+
if (mode === 'orthographic') {
|
|
300
|
+
// Calculate orthoSize from current perspective view so the model appears the same size
|
|
301
|
+
const distance = this.getDistance();
|
|
302
|
+
this.state.orthoSize = distance * Math.tan(this.state.camera.fov / 2);
|
|
1002
303
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
304
|
+
this.state.projectionMode = mode;
|
|
305
|
+
this.updateMatrices();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Toggle between perspective and orthographic projection
|
|
309
|
+
*/
|
|
310
|
+
toggleProjectionMode() {
|
|
311
|
+
this.setProjectionMode(this.state.projectionMode === 'perspective' ? 'orthographic' : 'perspective');
|
|
1009
312
|
}
|
|
1010
313
|
/**
|
|
1011
|
-
*
|
|
1012
|
-
* Keeps ratio under 10000:1 to prevent Z-fighting
|
|
314
|
+
* Get current projection mode
|
|
1013
315
|
*/
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
316
|
+
getProjectionMode() {
|
|
317
|
+
return this.state.projectionMode;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get orthographic view half-height
|
|
321
|
+
*/
|
|
322
|
+
getOrthoSize() {
|
|
323
|
+
return this.state.orthoSize;
|
|
324
|
+
}
|
|
325
|
+
updateMatrices() {
|
|
326
|
+
// Dynamically adapt near/far planes based on camera-to-target distance.
|
|
327
|
+
// This prevents near-plane clipping when zooming in close to geometry.
|
|
328
|
+
const dx = this.state.camera.position.x - this.state.camera.target.x;
|
|
329
|
+
const dy = this.state.camera.position.y - this.state.camera.target.y;
|
|
330
|
+
const dz = this.state.camera.position.z - this.state.camera.target.z;
|
|
331
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
332
|
+
this.state.camera.near = Math.max(0.01, distance * 0.001);
|
|
333
|
+
this.state.camera.far = Math.max(distance * 10, 1000);
|
|
334
|
+
this.state.viewMatrix = MathUtils.lookAt(this.state.camera.position, this.state.camera.target, this.state.camera.up);
|
|
335
|
+
if (this.state.projectionMode === 'orthographic') {
|
|
336
|
+
const h = this.state.orthoSize;
|
|
337
|
+
const w = h * this.state.camera.aspect;
|
|
338
|
+
this.state.projMatrix = MathUtils.orthographicReverseZ(-w, w, -h, h, this.state.camera.near, this.state.camera.far);
|
|
1021
339
|
}
|
|
1022
340
|
else {
|
|
1023
|
-
|
|
341
|
+
// Use reverse-Z projection for better depth precision
|
|
342
|
+
this.state.projMatrix = MathUtils.perspectiveReverseZ(this.state.camera.fov, this.state.camera.aspect, this.state.camera.near, this.state.camera.far);
|
|
1024
343
|
}
|
|
1025
|
-
this.
|
|
1026
|
-
}
|
|
1027
|
-
updateMatrices() {
|
|
1028
|
-
this.viewMatrix = MathUtils.lookAt(this.camera.position, this.camera.target, this.camera.up);
|
|
1029
|
-
// Use reverse-Z projection for better depth precision
|
|
1030
|
-
this.projMatrix = MathUtils.perspectiveReverseZ(this.camera.fov, this.camera.aspect, this.camera.near, this.camera.far);
|
|
1031
|
-
this.viewProjMatrix = MathUtils.multiply(this.projMatrix, this.viewMatrix);
|
|
344
|
+
this.state.viewProjMatrix = MathUtils.multiply(this.state.projMatrix, this.state.viewMatrix);
|
|
1032
345
|
}
|
|
1033
346
|
}
|
|
1034
347
|
//# sourceMappingURL=camera.js.map
|