@ifc-lite/renderer 1.41.1 → 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.
Files changed (120) hide show
  1. package/dist/camera-animation.d.ts +20 -32
  2. package/dist/camera-animation.d.ts.map +1 -1
  3. package/dist/camera-animation.js +71 -358
  4. package/dist/camera-animation.js.map +1 -1
  5. package/dist/camera-controls.d.ts +11 -51
  6. package/dist/camera-controls.d.ts.map +1 -1
  7. package/dist/camera-controls.js +140 -12
  8. package/dist/camera-controls.js.map +1 -1
  9. package/dist/camera-first-person.d.ts +52 -0
  10. package/dist/camera-first-person.d.ts.map +1 -0
  11. package/dist/camera-first-person.js +119 -0
  12. package/dist/camera-first-person.js.map +1 -0
  13. package/dist/camera-fit-policy.d.ts.map +1 -1
  14. package/dist/camera-fit-policy.js +18 -7
  15. package/dist/camera-fit-policy.js.map +1 -1
  16. package/dist/camera-framing.d.ts +86 -0
  17. package/dist/camera-framing.d.ts.map +1 -0
  18. package/dist/camera-framing.js +230 -0
  19. package/dist/camera-framing.js.map +1 -0
  20. package/dist/camera-guards.d.ts +148 -0
  21. package/dist/camera-guards.d.ts.map +1 -0
  22. package/dist/camera-guards.js +150 -0
  23. package/dist/camera-guards.js.map +1 -0
  24. package/dist/camera-matrices.d.ts +14 -0
  25. package/dist/camera-matrices.d.ts.map +1 -0
  26. package/dist/camera-matrices.js +146 -0
  27. package/dist/camera-matrices.js.map +1 -0
  28. package/dist/camera-preset-view.d.ts +56 -0
  29. package/dist/camera-preset-view.d.ts.map +1 -0
  30. package/dist/camera-preset-view.js +180 -0
  31. package/dist/camera-preset-view.js.map +1 -0
  32. package/dist/camera-projection.d.ts +1 -1
  33. package/dist/camera-projection.d.ts.map +1 -1
  34. package/dist/camera-projection.js +83 -30
  35. package/dist/camera-projection.js.map +1 -1
  36. package/dist/camera-state.d.ts +62 -0
  37. package/dist/camera-state.d.ts.map +1 -0
  38. package/dist/camera-state.js +5 -0
  39. package/dist/camera-state.js.map +1 -0
  40. package/dist/camera.d.ts +22 -11
  41. package/dist/camera.d.ts.map +1 -1
  42. package/dist/camera.js +110 -85
  43. package/dist/camera.js.map +1 -1
  44. package/dist/deviation/deviation-pipeline.d.ts.map +1 -1
  45. package/dist/deviation/deviation-pipeline.js +21 -4
  46. package/dist/deviation/deviation-pipeline.js.map +1 -1
  47. package/dist/environment.d.ts.map +1 -1
  48. package/dist/environment.js +23 -1
  49. package/dist/environment.js.map +1 -1
  50. package/dist/index.d.ts +333 -43
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +753 -704
  53. package/dist/index.js.map +1 -1
  54. package/dist/math.d.ts +59 -2
  55. package/dist/math.d.ts.map +1 -1
  56. package/dist/math.js +173 -23
  57. package/dist/math.js.map +1 -1
  58. package/dist/model-bounds-tracker.d.ts +96 -0
  59. package/dist/model-bounds-tracker.d.ts.map +1 -0
  60. package/dist/model-bounds-tracker.js +147 -0
  61. package/dist/model-bounds-tracker.js.map +1 -0
  62. package/dist/pointcloud/point-cloud-node.d.ts.map +1 -1
  63. package/dist/pointcloud/point-cloud-node.js +24 -4
  64. package/dist/pointcloud/point-cloud-node.js.map +1 -1
  65. package/dist/render-degradation.d.ts +77 -0
  66. package/dist/render-degradation.d.ts.map +1 -0
  67. package/dist/render-degradation.js +52 -0
  68. package/dist/render-degradation.js.map +1 -0
  69. package/dist/render-section-draw.d.ts +51 -0
  70. package/dist/render-section-draw.d.ts.map +1 -0
  71. package/dist/render-section-draw.js +66 -0
  72. package/dist/render-section-draw.js.map +1 -0
  73. package/dist/render-section-plane.d.ts +128 -0
  74. package/dist/render-section-plane.d.ts.map +1 -0
  75. package/dist/render-section-plane.js +298 -0
  76. package/dist/render-section-plane.js.map +1 -0
  77. package/dist/renderer-overlays.d.ts +144 -0
  78. package/dist/renderer-overlays.d.ts.map +1 -0
  79. package/dist/renderer-overlays.js +257 -0
  80. package/dist/renderer-overlays.js.map +1 -0
  81. package/dist/renderer-symbolic-overlays.d.ts +52 -0
  82. package/dist/renderer-symbolic-overlays.d.ts.map +1 -0
  83. package/dist/renderer-symbolic-overlays.js +115 -0
  84. package/dist/renderer-symbolic-overlays.js.map +1 -0
  85. package/dist/scene-geometry.d.ts +32 -0
  86. package/dist/scene-geometry.d.ts.map +1 -1
  87. package/dist/scene-geometry.js +66 -0
  88. package/dist/scene-geometry.js.map +1 -1
  89. package/dist/scene.d.ts +86 -1
  90. package/dist/scene.d.ts.map +1 -1
  91. package/dist/scene.js +391 -201
  92. package/dist/scene.js.map +1 -1
  93. package/dist/section-2d-lift.d.ts +105 -0
  94. package/dist/section-2d-lift.d.ts.map +1 -0
  95. package/dist/section-2d-lift.js +162 -0
  96. package/dist/section-2d-lift.js.map +1 -0
  97. package/dist/section-2d-line-buffer.d.ts +58 -0
  98. package/dist/section-2d-line-buffer.d.ts.map +1 -0
  99. package/dist/section-2d-line-buffer.js +109 -0
  100. package/dist/section-2d-line-buffer.js.map +1 -0
  101. package/dist/section-2d-overlay.d.ts +43 -75
  102. package/dist/section-2d-overlay.d.ts.map +1 -1
  103. package/dist/section-2d-overlay.js +189 -559
  104. package/dist/section-2d-overlay.js.map +1 -1
  105. package/dist/section-plane-basis.d.ts +22 -3
  106. package/dist/section-plane-basis.d.ts.map +1 -1
  107. package/dist/section-plane-basis.js +51 -10
  108. package/dist/section-plane-basis.js.map +1 -1
  109. package/dist/section-plane.d.ts.map +1 -1
  110. package/dist/section-plane.js +10 -1
  111. package/dist/section-plane.js.map +1 -1
  112. package/dist/shaders/section-2d-overlay.wgsl.d.ts +102 -0
  113. package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -0
  114. package/dist/shaders/section-2d-overlay.wgsl.js +281 -0
  115. package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -0
  116. package/dist/visual-enhancement.d.ts +33 -0
  117. package/dist/visual-enhancement.d.ts.map +1 -0
  118. package/dist/visual-enhancement.js +46 -0
  119. package/dist/visual-enhancement.js.map +1 -0
  120. package/package.json +4 -3
