@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
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Finiteness predicates shared by every camera sub-system.
3
+ *
4
+ * They live in one module rather than next to their first caller because the
5
+ * same three mistakes keep being made independently in `camera.ts`,
6
+ * `camera-controls.ts`, `camera-animation.ts` and `camera-projection.ts`:
7
+ *
8
+ * - **A magnitude comparison is not a finiteness check.** Every comparison
9
+ * involving NaN is false, so `dist < eps` does not reject NaN — it waves it
10
+ * through the guard that looks like it is rejecting it (#2441).
11
+ * - **A lower-bound floor rejects NaN but admits Infinity.** `len > 1e-10` is
12
+ * false for NaN and *true* for Infinity, and scaling infinite components by
13
+ * `1 / Infinity` is `Infinity * 0` — NaN. A guard written as
14
+ * `!Number.isNaN(...)` would pass every existing test and still be wrong.
15
+ * - **`Math.max`/`Math.min` are NaN-transparent.** `Math.max(0.01, NaN)` is
16
+ * `NaN`, not the floor, so a clamp forwards a malformed value rather than
17
+ * absorbing it.
18
+ *
19
+ * Every predicate here therefore keys on `Number.isFinite`, which is the only
20
+ * test that rejects NaN and both infinities together.
21
+ */
22
+ import type { Vec3 } from './types.js';
23
+ /**
24
+ * Smallest orthographic half-height the projection matrix is ever built from.
25
+ * Matches the floor the orthographic zoom clamps to in `CameraControls`.
26
+ */
27
+ export declare const MIN_ORTHO_SIZE = 0.01;
28
+ /** Default orthographic half-height, in world units, and the neutral fallback. */
29
+ export declare const DEFAULT_ORTHO_SIZE = 50;
30
+ /**
31
+ * Is `dist` a usable camera-to-target radius — finite, and at least `min` away
32
+ * from zero so dividing by it means something?
33
+ *
34
+ * The navigation gestures all opened with a bare `dist < eps` early return.
35
+ * That is a magnitude test, not a finiteness test, so a malformed pose walked
36
+ * straight past it (#2441). Past the guard, the gesture writes its result into
37
+ * `camera.position` *and* `camera.target`, so a single non-finite coordinate
38
+ * (a BCF viewpoint restored from a file reaches the public setters
39
+ * unvalidated) spreads across all six on the first drag.
40
+ *
41
+ * Callers keep their own fallback: there is no single substitute distance that
42
+ * is right for all of them, which is also why `Camera.getDistance()` reports
43
+ * the pose verbatim rather than sanitizing it centrally.
44
+ */
45
+ export declare function isUsableDistance(dist: number, min: number): boolean;
46
+ /**
47
+ * Is `up` a usable screen-up reference — every component finite?
48
+ *
49
+ * `up` is the third vector of the pose, and it reaches the camera exactly the
50
+ * way `position` and `target` do: BCF stores `CameraUpVector`, `bcfToViewerCoords`
51
+ * only swaps axes, and `applyViewpoint` writes the result straight through
52
+ * (`animateToWithUp` on the animated path — which bypasses `setUp` entirely —
53
+ * or `setUp` on the instant one).
54
+ *
55
+ * Finiteness only, deliberately no magnitude floor. `normalize` already absorbs
56
+ * a zero-length or degenerate `up` by returning `{0,0,0}`, and a *short* `up` is
57
+ * a valid input whose direction is perfectly well defined — `MathUtils.lookAt`
58
+ * documents the same thing and keys its own degeneracy test on the angle rather
59
+ * than a length for exactly this reason. A floor here would silently drop the
60
+ * cursor anchor for poses that navigate correctly today.
61
+ *
62
+ * The floor `normalize` used to have was a *lower* bound, which is what left
63
+ * the gap: `len > 1e-10` is false for NaN (so a NaN `up` degraded safely to
64
+ * the zero vector) but **true for Infinity**, and scaling infinite components
65
+ * by `1 / Infinity` is `Infinity * 0` — NaN. Measured, not reasoned: `up`
66
+ * `(Infinity, 1, 0)` on a finite pose came back from one cursor-anchored zoom
67
+ * with position AND target `(NaN, NaN, NaN)`, in both projection modes (#2441).
68
+ * Both copies of `normalize` now key that floor on `Number.isFinite` too
69
+ * (#2479), so an unusable `up` reaches the same zero vector by either route —
70
+ * this predicate says so at the top of the gesture rather than relying on a
71
+ * helper's degradation three lines in, and it is what stops the *pose* being
72
+ * rewritten from a basis that carries no orientation.
73
+ *
74
+ * As with {@link isUsableDistance}, the caller keeps its own fallback and the
75
+ * pose is left exactly as it was found — `getUp()` still reports the malformed
76
+ * vector, so a restored viewpoint is not silently rewritten on its way back out.
77
+ */
78
+ export declare function isUsableUp(up: Vec3): boolean;
79
+ /**
80
+ * Clamp an orthographic half-height, rejecting a non-finite one.
81
+ *
82
+ * `Math.max`/`Math.min` are NaN-transparent, so a clamp alone does not stop a
83
+ * malformed value reaching the projection matrix (#2441). Returns `null` when
84
+ * there is no usable size, so the caller can keep the value it already had
85
+ * rather than substitute an arbitrary one.
86
+ *
87
+ * The `Infinity` rejection is not theoretical: `orthoSize` is scaled in place
88
+ * by the orthographic zoom, so a legitimately huge but finite half-height
89
+ * overflows to `Infinity` after enough zoom-out notches, and
90
+ * `Math.max(MIN_ORTHO_SIZE, Infinity)` is `Infinity`.
91
+ */
92
+ export declare function usableOrthoSize(size: number): number | null;
93
+ /**
94
+ * Are all of `values` finite?
95
+ *
96
+ * The gesture *arguments* — wheel deltas, pointer deltas, walk axes — are a
97
+ * different input class from the pose the gestures already guard (#2473), and
98
+ * they reach the same arithmetic. Measured on the pre-guard code: `orbit(NaN,
99
+ * 0)`, `pan(NaN, 0)`, `zoom(NaN)` and `moveFirstPerson(NaN, 0, 0)` each turned
100
+ * a perfectly finite pose into a non-finite one in a single call, and
101
+ * `orbit(Infinity, 0)` / `pan(Infinity, 0)` / `moveFirstPerson(Infinity, 0, 0)`
102
+ * did the same — the two do **not** behave alike everywhere, so both must be
103
+ * tested. (`zoom` is the exception: its delta is clamped by
104
+ * `Math.min(|delta| * s, MAX_ZOOM_DELTA)`, which absorbs `Infinity` but not
105
+ * `NaN`.)
106
+ */
107
+ export declare function areFiniteNumbers(...values: number[]): boolean;
108
+ /**
109
+ * Is this a usable cursor anchor — a finite cursor position inside a viewport
110
+ * with a real extent?
111
+ *
112
+ * The cursor-anchored zoom divides by the canvas dimensions, so they need
113
+ * `> 0` and not merely "truthy". Truthiness already rejects `0` and `NaN`
114
+ * (both falsy) which is why a collapsed canvas never reached the division, but
115
+ * it accepts `Infinity` and a negative width — the latter mirrors the anchor
116
+ * and drifts the target the wrong way. `Number.isFinite(w) && w > 0` is the
117
+ * check that means what the division needs.
118
+ */
119
+ export declare function isUsableCursorAnchor(cursorX: number, cursorY: number, viewportWidth: number, viewportHeight: number): boolean;
120
+ /**
121
+ * Is this AABB something the camera can be fitted to?
122
+ *
123
+ * Two rejections, and they are different failures:
124
+ *
125
+ * - **Non-finite corners.** Fitting computes `center = (min + max) / 2` and
126
+ * `size = max - min`, and writes both into the pose. `Math.max` picking the
127
+ * largest extent is NaN-transparent, so one bad coordinate reaches
128
+ * `camera.position`, `camera.target` *and* (in orthographic mode)
129
+ * `orthoSize`, which `getOrthoSize()` then hands to a saved viewpoint —
130
+ * persisting the damage past the session (#2461).
131
+ * - **An inverted box** (`max < min` on any axis). That is the empty-AABB
132
+ * sentinel every accumulator in this package starts from (`min = +Infinity,
133
+ * max = -Infinity`) leaking out as if it were a real box, which is what a
134
+ * mesh with no finite vertices produces. Its centre is `NaN` and its size
135
+ * is negative; there is nothing to frame.
136
+ *
137
+ * A *degenerate* box (`max === min` on some or all axes — a flat wall, a
138
+ * single point) is explicitly **not** rejected: it is a legitimate thing to
139
+ * frame and `frameBounds` already centres on it.
140
+ *
141
+ * The answer to an unusable box is to reject the fit and leave the pose alone,
142
+ * not to clamp: there is no meaningful substitute for an infinite or inverted
143
+ * extent, and no previous bounds to fall back to at these entry points, which
144
+ * are stateless. Keeping the pose the user already had is the same policy
145
+ * `setAspect`, `setFOV` and `setOrthoSize` settled on (#2441/#2450).
146
+ */
147
+ export declare function isUsableBounds(min: Vec3, max: Vec3): boolean;
148
+ //# sourceMappingURL=camera-guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera-guards.d.ts","sourceRoot":"","sources":["../src/camera-guards.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC;;;GAGG;AACH,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAE5C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE3D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAK7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,OAAO,CAMT;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAO5D"}
@@ -0,0 +1,150 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Smallest orthographic half-height the projection matrix is ever built from.
6
+ * Matches the floor the orthographic zoom clamps to in `CameraControls`.
7
+ */
8
+ export const MIN_ORTHO_SIZE = 0.01;
9
+ /** Default orthographic half-height, in world units, and the neutral fallback. */
10
+ export const DEFAULT_ORTHO_SIZE = 50;
11
+ /**
12
+ * Is `dist` a usable camera-to-target radius — finite, and at least `min` away
13
+ * from zero so dividing by it means something?
14
+ *
15
+ * The navigation gestures all opened with a bare `dist < eps` early return.
16
+ * That is a magnitude test, not a finiteness test, so a malformed pose walked
17
+ * straight past it (#2441). Past the guard, the gesture writes its result into
18
+ * `camera.position` *and* `camera.target`, so a single non-finite coordinate
19
+ * (a BCF viewpoint restored from a file reaches the public setters
20
+ * unvalidated) spreads across all six on the first drag.
21
+ *
22
+ * Callers keep their own fallback: there is no single substitute distance that
23
+ * is right for all of them, which is also why `Camera.getDistance()` reports
24
+ * the pose verbatim rather than sanitizing it centrally.
25
+ */
26
+ export function isUsableDistance(dist, min) {
27
+ return Number.isFinite(dist) && dist >= min;
28
+ }
29
+ /**
30
+ * Is `up` a usable screen-up reference — every component finite?
31
+ *
32
+ * `up` is the third vector of the pose, and it reaches the camera exactly the
33
+ * way `position` and `target` do: BCF stores `CameraUpVector`, `bcfToViewerCoords`
34
+ * only swaps axes, and `applyViewpoint` writes the result straight through
35
+ * (`animateToWithUp` on the animated path — which bypasses `setUp` entirely —
36
+ * or `setUp` on the instant one).
37
+ *
38
+ * Finiteness only, deliberately no magnitude floor. `normalize` already absorbs
39
+ * a zero-length or degenerate `up` by returning `{0,0,0}`, and a *short* `up` is
40
+ * a valid input whose direction is perfectly well defined — `MathUtils.lookAt`
41
+ * documents the same thing and keys its own degeneracy test on the angle rather
42
+ * than a length for exactly this reason. A floor here would silently drop the
43
+ * cursor anchor for poses that navigate correctly today.
44
+ *
45
+ * The floor `normalize` used to have was a *lower* bound, which is what left
46
+ * the gap: `len > 1e-10` is false for NaN (so a NaN `up` degraded safely to
47
+ * the zero vector) but **true for Infinity**, and scaling infinite components
48
+ * by `1 / Infinity` is `Infinity * 0` — NaN. Measured, not reasoned: `up`
49
+ * `(Infinity, 1, 0)` on a finite pose came back from one cursor-anchored zoom
50
+ * with position AND target `(NaN, NaN, NaN)`, in both projection modes (#2441).
51
+ * Both copies of `normalize` now key that floor on `Number.isFinite` too
52
+ * (#2479), so an unusable `up` reaches the same zero vector by either route —
53
+ * this predicate says so at the top of the gesture rather than relying on a
54
+ * helper's degradation three lines in, and it is what stops the *pose* being
55
+ * rewritten from a basis that carries no orientation.
56
+ *
57
+ * As with {@link isUsableDistance}, the caller keeps its own fallback and the
58
+ * pose is left exactly as it was found — `getUp()` still reports the malformed
59
+ * vector, so a restored viewpoint is not silently rewritten on its way back out.
60
+ */
61
+ export function isUsableUp(up) {
62
+ return Number.isFinite(up.x) && Number.isFinite(up.y) && Number.isFinite(up.z);
63
+ }
64
+ /**
65
+ * Clamp an orthographic half-height, rejecting a non-finite one.
66
+ *
67
+ * `Math.max`/`Math.min` are NaN-transparent, so a clamp alone does not stop a
68
+ * malformed value reaching the projection matrix (#2441). Returns `null` when
69
+ * there is no usable size, so the caller can keep the value it already had
70
+ * rather than substitute an arbitrary one.
71
+ *
72
+ * The `Infinity` rejection is not theoretical: `orthoSize` is scaled in place
73
+ * by the orthographic zoom, so a legitimately huge but finite half-height
74
+ * overflows to `Infinity` after enough zoom-out notches, and
75
+ * `Math.max(MIN_ORTHO_SIZE, Infinity)` is `Infinity`.
76
+ */
77
+ export function usableOrthoSize(size) {
78
+ return Number.isFinite(size) ? Math.max(MIN_ORTHO_SIZE, size) : null;
79
+ }
80
+ /**
81
+ * Are all of `values` finite?
82
+ *
83
+ * The gesture *arguments* — wheel deltas, pointer deltas, walk axes — are a
84
+ * different input class from the pose the gestures already guard (#2473), and
85
+ * they reach the same arithmetic. Measured on the pre-guard code: `orbit(NaN,
86
+ * 0)`, `pan(NaN, 0)`, `zoom(NaN)` and `moveFirstPerson(NaN, 0, 0)` each turned
87
+ * a perfectly finite pose into a non-finite one in a single call, and
88
+ * `orbit(Infinity, 0)` / `pan(Infinity, 0)` / `moveFirstPerson(Infinity, 0, 0)`
89
+ * did the same — the two do **not** behave alike everywhere, so both must be
90
+ * tested. (`zoom` is the exception: its delta is clamped by
91
+ * `Math.min(|delta| * s, MAX_ZOOM_DELTA)`, which absorbs `Infinity` but not
92
+ * `NaN`.)
93
+ */
94
+ export function areFiniteNumbers(...values) {
95
+ for (const v of values) {
96
+ if (!Number.isFinite(v))
97
+ return false;
98
+ }
99
+ return true;
100
+ }
101
+ /**
102
+ * Is this a usable cursor anchor — a finite cursor position inside a viewport
103
+ * with a real extent?
104
+ *
105
+ * The cursor-anchored zoom divides by the canvas dimensions, so they need
106
+ * `> 0` and not merely "truthy". Truthiness already rejects `0` and `NaN`
107
+ * (both falsy) which is why a collapsed canvas never reached the division, but
108
+ * it accepts `Infinity` and a negative width — the latter mirrors the anchor
109
+ * and drifts the target the wrong way. `Number.isFinite(w) && w > 0` is the
110
+ * check that means what the division needs.
111
+ */
112
+ export function isUsableCursorAnchor(cursorX, cursorY, viewportWidth, viewportHeight) {
113
+ return (areFiniteNumbers(cursorX, cursorY, viewportWidth, viewportHeight) &&
114
+ viewportWidth > 0 &&
115
+ viewportHeight > 0);
116
+ }
117
+ /**
118
+ * Is this AABB something the camera can be fitted to?
119
+ *
120
+ * Two rejections, and they are different failures:
121
+ *
122
+ * - **Non-finite corners.** Fitting computes `center = (min + max) / 2` and
123
+ * `size = max - min`, and writes both into the pose. `Math.max` picking the
124
+ * largest extent is NaN-transparent, so one bad coordinate reaches
125
+ * `camera.position`, `camera.target` *and* (in orthographic mode)
126
+ * `orthoSize`, which `getOrthoSize()` then hands to a saved viewpoint —
127
+ * persisting the damage past the session (#2461).
128
+ * - **An inverted box** (`max < min` on any axis). That is the empty-AABB
129
+ * sentinel every accumulator in this package starts from (`min = +Infinity,
130
+ * max = -Infinity`) leaking out as if it were a real box, which is what a
131
+ * mesh with no finite vertices produces. Its centre is `NaN` and its size
132
+ * is negative; there is nothing to frame.
133
+ *
134
+ * A *degenerate* box (`max === min` on some or all axes — a flat wall, a
135
+ * single point) is explicitly **not** rejected: it is a legitimate thing to
136
+ * frame and `frameBounds` already centres on it.
137
+ *
138
+ * The answer to an unusable box is to reject the fit and leave the pose alone,
139
+ * not to clamp: there is no meaningful substitute for an infinite or inverted
140
+ * extent, and no previous bounds to fall back to at these entry points, which
141
+ * are stateless. Keeping the pose the user already had is the same policy
142
+ * `setAspect`, `setFOV` and `setOrthoSize` settled on (#2441/#2450).
143
+ */
144
+ export function isUsableBounds(min, max) {
145
+ return (areFiniteNumbers(min.x, min.y, min.z, max.x, max.y, max.z) &&
146
+ max.x >= min.x &&
147
+ max.y >= min.y &&
148
+ max.z >= min.z);
149
+ }
150
+ //# sourceMappingURL=camera-guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera-guards.js","sourceRoot":"","sources":["../src/camera-guards.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AA0B/D;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC;AAEnC,kFAAkF;AAClF,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,GAAW;IACxD,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,GAAG,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,UAAU,CAAC,EAAQ;IACjC,OAAO,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACjF,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAG,MAAgB;IAClD,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,OAAe,EACf,aAAqB,EACrB,cAAsB;IAEtB,OAAO,CACL,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,cAAc,CAAC;QACjE,aAAa,GAAG,CAAC;QACjB,cAAc,GAAG,CAAC,CACnB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,cAAc,CAAC,GAAS,EAAE,GAAS;IACjD,OAAO,CACL,gBAAgB,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QAC1D,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CACf,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { CameraInternalState } from './camera-state.js';
2
+ export declare function updateCameraMatrices(state: CameraInternalState): void;
3
+ /**
4
+ * Compute tight near/far for orthographic mode by projecting the scene
5
+ * bounding sphere onto the camera view direction.
6
+ *
7
+ * This gives optimal depth precision (minimizing z-fighting) while ensuring
8
+ * no geometry is clipped regardless of camera position or view angle.
9
+ */
10
+ export declare function computeOrthoNearFar(state: CameraInternalState, distance: number): {
11
+ near: number;
12
+ far: number;
13
+ };
14
+ //# sourceMappingURL=camera-matrices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera-matrices.d.ts","sourceRoot":"","sources":["../src/camera-matrices.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAG7D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,mBAAmB,GAAG,IAAI,CA4DrE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,mBAAmB,EAC1B,QAAQ,EAAE,MAAM,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAyE/B"}
@@ -0,0 +1,146 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * View / projection / view-projection matrix derivation.
6
+ *
7
+ * One subject: camera state in, three finite matrices out. Its failure story
8
+ * is the #2441 near/far backstops — a non-finite pose or `orthoSize` must not
9
+ * reach the projection matrix, because `MathUtils.lookAt` scrubs the *view*
10
+ * matrix but nothing scrubs the projection one, and a NaN there leaves the
11
+ * viewport dead while every other reading looks healthy.
12
+ *
13
+ * **Hot path.** `updateCameraMatrices` runs on every frame the camera moves.
14
+ * It takes the shared state object **by reference** and nothing else: do not
15
+ * destructure it into parameters (that allocates an arguments object per
16
+ * frame) and do not widen the parameter to a structural supertype (that
17
+ * invites megamorphic dispatch). The allocation profile is deliberately
18
+ * unchanged from the method this replaced — `lookAt`, `perspectiveReverseZ` /
19
+ * `orthographicReverseZ` and `multiply` each already return a fresh `Mat4`,
20
+ * and the ortho branch already returned a fresh `{near, far}` per frame.
21
+ */
22
+ import { MathUtils } from './math.js';
23
+ import { DEFAULT_ORTHO_SIZE, usableOrthoSize } from './camera-guards.js';
24
+ export function updateCameraMatrices(state) {
25
+ const dx = state.camera.position.x - state.camera.target.x;
26
+ const dy = state.camera.position.y - state.camera.target.y;
27
+ const dz = state.camera.position.z - state.camera.target.z;
28
+ const rawDistance = Math.sqrt(dx * dx + dy * dy + dz * dz);
29
+ // A non-finite position or target — a malformed BCF viewpoint reaches the
30
+ // public setters unvalidated — would put NaN into near/far and from there
31
+ // into the projection matrix, leaving the viewport dead even though
32
+ // `MathUtils.lookAt` scrubs the view matrix (#2441). Fall back to a
33
+ // neutral distance so the projection stays finite; the camera state itself
34
+ // is left as the caller set it.
35
+ const distance = Number.isFinite(rawDistance) ? rawDistance : 1;
36
+ state.viewMatrix = MathUtils.lookAt(state.camera.position, state.camera.target, state.camera.up);
37
+ if (state.projectionMode === 'orthographic') {
38
+ // Orthographic: project scene bounding sphere onto view direction for tight near/far.
39
+ // Tight range maximizes depth precision (less z-fighting) and prevents clipping.
40
+ const nf = computeOrthoNearFar(state, distance);
41
+ state.camera.near = nf.near;
42
+ state.camera.far = nf.far;
43
+ // Backstop for the writers that never see `setOrthoSize`'s clamp: the
44
+ // orthographic zoom scales `orthoSize` in place and the animator
45
+ // interpolates it, so a NaN reaching either (from a malformed pose, or
46
+ // from bounds that were themselves non-finite) would land straight in
47
+ // the projection matrix. Same fallback shape as the distance above.
48
+ //
49
+ // Finiteness alone is not the whole backstop: `Number.isFinite(0)` is
50
+ // true, and a zero — or negative — half-height makes `right - left` and
51
+ // `top - bottom` zero, so `orthographicReverseZ` divides by zero and the
52
+ // matrix comes back with `Infinity` scale terms. That is the same dead
53
+ // viewport as a NaN, reached through a *valid-but-falsy* value rather
54
+ // than an exotic one. `usableOrthoSize` is the single predicate that says
55
+ // both things at once, and it is the same one every writer already goes
56
+ // through — so this backstop now enforces exactly the invariant the
57
+ // writers promise rather than half of it (#2441).
58
+ const h = usableOrthoSize(state.orthoSize) ?? DEFAULT_ORTHO_SIZE;
59
+ const w = h * state.camera.aspect;
60
+ state.projMatrix = MathUtils.orthographicReverseZ(-w, w, -h, h, state.camera.near, state.camera.far);
61
+ }
62
+ else {
63
+ // Perspective: adapt near/far based on camera-to-target distance
64
+ state.camera.near = Math.max(0.01, distance * 0.001);
65
+ state.camera.far = Math.max(distance * 10, 1000);
66
+ state.projMatrix = MathUtils.perspectiveReverseZ(state.camera.fov, state.camera.aspect, state.camera.near, state.camera.far);
67
+ }
68
+ state.viewProjMatrix = MathUtils.multiply(state.projMatrix, state.viewMatrix);
69
+ }
70
+ /**
71
+ * Compute tight near/far for orthographic mode by projecting the scene
72
+ * bounding sphere onto the camera view direction.
73
+ *
74
+ * This gives optimal depth precision (minimizing z-fighting) while ensuring
75
+ * no geometry is clipped regardless of camera position or view angle.
76
+ */
77
+ export function computeOrthoNearFar(state, distance) {
78
+ const bounds = state.sceneBounds;
79
+ if (!bounds) {
80
+ // Fallback: generous range centered on camera
81
+ return { near: -Math.max(distance, 500), far: Math.max(distance, 500) };
82
+ }
83
+ // Scene bounding sphere center and radius
84
+ const cx = (bounds.min.x + bounds.max.x) / 2;
85
+ const cy = (bounds.min.y + bounds.max.y) / 2;
86
+ const cz = (bounds.min.z + bounds.max.z) / 2;
87
+ const ex = bounds.max.x - bounds.min.x;
88
+ const ey = bounds.max.y - bounds.min.y;
89
+ const ez = bounds.max.z - bounds.min.z;
90
+ const radius = Math.sqrt(ex * ex + ey * ey + ez * ez) / 2;
91
+ // View direction (camera looks from position toward target)
92
+ const pos = state.camera.position;
93
+ let vx = state.camera.target.x - pos.x;
94
+ let vy = state.camera.target.y - pos.y;
95
+ let vz = state.camera.target.z - pos.z;
96
+ const vLen = Math.sqrt(vx * vx + vy * vy + vz * vz);
97
+ // Add 10% padding for safety.
98
+ const pad = radius * 0.1 + 1;
99
+ let near;
100
+ let far;
101
+ if (Number.isFinite(vLen) && vLen > 1e-8) {
102
+ vx /= vLen;
103
+ vy /= vLen;
104
+ vz /= vLen;
105
+ // Signed distance from camera to scene center along view direction
106
+ const toCenter = (cx - pos.x) * vx + (cy - pos.y) * vy + (cz - pos.z) * vz;
107
+ // Near/far as distances from camera along view direction.
108
+ // The sphere spans [toCenter - radius, toCenter + radius] along view dir.
109
+ near = toCenter - radius - pad;
110
+ far = toCenter + radius + pad;
111
+ }
112
+ else {
113
+ // No view direction to project onto — position and target coincide, or
114
+ // the pose is non-finite. Skipping the normalization (which is what the
115
+ // bare `vLen > 1e-8` did) left `v` at its raw near-zero value, so
116
+ // `toCenter` collapsed to ~0 and the range came out centred on the
117
+ // *camera* rather than on the scene: a camera parked 1000 units away got
118
+ // `[-(r + pad), +(r + pad)]` and clipped the entire model away, which is
119
+ // a blank viewport reached through a range that is perfectly finite and
120
+ // therefore sails past the non-finite backstop below.
121
+ //
122
+ // There is no honest direction to substitute either: `MathUtils.lookAt`
123
+ // picks its own fallback basis for a degenerate pose, and any axis chosen
124
+ // here could disagree with the one the frame is actually drawn from.
125
+ // Bracket the whole bounding sphere symmetrically instead — that contains
126
+ // the scene whichever way `lookAt` ended up looking.
127
+ const span = Math.sqrt((cx - pos.x) * (cx - pos.x) +
128
+ (cy - pos.y) * (cy - pos.y) +
129
+ (cz - pos.z) * (cz - pos.z)) + radius + pad;
130
+ near = -span;
131
+ far = span;
132
+ }
133
+ // Ensure minimum range for depth precision
134
+ if (far - near < 1) {
135
+ near -= 0.5;
136
+ far += 0.5;
137
+ }
138
+ // Same contract as the perspective branch: a non-finite camera position or
139
+ // scene bound must not reach the projection matrix (#2441).
140
+ if (!Number.isFinite(near) || !Number.isFinite(far)) {
141
+ const fallback = Math.max(Number.isFinite(distance) ? distance : 0, 500);
142
+ return { near: -fallback, far: fallback };
143
+ }
144
+ return { near, far };
145
+ }
146
+ //# sourceMappingURL=camera-matrices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera-matrices.js","sourceRoot":"","sources":["../src/camera-matrices.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Preset views: the ViewCube's six named directions, plus the admission check
3
+ * that decides whether a preset may be applied at all.
4
+ *
5
+ * Split out of `camera-framing.ts`, which owns the *free* framing pickers
6
+ * (point / bounds / extents — "put the camera on this thing, keeping the
7
+ * current direction"). Presets are the opposite subject: the direction is
8
+ * dictated by the named face and the caller's rotation cycle, and the box is
9
+ * whatever the caller supplied or whatever the current pose implies. The two
10
+ * share only box arithmetic, which is why this module imports `centerOf` /
11
+ * `maxExtentOf` rather than owning a second copy.
12
+ *
13
+ * Pure by construction, exactly as `camera-framing.ts` is: everything here
14
+ * reads `CameraInternalState` and returns a target, nothing writes to it or
15
+ * touches the tween. That is what keeps both modules free of a cycle back into
16
+ * `camera-animation.ts`, which is the one that applies the results.
17
+ *
18
+ * **Guard placement.** `resolvePresetBounds` validates the box exactly once,
19
+ * here, and a rejection is `null`. Callers null-check and do nothing else: a
20
+ * second, defensive copy of the same guard in the caller would mean neither
21
+ * copy is load-bearing on its own, and the mutation test that pins this guard
22
+ * would go quiet while still looking green.
23
+ */
24
+ import type { Vec3 } from './types.js';
25
+ import type { CameraInternalState } from './camera-state.js';
26
+ import { type FramingBounds } from './camera-framing.js';
27
+ /**
28
+ * Get current bounds estimate (simplified - in production would use scene bounds).
29
+ */
30
+ export declare function estimateBoundsFromPose(state: CameraInternalState): FramingBounds;
31
+ /**
32
+ * Resolve the box a preset view should fit, from the caller's bounds or the
33
+ * current pose, and reject it if it is not usable.
34
+ *
35
+ * This is the whole admission decision for `setPresetView`, deliberately kept
36
+ * ahead of the rotation-cycling counter: a rejected preset must not advance
37
+ * the cycle, and the caller can only honour that if the verdict arrives before
38
+ * it mutates anything.
39
+ */
40
+ export declare function resolvePresetBounds(state: CameraInternalState, bounds?: FramingBounds): FramingBounds | null;
41
+ /**
42
+ * Pose for a preset view (Y-up coordinate system).
43
+ *
44
+ * `bounds` must already have passed {@link resolvePresetBounds}; `rotation` is
45
+ * the caller's 0-3 cycle position, which is why this takes it rather than
46
+ * owning it — the counter belongs to the animator, which is what remembers
47
+ * that the same face was clicked twice.
48
+ *
49
+ * @param buildingRotation Optional building rotation in radians (from IfcSite placement)
50
+ */
51
+ export declare function presetViewTarget(state: CameraInternalState, view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right', bounds: FramingBounds, rotation: number, buildingRotation?: number): {
52
+ position: Vec3;
53
+ target: Vec3;
54
+ up: Vec3;
55
+ };
56
+ //# sourceMappingURL=camera-preset-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera-preset-view.d.ts","sourceRoot":"","sources":["../src/camera-preset-view.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D,OAAO,EAAyC,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEhG;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,mBAAmB,GAAG,aAAa,CAsBhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,mBAAmB,EAC1B,MAAM,CAAC,EAAE,aAAa,GACrB,aAAa,GAAG,IAAI,CAYtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,mBAAmB,EAC1B,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAC5D,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,CAAC,EAAE,MAAM,GACxB;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,CA0H5C"}