@flighthq/camera 0.3.0 → 0.3.1-next.1050.9114695

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/camera.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare function getCamera3DInverseViewProjectionMatrix4(out: Matrix4Like
4
4
  export declare function getCamera3DViewProjectionMatrix4(out: Matrix4Like, camera: Readonly<Camera3D>, aspect: number): void;
5
5
  export declare function setCamera3DAspect(camera: Camera3D, aspect: number): void;
6
6
  export declare function setCamera3DJitter(camera: Camera3D, x: number, y: number): void;
7
+ export declare function setCamera3DViewGuard(guard: ((camera: Readonly<Camera3D>) => void) | null): void;
7
8
  export declare function setCamera3DViewMatrix4FromLookAt(camera: Camera3D, eye: Readonly<Vector3Like>, target: Readonly<Vector3Like>, up: Readonly<Vector3Like>): void;
8
9
  export declare function setCamera3DViewMatrix4FromMatrix4(camera: Camera3D, view: Readonly<Matrix4Like>): void;
9
10
  export declare function updateCamera3DInverseViewProjection(camera: Readonly<Camera3D>, aspect: number): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../src/camera.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAUpG,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,QAAQ,CASxE;AASD,wBAAgB,uCAAuC,CACrD,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAC1B,MAAM,EAAE,MAAM,GACb,OAAO,CAGT;AAQD,wBAAgB,gCAAgC,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAInH;AAQD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAOxE;AAID,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAG9E;AAOD,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC1B,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC7B,EAAE,EAAE,QAAQ,CAAC,WAAW,CAAC,GACxB,IAAI,CAEN;AAID,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAErG;AAMD,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAIvG"}
1
+ {"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../src/camera.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAUpG,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,QAAQ,CASxE;AASD,wBAAgB,uCAAuC,CACrD,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAC1B,MAAM,EAAE,MAAM,GACb,OAAO,CAGT;AAQD,wBAAgB,gCAAgC,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAInH;AAQD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAOxE;AAID,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAG9E;AAMD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAE/F;AAOD,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC1B,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC7B,EAAE,EAAE,QAAQ,CAAC,WAAW,CAAC,GACxB,IAAI,CAEN;AAeD,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAGrG;AAMD,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAIvG"}
package/dist/camera.js CHANGED
@@ -59,6 +59,13 @@ export function setCamera3DJitter(camera, x, y) {
59
59
  camera.jitter.x = x;
60
60
  camera.jitter.y = y;
61
61
  }
62
+ // The seam the guard layer installs into; `null` keeps this module free of message text and of any
63
+ // dependency on @flighthq/log. Installed here rather than at each consumer because the precondition is a
64
+ // fact about the CAMERA — reporting it once where the matrix enters beats one warning per consumer that
65
+ // happens to rely on it.
66
+ export function setCamera3DViewGuard(guard) {
67
+ camera3DViewGuard = guard;
68
+ }
62
69
  // Builds the camera's world->view matrix in place from an eye position, a look-at target, and an
63
70
  // up vector (right-handed look-at). This is the common path for positioning a camera without an
64
71
  // explicit world transform.
@@ -69,8 +76,20 @@ export function setCamera3DViewMatrix4FromLookAt(camera, eye, target, up) {
69
76
  }
70
77
  // Copies a precomputed world->view matrix into the camera in place. Use this when the view matrix
71
78
  // is derived elsewhere (for example, the inverse of a scene node's world transform).
79
+ //
80
+ // ★ PRECONDITION: `view` must be ORTHONORMAL — a rigid placement, position and orientation only. A view
81
+ // matrix is the inverse of a camera's world placement, and a placement does not scale; this is what the
82
+ // term means everywhere (Unity `worldToCameraMatrix`, Unreal's view matrix), not a Flight restriction.
83
+ // Reflections are FINE: an improper orthogonal matrix (det = -1, as a water or mirror camera produces)
84
+ // is still orthogonal, so Rᵀ is still R⁻¹ and every consumer that relies on the transpose stays exact.
85
+ // What is excluded is a SCALED view, which has no coherent meaning as a placement inverse.
86
+ //
87
+ // The precondition is not enforced here — checking it on every assignment would cost every correct
88
+ // caller for the benefit of an incorrect one. `explainCamera3DView` reports it on demand, and
89
+ // `enableCameraGuards` warns on it; consumers such as `getCamera3DPosition` simply rely on it.
72
90
  export function setCamera3DViewMatrix4FromMatrix4(camera, view) {
73
91
  camera.view.m.set(view.m);
92
+ camera3DViewGuard?.(camera);
74
93
  }
75
94
  // Refreshes the camera's required inverse-view-projection cache for the supplied viewport aspect.
76
95
  // Returns true on success or false when projection×view is non-invertible, leaving the prior valid
@@ -100,4 +119,5 @@ function applyCamera3DProjectionJitter(out, x, y) {
100
119
  const __scratchInverse = createMatrix4();
101
120
  const __scratchProjection = createMatrix4();
102
121
  const __scratchViewProjection = createMatrix4();
122
+ let camera3DViewGuard = null;
103
123
  //# sourceMappingURL=camera.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"camera.js","sourceRoot":"","sources":["../src/camera.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"camera.js","sourceRoot":"","sources":["../src/camera.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,8 @@ export * from './camera';
3
3
  export * from './camera2d';
4
4
  export * from './culling';
5
5
  export * from './depth';
6
+ export * from './enableCameraGuards';
7
+ export * from './explainCamera3DView';
6
8
  export * from './frustumCorners';
7
9
  export * from './intersection';
8
10
  export * from './parallax';
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,QAAQ,CAAC"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,QAAQ,CAAC"}
package/dist/contract.js CHANGED
@@ -3,6 +3,8 @@ export * from './camera';
3
3
  export * from './camera2d';
4
4
  export * from './culling';
5
5
  export * from './depth';
6
+ export * from './enableCameraGuards';
7
+ export * from './explainCamera3DView';
6
8
  export * from './frustumCorners';
7
9
  export * from './intersection';
8
10
  export * from './parallax';
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,QAAQ,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,QAAQ,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function areCameraGuardsEnabled(): boolean;
2
+ export declare function disableCameraGuards(): void;
3
+ export declare function enableCameraGuards(): void;
4
+ //# sourceMappingURL=enableCameraGuards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableCameraGuards.d.ts","sourceRoot":"","sources":["../src/enableCameraGuards.ts"],"names":[],"mappings":"AAQA,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAKD,wBAAgB,kBAAkB,IAAI,IAAI,CAIzC"}
@@ -0,0 +1,45 @@
1
+ import { logOnce } from '@flighthq/log/contract';
2
+ import { LogLevel } from '@flighthq/types/contract';
3
+ import { setCamera3DViewGuard } from './camera';
4
+ import { explainCamera3DView } from './explainCamera3DView';
5
+ import { setCamera2DVisibleBoundsGuard } from './visibleBounds';
6
+ export function areCameraGuardsEnabled() {
7
+ return cameraGuardsEnabled;
8
+ }
9
+ export function disableCameraGuards() {
10
+ setCamera3DViewGuard(null);
11
+ setCamera2DVisibleBoundsGuard(null);
12
+ cameraGuardsEnabled = false;
13
+ }
14
+ // Installs opt-in diagnostics for camera inputs that leave a computation with no answer. The core
15
+ // modules stay message-free, so an application that omits this one sheds both the text and
16
+ // @flighthq/log.
17
+ export function enableCameraGuards() {
18
+ setCamera3DViewGuard(warnOnNonOrthonormalCamera3DView);
19
+ setCamera2DVisibleBoundsGuard(warnOnDegenerateCamera2DVisibleBounds);
20
+ cameraGuardsEnabled = true;
21
+ }
22
+ // The unbounded rectangle is a deliberate fail-toward-drawing, not a silent success: nothing is culled,
23
+ // so the frame is correct but pays for every object. That is a performance cliff with no visible
24
+ // symptom, which is exactly the case a guard exists to name.
25
+ function warnOnDegenerateCamera2DVisibleBounds(camera) {
26
+ logOnce(`camera:degenerate-visible-bounds:${camera.zoom}`, LogLevel.Warn, {
27
+ message: 'getCamera2DVisibleBounds: the view matrix has no inverse, so the visible rectangle is unbounded and nothing is culled — a zoom of 0 is the usual cause; set a non-zero zoom.',
28
+ zoom: camera.zoom,
29
+ });
30
+ }
31
+ // Names the CAUSE, not any one consequence: several consumers rely on the orthonormality precondition
32
+ // and warning once per consumer would be noise. A caller that wants to connect this to a specific wrong
33
+ // answer has `explainCamera3DView` for the numbers.
34
+ function warnOnNonOrthonormalCamera3DView(camera) {
35
+ const explanation = explainCamera3DView(camera);
36
+ if (explanation.isOrthonormal)
37
+ return;
38
+ logOnce('camera:non-orthonormal-view', LogLevel.Warn, {
39
+ message: 'setCamera3DViewMatrix4FromMatrix4: the view matrix is not orthonormal, which its consumers rely on — a scaled matrix is the usual cause. Reflections are fine; scale is not. Call explainCamera3DView(camera) for the measured deviations.',
40
+ scaleDeviation: explanation.scaleDeviation,
41
+ shearDeviation: explanation.shearDeviation,
42
+ });
43
+ }
44
+ let cameraGuardsEnabled = false;
45
+ //# sourceMappingURL=enableCameraGuards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableCameraGuards.js","sourceRoot":"","sources":["../src/enableCameraGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAEhE,MAAM,UAAU,sBAAsB;IACpC,OAAO,mBAAmB,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAC3B,6BAA6B,CAAC,IAAI,CAAC,CAAC;IACpC,mBAAmB,GAAG,KAAK,CAAC;AAC9B,CAAC;AAED,kGAAkG;AAClG,2FAA2F;AAC3F,iBAAiB;AACjB,MAAM,UAAU,kBAAkB;IAChC,oBAAoB,CAAC,gCAAgC,CAAC,CAAC;IACvD,6BAA6B,CAAC,qCAAqC,CAAC,CAAC;IACrE,mBAAmB,GAAG,IAAI,CAAC;AAC7B,CAAC;AAED,wGAAwG;AACxG,iGAAiG;AACjG,6DAA6D;AAC7D,SAAS,qCAAqC,CAAC,MAA0B;IACvE,OAAO,CAAC,oCAAoC,MAAM,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,IAAI,EAAE;QACxE,OAAO,EACL,8KAA8K;QAChL,IAAI,EAAE,MAAM,CAAC,IAAI;KAClB,CAAC,CAAC;AACL,CAAC;AAED,sGAAsG;AACtG,wGAAwG;AACxG,oDAAoD;AACpD,SAAS,gCAAgC,CAAC,MAA0B;IAClE,MAAM,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAChD,IAAI,WAAW,CAAC,aAAa;QAAE,OAAO;IACtC,OAAO,CAAC,6BAA6B,EAAE,QAAQ,CAAC,IAAI,EAAE;QACpD,OAAO,EACL,4OAA4O;QAC9O,cAAc,EAAE,WAAW,CAAC,cAAc;QAC1C,cAAc,EAAE,WAAW,CAAC,cAAc;KAC3C,CAAC,CAAC;AACL,CAAC;AAED,IAAI,mBAAmB,GAAG,KAAK,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Camera3D, Camera3DViewExplanation } from '@flighthq/types/contract';
2
+ export declare function explainCamera3DView(camera: Readonly<Camera3D>): Camera3DViewExplanation;
3
+ //# sourceMappingURL=explainCamera3DView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explainCamera3DView.d.ts","sourceRoot":"","sources":["../src/explainCamera3DView.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAYlF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,uBAAuB,CA6BvF"}
@@ -0,0 +1,35 @@
1
+ // Reports whether a camera's view matrix satisfies the orthonormality precondition its consumers rely
2
+ // on, as PLAIN DATA rather than a verdict. Callers get the measured deviations and decide for
3
+ // themselves what that means for the answer they got — `getCamera3DPosition`, for instance, is exact
4
+ // for any orthogonal basis and merely wrong for a scaled one, which is a judgement about their use, not
5
+ // about the matrix.
6
+ //
7
+ // Reflections pass: `determinant` is reported so a caller can tell a mirror camera (-1) from a rigid one
8
+ // (+1), but neither is a violation — an improper orthogonal matrix still satisfies Rᵀ = R⁻¹.
9
+ //
10
+ // Shakeable: nothing in the render path calls this, so an application that never asks sheds it entirely.
11
+ export function explainCamera3DView(camera) {
12
+ const m = camera.view.m;
13
+ const x = { x: m[0], y: m[1], z: m[2] };
14
+ const y = { x: m[4], y: m[5], z: m[6] };
15
+ const z = { x: m[8], y: m[9], z: m[10] };
16
+ const lengthX = Math.hypot(x.x, x.y, x.z);
17
+ const lengthY = Math.hypot(y.x, y.y, y.z);
18
+ const lengthZ = Math.hypot(z.x, z.y, z.z);
19
+ const determinant = m[0] * (m[5] * m[10] - m[6] * m[9]) - m[4] * (m[1] * m[10] - m[2] * m[9]) + m[8] * (m[1] * m[6] - m[2] * m[5]);
20
+ // Largest absolute deviation from unit length across the three basis vectors, and from zero across
21
+ // the three pairwise dots. One number each, so a caller can threshold without redoing the algebra.
22
+ const scaleDeviation = Math.max(Math.abs(lengthX - 1), Math.abs(lengthY - 1), Math.abs(lengthZ - 1));
23
+ const shearDeviation = Math.max(Math.abs(x.x * y.x + x.y * y.y + x.z * y.z), Math.abs(x.x * z.x + x.y * z.y + x.z * z.z), Math.abs(y.x * z.x + y.y * z.y + y.z * z.z));
24
+ return {
25
+ determinant,
26
+ isOrthonormal: scaleDeviation <= ORTHONORMAL_TOLERANCE && shearDeviation <= ORTHONORMAL_TOLERANCE,
27
+ isReflection: determinant < 0,
28
+ scaleDeviation,
29
+ shearDeviation,
30
+ };
31
+ }
32
+ // Loose enough to pass a matrix that has been through a decompose/recompose round trip in float32,
33
+ // tight enough that a deliberate scale of even 1% is reported.
34
+ const ORTHONORMAL_TOLERANCE = 1e-3;
35
+ //# sourceMappingURL=explainCamera3DView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explainCamera3DView.js","sourceRoot":"","sources":["../src/explainCamera3DView.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { configureDirectionalShadowCamera3D, configureDirectionalShadowCamera3DTightFit, createCamera2D, createCamera3D, createOrthographicProjection, createPerspectiveProjection, getCamera2DParallaxPoint, getCamera2DViewMatrix, getCamera2DVisibleBounds, getCamera3DForward, getCamera3DFrustum, getCamera3DFrustumCorners, getCamera3DInverseViewProjectionMatrix4, getCamera3DLinearDepth, getCamera3DPosition, getCamera3DRayThroughBoundingSphere, getCamera3DRight, getCamera3DScreenToWorldRay, getCamera3DUp, getCamera3DViewProjectionMatrix4, getCamera3DViewSpaceZ, getCamera3DWorldToScreen, getOrthographicProjectionTexelSize, intersectCamera3DRayWithPlane, isBoxInCamera3DFrustum, isOrthographicProjection, isPerspectiveProjection, isPointInCamera3DFrustum, isSphereInCamera3DFrustum, projectCamera2DPoint, setCamera3DAspect, setCamera3DJitter, setCamera3DViewMatrix4FromLookAt, setCamera3DViewMatrix4FromMatrix4, setProjectionMatrix4, unprojectCamera2DPoint, updateCamera3DInverseViewProjection, zoomCamera2DAtScreenPoint, } from './contract';
1
+ export { areCameraGuardsEnabled, configureDirectionalShadowCamera3D, configureDirectionalShadowCamera3DTightFit, createCamera2D, createCamera3D, createOrthographicProjection, createPerspectiveProjection, disableCameraGuards, enableCameraGuards, getCamera2DParallaxPoint, getCamera2DViewMatrix, getCamera2DVisibleBounds, getCamera3DForward, getCamera3DFrustum, getCamera3DFrustumCorners, getCamera3DInverseViewProjectionMatrix4, getCamera3DLinearDepth, getCamera3DPosition, getCamera3DRayThroughBoundingSphere, getCamera3DRight, getCamera3DScreenToWorldRay, getCamera3DUp, getCamera3DViewProjectionMatrix4, getCamera3DViewSpaceZ, getCamera3DWorldToScreen, getOrthographicProjectionTexelSize, intersectCamera3DRayWithPlane, isBoxInCamera3DFrustum, isOrthographicProjection, isPerspectiveProjection, isPointInCamera3DFrustum, isSphereInCamera3DFrustum, projectCamera2DPoint, setCamera3DAspect, setCamera3DJitter, setCamera3DViewMatrix4FromLookAt, setCamera3DViewMatrix4FromMatrix4, setProjectionMatrix4, unprojectCamera2DPoint, updateCamera3DInverseViewProjection, zoomCamera2DAtScreenPoint, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,EAClC,0CAA0C,EAC1C,cAAc,EACd,cAAc,EACd,4BAA4B,EAC5B,2BAA2B,EAC3B,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,kBAAkB,EAClB,kBAAkB,EAClB,yBAAyB,EACzB,uCAAuC,EACvC,sBAAsB,EACtB,mBAAmB,EACnB,mCAAmC,EACnC,gBAAgB,EAChB,2BAA2B,EAC3B,aAAa,EACb,gCAAgC,EAChC,qBAAqB,EACrB,wBAAwB,EACxB,kCAAkC,EAClC,6BAA6B,EAC7B,sBAAsB,EACtB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,gCAAgC,EAChC,iCAAiC,EACjC,oBAAoB,EACpB,sBAAsB,EACtB,mCAAmC,EACnC,yBAAyB,GAC1B,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,kCAAkC,EAClC,0CAA0C,EAC1C,cAAc,EACd,cAAc,EACd,4BAA4B,EAC5B,2BAA2B,EAC3B,mBAAmB,EACnB,kBAAkB,EAClB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,kBAAkB,EAClB,kBAAkB,EAClB,yBAAyB,EACzB,uCAAuC,EACvC,sBAAsB,EACtB,mBAAmB,EACnB,mCAAmC,EACnC,gBAAgB,EAChB,2BAA2B,EAC3B,aAAa,EACb,gCAAgC,EAChC,qBAAqB,EACrB,wBAAwB,EACxB,kCAAkC,EAClC,6BAA6B,EAC7B,sBAAsB,EACtB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,gCAAgC,EAChC,iCAAiC,EACjC,oBAAoB,EACpB,sBAAsB,EACtB,mCAAmC,EACnC,yBAAyB,GAC1B,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { configureDirectionalShadowCamera3D, configureDirectionalShadowCamera3DTightFit, createCamera2D, createCamera3D, createOrthographicProjection, createPerspectiveProjection, getCamera2DParallaxPoint, getCamera2DViewMatrix, getCamera2DVisibleBounds, getCamera3DForward, getCamera3DFrustum, getCamera3DFrustumCorners, getCamera3DInverseViewProjectionMatrix4, getCamera3DLinearDepth, getCamera3DPosition, getCamera3DRayThroughBoundingSphere, getCamera3DRight, getCamera3DScreenToWorldRay, getCamera3DUp, getCamera3DViewProjectionMatrix4, getCamera3DViewSpaceZ, getCamera3DWorldToScreen, getOrthographicProjectionTexelSize, intersectCamera3DRayWithPlane, isBoxInCamera3DFrustum, isOrthographicProjection, isPerspectiveProjection, isPointInCamera3DFrustum, isSphereInCamera3DFrustum, projectCamera2DPoint, setCamera3DAspect, setCamera3DJitter, setCamera3DViewMatrix4FromLookAt, setCamera3DViewMatrix4FromMatrix4, setProjectionMatrix4, unprojectCamera2DPoint, updateCamera3DInverseViewProjection, zoomCamera2DAtScreenPoint, } from './contract';
1
+ export { areCameraGuardsEnabled, configureDirectionalShadowCamera3D, configureDirectionalShadowCamera3DTightFit, createCamera2D, createCamera3D, createOrthographicProjection, createPerspectiveProjection, disableCameraGuards, enableCameraGuards, getCamera2DParallaxPoint, getCamera2DViewMatrix, getCamera2DVisibleBounds, getCamera3DForward, getCamera3DFrustum, getCamera3DFrustumCorners, getCamera3DInverseViewProjectionMatrix4, getCamera3DLinearDepth, getCamera3DPosition, getCamera3DRayThroughBoundingSphere, getCamera3DRight, getCamera3DScreenToWorldRay, getCamera3DUp, getCamera3DViewProjectionMatrix4, getCamera3DViewSpaceZ, getCamera3DWorldToScreen, getOrthographicProjectionTexelSize, intersectCamera3DRayWithPlane, isBoxInCamera3DFrustum, isOrthographicProjection, isPerspectiveProjection, isPointInCamera3DFrustum, isSphereInCamera3DFrustum, projectCamera2DPoint, setCamera3DAspect, setCamera3DJitter, setCamera3DViewMatrix4FromLookAt, setCamera3DViewMatrix4FromMatrix4, setProjectionMatrix4, unprojectCamera2DPoint, updateCamera3DInverseViewProjection, zoomCamera2DAtScreenPoint, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,EAClC,0CAA0C,EAC1C,cAAc,EACd,cAAc,EACd,4BAA4B,EAC5B,2BAA2B,EAC3B,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,kBAAkB,EAClB,kBAAkB,EAClB,yBAAyB,EACzB,uCAAuC,EACvC,sBAAsB,EACtB,mBAAmB,EACnB,mCAAmC,EACnC,gBAAgB,EAChB,2BAA2B,EAC3B,aAAa,EACb,gCAAgC,EAChC,qBAAqB,EACrB,wBAAwB,EACxB,kCAAkC,EAClC,6BAA6B,EAC7B,sBAAsB,EACtB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,gCAAgC,EAChC,iCAAiC,EACjC,oBAAoB,EACpB,sBAAsB,EACtB,mCAAmC,EACnC,yBAAyB,GAC1B,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,kCAAkC,EAClC,0CAA0C,EAC1C,cAAc,EACd,cAAc,EACd,4BAA4B,EAC5B,2BAA2B,EAC3B,mBAAmB,EACnB,kBAAkB,EAClB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,kBAAkB,EAClB,kBAAkB,EAClB,yBAAyB,EACzB,uCAAuC,EACvC,sBAAsB,EACtB,mBAAmB,EACnB,mCAAmC,EACnC,gBAAgB,EAChB,2BAA2B,EAC3B,aAAa,EACb,gCAAgC,EAChC,qBAAqB,EACrB,wBAAwB,EACxB,kCAAkC,EAClC,6BAA6B,EAC7B,sBAAsB,EACtB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,gCAAgC,EAChC,iCAAiC,EACjC,oBAAoB,EACpB,sBAAsB,EACtB,mCAAmC,EACnC,yBAAyB,GAC1B,MAAM,YAAY,CAAC"}
@@ -1,3 +1,4 @@
1
1
  import type { Camera2D, RectangleLike } from '@flighthq/types/contract';
2
2
  export declare function getCamera2DVisibleBounds(camera: Readonly<Camera2D>, out: RectangleLike): void;
3
+ export declare function setCamera2DVisibleBoundsGuard(guard: ((camera: Readonly<Camera2D>) => void) | null): void;
3
4
  //# sourceMappingURL=visibleBounds.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"visibleBounds.d.ts","sourceRoot":"","sources":["../src/visibleBounds.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAUxE,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI,CAI7F"}
1
+ {"version":3,"file":"visibleBounds.d.ts","sourceRoot":"","sources":["../src/visibleBounds.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAkBxE,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI,CAW7F;AAID,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAExG"}
@@ -6,11 +6,40 @@ import { getCamera2DViewMatrix } from './viewMatrix';
6
6
  // higher zoom it shrinks (a smaller world region fills the viewport); under rotation it is the
7
7
  // enclosing AABB of the rotated view, so it over-covers rather than clipping visible content. The
8
8
  // rectangle is always centered on the camera position `(x, y)`.
9
+ //
10
+ // A zero `zoom` makes the view matrix singular and it has no inverse, so there is no world rectangle
11
+ // to report. `zoom` is deliberately NOT clamped to rescue this: it is a caller-set value, and quietly
12
+ // substituting a different one is the kind of hidden correction the SDK does not do. The failure is
13
+ // handled HERE instead, and it fails toward drawing — an unbounded rectangle, so the cull excludes
14
+ // nothing. The asymmetry is the whole reason: over-drawing is slow and visible, under-drawing is
15
+ // silent and wrong, and a cull bound that silently collapses removes content with nothing to point at
16
+ // the cause.
9
17
  export function getCamera2DVisibleBounds(camera, out) {
10
18
  getCamera2DViewMatrix(camera, scratchMatrix);
11
- inverseMatrix(scratchInverse, scratchMatrix);
19
+ if (!inverseMatrix(scratchInverse, scratchMatrix)) {
20
+ out.x = UNBOUNDED_ORIGIN;
21
+ out.y = UNBOUNDED_ORIGIN;
22
+ out.width = UNBOUNDED_EXTENT;
23
+ out.height = UNBOUNDED_EXTENT;
24
+ degenerateVisibleBoundsGuard?.(camera);
25
+ return;
26
+ }
12
27
  matrixTransformBounds(out, scratchInverse, 0, 0, camera.viewportWidth, camera.viewportHeight);
13
28
  }
29
+ // The seam the guard layer installs into; `null` keeps this module free of message text and of any
30
+ // dependency on @flighthq/log, so an application that never imports the guards sheds both.
31
+ export function setCamera2DVisibleBoundsGuard(guard) {
32
+ degenerateVisibleBoundsGuard = guard;
33
+ }
34
+ // Finite extremes rather than ±Infinity. With `-Infinity` origin and `Infinity` extent, a max edge is
35
+ // `-Infinity + Infinity` = NaN, and every NaN comparison is false — which `intersectsRectangle` happens
36
+ // to read as "overlaps", because it is written as the negation of four disjoint tests. That is the
37
+ // right answer reached by accident, and only for that one predicate: any cull that compares centers,
38
+ // areas, or sorts by extent gets NaN instead. Half of MAX_VALUE keeps `x + width` finite, so ordinary
39
+ // arithmetic holds everywhere and the rectangle still encloses any real content.
40
+ const UNBOUNDED_EXTENT = Number.MAX_VALUE;
41
+ const UNBOUNDED_ORIGIN = -Number.MAX_VALUE / 2;
42
+ let degenerateVisibleBoundsGuard = null;
14
43
  const scratchInverse = createMatrix();
15
44
  const scratchMatrix = createMatrix();
16
45
  //# sourceMappingURL=visibleBounds.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"visibleBounds.js","sourceRoot":"","sources":["../src/visibleBounds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAGjG,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,sGAAsG;AACtG,qGAAqG;AACrG,oGAAoG;AACpG,+FAA+F;AAC/F,kGAAkG;AAClG,gEAAgE;AAChE,MAAM,UAAU,wBAAwB,CAAC,MAA0B,EAAE,GAAkB;IACrF,qBAAqB,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC7C,aAAa,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;IAC7C,qBAAqB,CAAC,GAAG,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,cAAc,CAAC,CAAC;AAChG,CAAC;AAED,MAAM,cAAc,GAAG,YAAY,EAAE,CAAC;AACtC,MAAM,aAAa,GAAG,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"visibleBounds.js","sourceRoot":"","sources":["../src/visibleBounds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAGjG,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,sGAAsG;AACtG,qGAAqG;AACrG,oGAAoG;AACpG,+FAA+F;AAC/F,kGAAkG;AAClG,gEAAgE;AAChE,EAAE;AACF,qGAAqG;AACrG,sGAAsG;AACtG,oGAAoG;AACpG,mGAAmG;AACnG,iGAAiG;AACjG,sGAAsG;AACtG,aAAa;AACb,MAAM,UAAU,wBAAwB,CAAC,MAA0B,EAAE,GAAkB;IACrF,qBAAqB,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC7C,IAAI,CAAC,aAAa,CAAC,cAAc,EAAE,aAAa,CAAC,EAAE,CAAC;QAClD,GAAG,CAAC,CAAC,GAAG,gBAAgB,CAAC;QACzB,GAAG,CAAC,CAAC,GAAG,gBAAgB,CAAC;QACzB,GAAG,CAAC,KAAK,GAAG,gBAAgB,CAAC;QAC7B,GAAG,CAAC,MAAM,GAAG,gBAAgB,CAAC;QAC9B,4BAA4B,EAAE,CAAC,MAAM,CAAC,CAAC;QACvC,OAAO;IACT,CAAC;IACD,qBAAqB,CAAC,GAAG,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,cAAc,CAAC,CAAC;AAChG,CAAC;AAED,mGAAmG;AACnG,2FAA2F;AAC3F,MAAM,UAAU,6BAA6B,CAAC,KAAoD;IAChG,4BAA4B,GAAG,KAAK,CAAC;AACvC,CAAC;AAED,sGAAsG;AACtG,wGAAwG;AACxG,mGAAmG;AACnG,qGAAqG;AACrG,sGAAsG;AACtG,iFAAiF;AACjF,MAAM,gBAAgB,GAAG,MAAM,CAAC,SAAS,CAAC;AAC1C,MAAM,gBAAgB,GAAG,CAAC,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC;AAE/C,IAAI,4BAA4B,GAAkD,IAAI,CAAC;AACvF,MAAM,cAAc,GAAG,YAAY,EAAE,CAAC;AACtC,MAAM,aAAa,GAAG,YAAY,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/camera",
3
- "version": "0.3.0",
3
+ "version": "0.3.1-next.1050.9114695",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,9 +38,10 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/entity": "0.3.0",
42
- "@flighthq/geometry": "0.3.0",
43
- "@flighthq/types": "0.3.0"
41
+ "@flighthq/entity": "0.3.1-next.1050.9114695",
42
+ "@flighthq/geometry": "0.3.1-next.1050.9114695",
43
+ "@flighthq/log": "0.3.1-next.1050.9114695",
44
+ "@flighthq/types": "0.3.1-next.1050.9114695"
44
45
  },
45
46
  "devDependencies": {
46
47
  "typescript": "^5.3.0"
@@ -12,6 +12,7 @@ import {
12
12
  getCamera3DViewProjectionMatrix4,
13
13
  setCamera3DAspect,
14
14
  setCamera3DJitter,
15
+ setCamera3DViewGuard,
15
16
  setCamera3DViewMatrix4FromLookAt,
16
17
  setCamera3DViewMatrix4FromMatrix4,
17
18
  updateCamera3DInverseViewProjection,
@@ -157,6 +158,25 @@ describe('setCamera3DJitter', () => {
157
158
  });
158
159
  });
159
160
 
161
+ describe('setCamera3DViewGuard', () => {
162
+ it('is notified whenever a view matrix is assigned, and not otherwise', () => {
163
+ const seen: number[] = [];
164
+ setCamera3DViewGuard((camera) => seen.push(camera.view.m[0]));
165
+ try {
166
+ const camera = createCamera3D({
167
+ far: 100,
168
+ near: 0.1,
169
+ projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
170
+ });
171
+ expect(seen).toEqual([]);
172
+ setCamera3DViewMatrix4FromMatrix4(camera, createMatrix4(4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1));
173
+ expect(seen).toEqual([4]);
174
+ } finally {
175
+ setCamera3DViewGuard(null);
176
+ }
177
+ });
178
+ });
179
+
160
180
  describe('setCamera3DViewMatrix4FromLookAt', () => {
161
181
  it('builds the view matrix from eye-target-up', () => {
162
182
  const camera = createCamera3D({
@@ -0,0 +1,94 @@
1
+ import { createMatrix4, createRectangle } from '@flighthq/geometry/contract';
2
+ import { setLogSink } from '@flighthq/log/contract';
3
+ import type { Camera2D, LogEntry } from '@flighthq/types/contract';
4
+ import { afterEach, describe, expect, test } from 'vitest';
5
+
6
+ import { createCamera3D, setCamera3DViewMatrix4FromMatrix4 } from './camera';
7
+ import { createCamera2D } from './camera2d';
8
+ import { areCameraGuardsEnabled, disableCameraGuards, enableCameraGuards } from './enableCameraGuards';
9
+ import { createPerspectiveProjection } from './projection';
10
+ import { getCamera2DVisibleBounds } from './visibleBounds';
11
+
12
+ describe('areCameraGuardsEnabled', () => {
13
+ test('reports whether the guards are installed', () => {
14
+ expect(areCameraGuardsEnabled()).toBe(false);
15
+ enableCameraGuards();
16
+ expect(areCameraGuardsEnabled()).toBe(true);
17
+ disableCameraGuards();
18
+ expect(areCameraGuardsEnabled()).toBe(false);
19
+ });
20
+ });
21
+
22
+ describe('disableCameraGuards', () => {
23
+ test('leaves the core silent again once removed', () => {
24
+ enableCameraGuards();
25
+ disableCameraGuards();
26
+ expect(captureLog(() => getCamera2DVisibleBounds(degenerateCamera(), createRectangle()))).toHaveLength(0);
27
+ });
28
+ });
29
+
30
+ describe('enableCameraGuards', () => {
31
+ // `logOnce` keys on the zoom and has no reset, so this is the ONE test allowed to trigger the
32
+ // degenerate key — a second would be deduplicated away and assert nothing. The seam itself is tested
33
+ // directly, and deterministically, in visibleBounds.test.ts.
34
+ test('warns, naming the zoom and the consequence, when the visible bounds cannot be computed', () => {
35
+ enableCameraGuards();
36
+ const entries = captureLog(() => getCamera2DVisibleBounds(degenerateCamera(), createRectangle()));
37
+ expect(entries).toHaveLength(1);
38
+ expect(entries[0].data).toMatchObject({ zoom: 0 });
39
+ expect(String((entries[0].data as { message: string }).message)).toContain('nothing is culled');
40
+ });
41
+
42
+ test('warns on a non-orthonormal 3D view, naming the cause rather than a consumer', () => {
43
+ enableCameraGuards();
44
+ const entries = captureLog(() => {
45
+ const camera = createCamera3D({
46
+ far: 100,
47
+ near: 0.1,
48
+ projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
49
+ });
50
+ setCamera3DViewMatrix4FromMatrix4(camera, createMatrix4(3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1));
51
+ });
52
+ expect(entries).toHaveLength(1);
53
+ expect(String((entries[0].data as { message: string }).message)).toContain('not orthonormal');
54
+ });
55
+
56
+ // Reflections satisfy the precondition, so warning on them would be a false positive on a legitimate
57
+ // water/mirror camera.
58
+ test('stays silent for a reflection view', () => {
59
+ enableCameraGuards();
60
+ const entries = captureLog(() => {
61
+ const camera = createCamera3D({
62
+ far: 100,
63
+ near: 0.1,
64
+ projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
65
+ });
66
+ setCamera3DViewMatrix4FromMatrix4(camera, createMatrix4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 2, 3, 4, 1));
67
+ });
68
+ expect(entries).toHaveLength(0);
69
+ });
70
+
71
+ test('stays silent for a camera whose view matrix inverts', () => {
72
+ enableCameraGuards();
73
+ expect(captureLog(() => getCamera2DVisibleBounds(createCamera2D(64, 64), createRectangle()))).toHaveLength(0);
74
+ });
75
+ });
76
+
77
+ afterEach(() => {
78
+ disableCameraGuards();
79
+ });
80
+
81
+ function captureLog(run: () => void): LogEntry[] {
82
+ const entries: LogEntry[] = [];
83
+ setLogSink((entry) => entries.push(entry));
84
+ try {
85
+ run();
86
+ } finally {
87
+ setLogSink(null);
88
+ }
89
+ return entries;
90
+ }
91
+
92
+ function degenerateCamera(): Camera2D {
93
+ return createCamera2D(64, 64, { zoom: 0 });
94
+ }
@@ -0,0 +1,49 @@
1
+ import { createMatrix4 } from '@flighthq/geometry/contract';
2
+ import { describe, expect, test } from 'vitest';
3
+
4
+ import { createCamera3D, setCamera3DViewMatrix4FromLookAt, setCamera3DViewMatrix4FromMatrix4 } from './camera';
5
+ import { explainCamera3DView } from './explainCamera3DView';
6
+ import { createPerspectiveProjection } from './projection';
7
+
8
+ describe('explainCamera3DView', () => {
9
+ test('accepts a rigid lookAt view', () => {
10
+ const camera = perspectiveCamera();
11
+ setCamera3DViewMatrix4FromLookAt(camera, { x: 0, y: 0, z: 10 }, { x: 0, y: 0, z: 0 }, { x: 0, y: 1, z: 0 });
12
+ const explanation = explainCamera3DView(camera);
13
+ expect(explanation.isOrthonormal).toBe(true);
14
+ expect(explanation.isReflection).toBe(false);
15
+ expect(explanation.determinant).toBeCloseTo(1, 6);
16
+ });
17
+
18
+ // The case manager asked to be costed before the contract was accepted: a mirror camera is improper
19
+ // orthogonal (det = -1) but STILL orthogonal, so Rᵀ = R⁻¹ holds and nothing that relies on the
20
+ // transpose is lost. It must not be reported as a violation.
21
+ test('accepts a reflection view and reports it as one', () => {
22
+ const camera = perspectiveCamera();
23
+ setCamera3DViewMatrix4FromMatrix4(camera, createMatrix4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, 7, 9, 1));
24
+ const explanation = explainCamera3DView(camera);
25
+ expect(explanation.isOrthonormal).toBe(true);
26
+ expect(explanation.isReflection).toBe(true);
27
+ expect(explanation.determinant).toBeCloseTo(-1, 6);
28
+ });
29
+
30
+ test('reports a scaled view as non-orthonormal, with the deviation', () => {
31
+ const camera = perspectiveCamera();
32
+ setCamera3DViewMatrix4FromMatrix4(camera, createMatrix4(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1));
33
+ const explanation = explainCamera3DView(camera);
34
+ expect(explanation.isOrthonormal).toBe(false);
35
+ expect(explanation.scaleDeviation).toBeCloseTo(1, 6);
36
+ });
37
+
38
+ test('reports a sheared view as non-orthonormal', () => {
39
+ const camera = perspectiveCamera();
40
+ setCamera3DViewMatrix4FromMatrix4(camera, createMatrix4(1, 0, 0, 0, 0.5, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1));
41
+ const explanation = explainCamera3DView(camera);
42
+ expect(explanation.isOrthonormal).toBe(false);
43
+ expect(explanation.shearDeviation).toBeGreaterThan(0.1);
44
+ });
45
+ });
46
+
47
+ function perspectiveCamera(): ReturnType<typeof createCamera3D> {
48
+ return createCamera3D({ far: 100, near: 0.1, projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }) });
49
+ }
@@ -1,9 +1,9 @@
1
- import { createRectangle, createVector2 } from '@flighthq/geometry/contract';
2
- import { describe, expect, it } from 'vitest';
1
+ import { createRectangle, createVector2, intersectsRectangle } from '@flighthq/geometry/contract';
2
+ import { describe, expect, it, test } from 'vitest';
3
3
 
4
4
  import { createCamera2D } from './camera2d';
5
5
  import { unprojectCamera2DPoint } from './projection2d';
6
- import { getCamera2DVisibleBounds } from './visibleBounds';
6
+ import { getCamera2DVisibleBounds, setCamera2DVisibleBoundsGuard } from './visibleBounds';
7
7
 
8
8
  describe('getCamera2DVisibleBounds', () => {
9
9
  it('covers the full viewport in world units at zoom 1', () => {
@@ -52,3 +52,35 @@ describe('getCamera2DVisibleBounds', () => {
52
52
  }
53
53
  });
54
54
  });
55
+
56
+ describe('setCamera2DVisibleBoundsGuard', () => {
57
+ test('fails toward drawing: an unbounded rectangle that still intersects distant content', () => {
58
+ const out = createRectangle();
59
+ getCamera2DVisibleBounds(createCamera2D(64, 64, { zoom: 0 }), out);
60
+ // Finite on purpose — the max edge must survive ordinary arithmetic, not rely on NaN comparisons.
61
+ expect(Number.isFinite(out.x + out.width)).toBe(true);
62
+ expect(Number.isFinite(out.y + out.height)).toBe(true);
63
+ // The property that matters is behavioural, not the constant: nothing gets culled.
64
+ expect(intersectsRectangle(out, createRectangle(1e12, -4e11, 10, 10))).toBe(true);
65
+ expect(intersectsRectangle(out, createRectangle(-9e15, 8e14, 1, 1))).toBe(true);
66
+ });
67
+
68
+ test('notifies the installed guard exactly once per degenerate call, and never for an invertible camera', () => {
69
+ const seen: number[] = [];
70
+ setCamera2DVisibleBoundsGuard((camera) => seen.push(camera.zoom));
71
+ try {
72
+ getCamera2DVisibleBounds(createCamera2D(64, 64, { zoom: 0 }), createRectangle());
73
+ getCamera2DVisibleBounds(createCamera2D(64, 64), createRectangle());
74
+ expect(seen).toEqual([0]);
75
+ } finally {
76
+ setCamera2DVisibleBoundsGuard(null);
77
+ }
78
+ });
79
+
80
+ test('leaves an invertible camera computing the same bounds as before', () => {
81
+ const out = createRectangle();
82
+ getCamera2DVisibleBounds(createCamera2D(64, 32), out);
83
+ expect(out.width).toBeCloseTo(64, 6);
84
+ expect(out.height).toBeCloseTo(32, 6);
85
+ });
86
+ });