@@ -1,16 +1,25 @@
1
1
  /**
2
- * Camera animation system handling tweened transitions, inertia/momentum,
3
- * preset views, and first-person mode.
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-controls.js';
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, and first-person movement.
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":"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"}
1
+ {"version":3,"file":"camera-animation.d.ts","sourceRoot":"","sources":["../src/camera-animation.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAG7B;;;;GAIG;AACH,qBAAa,cAAc;IAwBvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAzB7B,OAAO,CAAC,QAAQ,CAA2D;IAC3E,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,WAAW,CAAS;IAG5B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,iBAAiB,CAAqB;IAC9C,OAAO,CAAC,oBAAoB,CAAqB;IACjD,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,uBAAuB,CAAuB;IACtD,OAAO,CAAC,qBAAqB,CAAuB;IACpD,OAAO,CAAC,eAAe,CAAwC;IAG/D,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,kBAAkB,CAAK;gBAGZ,KAAK,EAAE,mBAAmB,EAC1B,cAAc,EAAE,MAAM,IAAI,EAC1B,QAAQ,EAAE,cAAc,EACxB,UAAU,EAAE,gBAAgB;IAc/C,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAMtD,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAMtE,eAAe,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI;IAK9C;;OAEG;IACH,mBAAmB,IAAI,IAAI;IAK3B;;;OAGG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAyHnC;;;OAGG;IACG,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,SAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5D;;;OAGG;IACG,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,SAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMhE,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,SAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrE;;;;OAIG;IACH,aAAa,CACX,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAC5D,MAAM,CAAC,EAAE,aAAa,EACtB,gBAAgB,CAAC,EAAE,MAAM,GACxB,IAAI;IAkBP;;OAEG;IACG,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,SAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BpG;;OAEG;IACG,eAAe,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,SAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiChG;;OAEG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACH,WAAW,IAAI,IAAI;IAQnB;;;OAGG;IACH,KAAK,IAAI,IAAI;CAkBd"}
@@ -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, and first-person movement.
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
- this.state.orthoSize = this.animationStartOrthoSize + (this.animationEndOrthoSize - this.animationStartOrthoSize) * t;
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
- this.state.orthoSize = this.animationEndOrthoSize;
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
- // Keep current viewing direction and distance
164
- const dir = {
165
- x: this.state.camera.position.x - this.state.camera.target.x,
166
- y: this.state.camera.position.y - this.state.camera.target.y,
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 center = {
183
- x: (min.x + max.x) / 2,
184
- y: (min.y + max.y) / 2,
185
- z: (min.z + max.z) / 2,
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 center = {
255
- x: (min.x + max.x) / 2,
256
- y: (min.y + max.y) / 2,
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(distance);
270
- // Keep current viewing direction
271
- const dir = {
272
- x: this.state.camera.position.x - this.state.camera.target.x,
273
- y: this.state.camera.position.y - this.state.camera.target.y,
274
- z: this.state.camera.position.z - this.state.camera.target.z,
275
- };
276
- const currentDistance = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
277
- // Normalize direction
278
- if (currentDistance > 1e-10) {
279
- dir.x /= currentDistance;
280
- dir.y /= currentDistance;
281
- dir.z /= currentDistance;
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
- // Fallback direction
285
- dir.x = 0.6;
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
- // New position: center + direction * distance
294
- const endPos = {
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
  */