@flighthq/camera 0.3.0-next.906.07cea63 → 0.3.1-next.1041.35b950c

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
@@ -6,5 +6,5 @@ export declare function setCamera3DAspect(camera: Camera3D, aspect: number): voi
6
6
  export declare function setCamera3DJitter(camera: Camera3D, x: number, y: number): void;
7
7
  export declare function setCamera3DViewMatrix4FromLookAt(camera: Camera3D, eye: Readonly<Vector3Like>, target: Readonly<Vector3Like>, up: Readonly<Vector3Like>): void;
8
8
  export declare function setCamera3DViewMatrix4FromMatrix4(camera: Camera3D, view: Readonly<Matrix4Like>): void;
9
- export declare function updateCamera3DInverseViewProjection(camera: Camera3D, aspect: number): boolean;
9
+ export declare function updateCamera3DInverseViewProjection(camera: Readonly<Camera3D>, aspect: number): boolean;
10
10
  //# sourceMappingURL=camera.d.ts.map
@@ -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;AAUD,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,CAGnH;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;AAQD,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAO7F"}
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"}
package/dist/camera.js CHANGED
@@ -3,10 +3,10 @@ import { createMatrix4, createVector2, inverseMatrix4, multiplyMatrix4, setMatri
3
3
  import { setProjectionMatrix4 } from './projection';
4
4
  // Allocates a 3D camera. The camera stores its projection descriptor, a world->view Matrix4
5
5
  // (`view`, initialized to identity), the clip-plane distances `near`/`far`, the per-frame
6
- // sub-pixel NDC `jitter` (consumed by TAA, initialized to zero), and a cached
7
- // `inverseViewProjection` (consumed by TAA / velocity / fog / depth-of-field, initialized to
8
- // identity). The view matrix is canonical: the camera has no separate Transform3D — a Matrix4 is
9
- // the only world->view representation.
6
+ // sub-pixel NDC `jitter` (applied to every projection, initialized to zero), and the last
7
+ // `inverseViewProjection` computed for a reconstruction pass (initialized to identity). The view
8
+ // matrix is canonical: the camera has no separate Transform3D — a Matrix4 is the only world->view
9
+ // representation.
10
10
  export function createCamera3D(opts) {
11
11
  return createEntity({
12
12
  far: opts.far,
@@ -19,12 +19,11 @@ export function createCamera3D(opts) {
19
19
  }
20
20
  // Writes the inverse of the camera's view-projection matrix into `out` and returns true, or
21
21
  // returns false (writing NaN into `out`) when the view-projection is non-invertible. `aspect`
22
- // is the viewport width / height. This is the matrix the existing TAA / velocity / fog /
23
- // depth-of-field effects consume to reconstruct world position from NDC.
22
+ // is the viewport width / height. Skybox and other world-position reconstruction passes use this
23
+ // calculation to refresh Camera3D.inverseViewProjection immediately before consuming it.
24
24
  //
25
25
  // Reads camera fields into a scratch matrix before writing `out`, so it is safe even if `out`
26
- // aliases the camera's own `inverseViewProjection` or `view`. Use
27
- // `updateCamera3DInverseViewProjection` instead to safely update the cached field.
26
+ // aliases the camera's own `inverseViewProjection` or `view`.
28
27
  export function getCamera3DInverseViewProjectionMatrix4(out, camera, aspect) {
29
28
  getCamera3DViewProjectionMatrix4(__scratchViewProjection, camera, aspect);
30
29
  return inverseMatrix4(out, __scratchViewProjection);
@@ -37,6 +36,7 @@ export function getCamera3DInverseViewProjectionMatrix4(out, camera, aspect) {
37
36
  // aliases the camera's own `view`.
38
37
  export function getCamera3DViewProjectionMatrix4(out, camera, aspect) {
39
38
  setProjectionMatrix4(__scratchProjection, camera.projection, aspect, camera.near, camera.far);
39
+ applyCamera3DProjectionJitter(__scratchProjection, camera.jitter.x, camera.jitter.y);
40
40
  multiplyMatrix4(out, __scratchProjection, camera.view);
41
41
  }
42
42
  // Sets the viewport aspect ratio (width / height) on the camera's projection, in place. For a
@@ -53,8 +53,8 @@ export function setCamera3DAspect(camera, aspect) {
53
53
  }
54
54
  projection.halfWidth = projection.halfHeight * aspect;
55
55
  }
56
- // Sets the camera's per-frame sub-pixel jitter offset (in NDC), in place. TAA reads this when
57
- // building the jittered projection matrix.
56
+ // Sets the camera's per-frame sub-pixel jitter offset (in NDC), in place. Every view-projection
57
+ // helper applies it to clip X/Y before the perspective divide.
58
58
  export function setCamera3DJitter(camera, x, y) {
59
59
  camera.jitter.x = x;
60
60
  camera.jitter.y = y;
@@ -72,20 +72,30 @@ export function setCamera3DViewMatrix4FromLookAt(camera, eye, target, up) {
72
72
  export function setCamera3DViewMatrix4FromMatrix4(camera, view) {
73
73
  camera.view.m.set(view.m);
74
74
  }
75
- // Recomputes and stores the inverse view-projection into camera.inverseViewProjection for the
76
- // given aspect. Returns true on success or false when the matrix is non-invertible (leaving the
77
- // cached field untouched). `aspect` is viewport width / height.
78
- //
79
- // Call this once per frame after setting the view matrix and before any effects that read
80
- // camera.inverseViewProjection (TAA, velocity, fog, depth-of-field).
75
+ // Refreshes the camera's required inverse-view-projection cache for the supplied viewport aspect.
76
+ // Returns true on success or false when projection×view is non-invertible, leaving the prior valid
77
+ // cache untouched. Reconstruction consumers call this immediately before reading the field so it
78
+ // cannot silently drift after mutable camera inputs change.
81
79
  export function updateCamera3DInverseViewProjection(camera, aspect) {
82
- // Write into a scratch first so the cache is never clobbered with NaN on failure.
83
80
  const ok = getCamera3DInverseViewProjectionMatrix4(__scratchInverse, camera, aspect);
84
- if (ok) {
81
+ if (ok)
85
82
  camera.inverseViewProjection.m.set(__scratchInverse.m);
86
- }
87
83
  return ok;
88
84
  }
85
+ // Applies an NDC translation to a projection matrix: clip.xy += jitter * clip.w. Expressing it as
86
+ // row operations handles perspective (offset in m[8]/m[9]) and orthographic (m[12]/m[13]) matrices
87
+ // uniformly.
88
+ function applyCamera3DProjectionJitter(out, x, y) {
89
+ const m = out.m;
90
+ m[0] += x * m[3];
91
+ m[4] += x * m[7];
92
+ m[8] += x * m[11];
93
+ m[12] += x * m[15];
94
+ m[1] += y * m[3];
95
+ m[5] += y * m[7];
96
+ m[9] += y * m[11];
97
+ m[13] += y * m[15];
98
+ }
89
99
  // Scratch matrices reused by the view-projection helpers. Single-threaded; not re-entrant.
90
100
  const __scratchInverse = createMatrix4();
91
101
  const __scratchProjection = createMatrix4();
@@ -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,7 @@ export * from './camera';
3
3
  export * from './camera2d';
4
4
  export * from './culling';
5
5
  export * from './depth';
6
+ export * from './enableCameraGuards';
6
7
  export * from './frustumCorners';
7
8
  export * from './intersection';
8
9
  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,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,7 @@ export * from './camera';
3
3
  export * from './camera2d';
4
4
  export * from './culling';
5
5
  export * from './depth';
6
+ export * from './enableCameraGuards';
6
7
  export * from './frustumCorners';
7
8
  export * from './intersection';
8
9
  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,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":"AAMA,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AAKD,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC"}
@@ -0,0 +1,28 @@
1
+ import { logOnce } from '@flighthq/log/contract';
2
+ import { LogLevel } from '@flighthq/types/contract';
3
+ import { setCamera2DVisibleBoundsGuard } from './visibleBounds';
4
+ export function areCameraGuardsEnabled() {
5
+ return cameraGuardsEnabled;
6
+ }
7
+ export function disableCameraGuards() {
8
+ setCamera2DVisibleBoundsGuard(null);
9
+ cameraGuardsEnabled = false;
10
+ }
11
+ // Installs opt-in diagnostics for camera inputs that leave a computation with no answer. The core
12
+ // modules stay message-free, so an application that omits this one sheds both the text and
13
+ // @flighthq/log.
14
+ export function enableCameraGuards() {
15
+ setCamera2DVisibleBoundsGuard(warnOnDegenerateCamera2DVisibleBounds);
16
+ cameraGuardsEnabled = true;
17
+ }
18
+ // The unbounded rectangle is a deliberate fail-toward-drawing, not a silent success: nothing is culled,
19
+ // so the frame is correct but pays for every object. That is a performance cliff with no visible
20
+ // symptom, which is exactly the case a guard exists to name.
21
+ function warnOnDegenerateCamera2DVisibleBounds(camera) {
22
+ logOnce(`camera:degenerate-visible-bounds:${camera.zoom}`, LogLevel.Warn, {
23
+ 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.',
24
+ zoom: camera.zoom,
25
+ });
26
+ }
27
+ let cameraGuardsEnabled = false;
28
+ //# 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,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAEhE,MAAM,UAAU,sBAAsB;IACpC,OAAO,mBAAmB,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,6BAA6B,CAAC,IAAI,CAAC,CAAC;IACpC,mBAAmB,GAAG,KAAK,CAAC;AAC9B,CAAC;AAED,kGAAkG;AAClG,2FAA2F;AAC3F,iBAAiB;AACjB,MAAM,UAAU,kBAAkB;IAChC,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,IAAI,mBAAmB,GAAG,KAAK,CAAC"}
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, 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,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, 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,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"}
@@ -2,6 +2,7 @@ import type { Matrix4Like } from '@flighthq/types/contract';
2
2
  import type { OrthographicProjection, OrthographicProjectionOptions, PerspectiveProjection, PerspectiveProjectionOptions, Projection } from '@flighthq/types/contract';
3
3
  export declare function createOrthographicProjection(opts: Readonly<OrthographicProjectionOptions>): OrthographicProjection;
4
4
  export declare function createPerspectiveProjection(opts: Readonly<PerspectiveProjectionOptions>): PerspectiveProjection;
5
+ export declare function getOrthographicProjectionTexelSize(projection: Readonly<OrthographicProjection>, pixelWidth: number, pixelHeight: number): number;
5
6
  export declare function isOrthographicProjection(projection: Readonly<Projection>): projection is OrthographicProjection;
6
7
  export declare function isPerspectiveProjection(projection: Readonly<Projection>): projection is PerspectiveProjection;
7
8
  export declare function setProjectionMatrix4(out: Matrix4Like, projection: Readonly<Projection>, aspect: number, near: number, far: number): void;
@@ -1 +1 @@
1
- {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EACV,sBAAsB,EACtB,6BAA6B,EAC7B,qBAAqB,EACrB,4BAA4B,EAC5B,UAAU,EACX,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,QAAQ,CAAC,6BAA6B,CAAC,GAAG,sBAAsB,CAMlH;AAID,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,QAAQ,CAAC,4BAA4B,CAAC,GAAG,qBAAqB,CAM/G;AAGD,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,IAAI,sBAAsB,CAE/G;AAGD,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,IAAI,qBAAqB,CAE7G;AAUD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,WAAW,EAChB,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,IAAI,CAWN"}
1
+ {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EACV,sBAAsB,EACtB,6BAA6B,EAC7B,qBAAqB,EACrB,4BAA4B,EAC5B,UAAU,EACX,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,QAAQ,CAAC,6BAA6B,CAAC,GAAG,sBAAsB,CAMlH;AAID,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,QAAQ,CAAC,4BAA4B,CAAC,GAAG,qBAAqB,CAM/G;AAKD,wBAAgB,kCAAkC,CAChD,UAAU,EAAE,QAAQ,CAAC,sBAAsB,CAAC,EAC5C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAGD,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,IAAI,sBAAsB,CAE/G;AAGD,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,IAAI,qBAAqB,CAE7G;AAUD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,WAAW,EAChB,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,IAAI,CAWN"}
@@ -18,6 +18,12 @@ export function createPerspectiveProjection(opts) {
18
18
  kind: 'perspective',
19
19
  };
20
20
  }
21
+ // Returns the larger world-space footprint of one texel across an orthographic projection. Using the
22
+ // larger axis gives receiver offsets one conservative, aspect-independent unit even when the shadow
23
+ // map or light projection is not square. `pixelWidth`/`pixelHeight` must be positive.
24
+ export function getOrthographicProjectionTexelSize(projection, pixelWidth, pixelHeight) {
25
+ return Math.max((projection.halfWidth * 2) / pixelWidth, (projection.halfHeight * 2) / pixelHeight);
26
+ }
21
27
  // True when the projection is an orthographic descriptor. Narrows the discriminated union.
22
28
  export function isOrthographicProjection(projection) {
23
29
  return projection.kind === 'orthographic';
@@ -1 +1 @@
1
- {"version":3,"file":"projection.js","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAU5F,0FAA0F;AAC1F,wFAAwF;AACxF,wEAAwE;AACxE,MAAM,UAAU,4BAA4B,CAAC,IAA6C;IACxF,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,IAAI,EAAE,cAAc;KACrB,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,gGAAgG;AAChG,MAAM,UAAU,2BAA2B,CAAC,IAA4C;IACtF,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC;QACxB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,aAAa;KACpB,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,wBAAwB,CAAC,UAAgC;IACvE,OAAO,UAAU,CAAC,IAAI,KAAK,cAAc,CAAC;AAC5C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,uBAAuB,CAAC,UAAgC;IACtE,OAAO,UAAU,CAAC,IAAI,KAAK,aAAa,CAAC;AAC3C,CAAC;AAED,iGAAiG;AACjG,uFAAuF;AACvF,iGAAiG;AACjG,iGAAiG;AACjG,6CAA6C;AAC7C,EAAE;AACF,4FAA4F;AAC5F,yCAAyC;AACzC,MAAM,UAAU,oBAAoB,CAClC,GAAgB,EAChB,UAAgC,EAChC,MAAc,EACd,IAAY,EACZ,GAAW;IAEX,IAAI,UAAU,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QACtC,0FAA0F;QAC1F,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;QACpD,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;QAC3D,OAAO;IACT,CAAC;IAED,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;IACvC,MAAM,UAAU,GAAG,UAAU,CAAC,UAAU,CAAC;IACzC,sBAAsB,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AACzF,CAAC"}
1
+ {"version":3,"file":"projection.js","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAU5F,0FAA0F;AAC1F,wFAAwF;AACxF,wEAAwE;AACxE,MAAM,UAAU,4BAA4B,CAAC,IAA6C;IACxF,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,IAAI,EAAE,cAAc;KACrB,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,gGAAgG;AAChG,MAAM,UAAU,2BAA2B,CAAC,IAA4C;IACtF,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC;QACxB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,aAAa;KACpB,CAAC;AACJ,CAAC;AAED,qGAAqG;AACrG,oGAAoG;AACpG,sFAAsF;AACtF,MAAM,UAAU,kCAAkC,CAChD,UAA4C,EAC5C,UAAkB,EAClB,WAAmB;IAEnB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,UAAU,EAAE,CAAC,UAAU,CAAC,UAAU,GAAG,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC;AACtG,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,wBAAwB,CAAC,UAAgC;IACvE,OAAO,UAAU,CAAC,IAAI,KAAK,cAAc,CAAC;AAC5C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,uBAAuB,CAAC,UAAgC;IACtE,OAAO,UAAU,CAAC,IAAI,KAAK,aAAa,CAAC;AAC3C,CAAC;AAED,iGAAiG;AACjG,uFAAuF;AACvF,iGAAiG;AACjG,iGAAiG;AACjG,6CAA6C;AAC7C,EAAE;AACF,4FAA4F;AAC5F,yCAAyC;AACzC,MAAM,UAAU,oBAAoB,CAClC,GAAgB,EAChB,UAAgC,EAChC,MAAc,EACd,IAAY,EACZ,GAAW;IAEX,IAAI,UAAU,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QACtC,0FAA0F;QAC1F,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;QACpD,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;QAC3D,OAAO;IACT,CAAC;IAED,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;IACvC,MAAM,UAAU,GAAG,UAAU,CAAC,UAAU,CAAC;IACzC,sBAAsB,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AACzF,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,8 @@
1
1
  {
2
2
  "name": "@flighthq/camera",
3
- "version": "0.3.0-next.906.07cea63",
3
+ "version": "0.3.1-next.1041.35b950c",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
4
6
  "repository": {
5
7
  "type": "git",
6
8
  "url": "https://github.com/flighthq/flight.git",
@@ -36,9 +38,10 @@
36
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
39
  },
38
40
  "dependencies": {
39
- "@flighthq/entity": "0.3.0-next.906.07cea63",
40
- "@flighthq/geometry": "0.3.0-next.906.07cea63",
41
- "@flighthq/types": "0.3.0-next.906.07cea63"
41
+ "@flighthq/entity": "0.3.1-next.1041.35b950c",
42
+ "@flighthq/geometry": "0.3.1-next.1041.35b950c",
43
+ "@flighthq/log": "0.3.1-next.1041.35b950c",
44
+ "@flighthq/types": "0.3.1-next.1041.35b950c"
42
45
  },
43
46
  "devDependencies": {
44
47
  "typescript": "^5.3.0"
@@ -89,6 +89,23 @@ describe('getCamera3DViewProjectionMatrix4', () => {
89
89
  }
90
90
  });
91
91
 
92
+ it('applies the camera jitter as an NDC projection offset', () => {
93
+ const camera = createCamera3D({
94
+ far: 100,
95
+ near: 0.1,
96
+ projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
97
+ });
98
+ const base = createMatrix4();
99
+ getCamera3DViewProjectionMatrix4(base, camera, 1);
100
+
101
+ setCamera3DJitter(camera, 0.25, -0.5);
102
+ const jittered = createMatrix4();
103
+ getCamera3DViewProjectionMatrix4(jittered, camera, 1);
104
+
105
+ expect(jittered.m[8]).toBeCloseTo(base.m[8] - 0.25);
106
+ expect(jittered.m[9]).toBeCloseTo(base.m[9] + 0.5);
107
+ });
108
+
92
109
  it('is safe when out aliases the camera view', () => {
93
110
  const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
94
111
  const camera = createCamera3D({ far: 100, near: 0.1, projection });
@@ -181,35 +198,32 @@ describe('setCamera3DViewMatrix4FromMatrix4', () => {
181
198
  });
182
199
 
183
200
  describe('updateCamera3DInverseViewProjection', () => {
184
- it('stores the inverse view-projection into camera.inverseViewProjection and returns true', () => {
185
- const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
186
- const camera = createCamera3D({ far: 100, near: 0.1, projection });
187
- setCamera3DViewMatrix4FromLookAt(camera, createVector3(1, 2, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
188
-
189
- const result = updateCamera3DInverseViewProjection(camera, 1.5);
190
- expect(result).toBe(true);
201
+ it('refreshes the required cache from the jittered projection', () => {
202
+ const camera = createCamera3D({
203
+ far: 100,
204
+ near: 0.1,
205
+ projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
206
+ });
207
+ setCamera3DJitter(camera, 0.25, -0.5);
208
+ expect(updateCamera3DInverseViewProjection(camera, 1.5)).toBe(true);
191
209
 
192
- // Compute expected inverse independently.
193
210
  const vp = createMatrix4();
194
211
  getCamera3DViewProjectionMatrix4(vp, camera, 1.5);
195
212
  const expected = createMatrix4();
196
213
  inverseMatrix4(expected, vp);
197
-
198
- for (let i = 0; i < 16; i++) {
199
- expect(camera.inverseViewProjection.m[i]).toBeCloseTo(expected.m[i]);
200
- }
214
+ for (let i = 0; i < 16; i++) expect(camera.inverseViewProjection.m[i]).toBeCloseTo(expected.m[i]);
201
215
  });
202
216
 
203
- it('returns false and leaves inverseViewProjection untouched for a non-invertible matrix', () => {
204
- const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
205
- const camera = createCamera3D({ far: 100, near: 0.1, projection });
206
- camera.near = 1;
207
- camera.far = 1; // Degenerate.
208
- // Store a known value so we can confirm it was not overwritten.
217
+ it('leaves the last valid cache untouched for a non-invertible projection', () => {
218
+ const camera = createCamera3D({
219
+ far: 100,
220
+ near: 0.1,
221
+ projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
222
+ });
209
223
  camera.inverseViewProjection.m[0] = 42;
210
-
211
- const result = updateCamera3DInverseViewProjection(camera, 1);
212
- expect(result).toBe(false);
224
+ camera.near = 1;
225
+ camera.far = 1;
226
+ expect(updateCamera3DInverseViewProjection(camera, 1)).toBe(false);
213
227
  expect(camera.inverseViewProjection.m[0]).toBe(42);
214
228
  });
215
229
  });
@@ -0,0 +1,63 @@
1
+ import { 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 { createCamera2D } from './camera2d';
7
+ import { areCameraGuardsEnabled, disableCameraGuards, enableCameraGuards } from './enableCameraGuards';
8
+ import { getCamera2DVisibleBounds } from './visibleBounds';
9
+
10
+ describe('areCameraGuardsEnabled', () => {
11
+ test('reports whether the guards are installed', () => {
12
+ expect(areCameraGuardsEnabled()).toBe(false);
13
+ enableCameraGuards();
14
+ expect(areCameraGuardsEnabled()).toBe(true);
15
+ disableCameraGuards();
16
+ expect(areCameraGuardsEnabled()).toBe(false);
17
+ });
18
+ });
19
+
20
+ describe('disableCameraGuards', () => {
21
+ test('leaves the core silent again once removed', () => {
22
+ enableCameraGuards();
23
+ disableCameraGuards();
24
+ expect(captureLog(() => getCamera2DVisibleBounds(degenerateCamera(), createRectangle()))).toHaveLength(0);
25
+ });
26
+ });
27
+
28
+ describe('enableCameraGuards', () => {
29
+ // `logOnce` keys on the zoom and has no reset, so this is the ONE test allowed to trigger the
30
+ // degenerate key — a second would be deduplicated away and assert nothing. The seam itself is tested
31
+ // directly, and deterministically, in visibleBounds.test.ts.
32
+ test('warns, naming the zoom and the consequence, when the visible bounds cannot be computed', () => {
33
+ enableCameraGuards();
34
+ const entries = captureLog(() => getCamera2DVisibleBounds(degenerateCamera(), createRectangle()));
35
+ expect(entries).toHaveLength(1);
36
+ expect(entries[0].data).toMatchObject({ zoom: 0 });
37
+ expect(String((entries[0].data as { message: string }).message)).toContain('nothing is culled');
38
+ });
39
+
40
+ test('stays silent for a camera whose view matrix inverts', () => {
41
+ enableCameraGuards();
42
+ expect(captureLog(() => getCamera2DVisibleBounds(createCamera2D(64, 64), createRectangle()))).toHaveLength(0);
43
+ });
44
+ });
45
+
46
+ afterEach(() => {
47
+ disableCameraGuards();
48
+ });
49
+
50
+ function captureLog(run: () => void): LogEntry[] {
51
+ const entries: LogEntry[] = [];
52
+ setLogSink((entry) => entries.push(entry));
53
+ try {
54
+ run();
55
+ } finally {
56
+ setLogSink(null);
57
+ }
58
+ return entries;
59
+ }
60
+
61
+ function degenerateCamera(): Camera2D {
62
+ return createCamera2D(64, 64, { zoom: 0 });
63
+ }
@@ -4,6 +4,7 @@ import type { OrthographicProjection, PerspectiveProjection } from '@flighthq/ty
4
4
  import {
5
5
  createOrthographicProjection,
6
6
  createPerspectiveProjection,
7
+ getOrthographicProjectionTexelSize,
7
8
  isOrthographicProjection,
8
9
  isPerspectiveProjection,
9
10
  setProjectionMatrix4,
@@ -32,6 +33,15 @@ describe('createPerspectiveProjection', () => {
32
33
  });
33
34
  });
34
35
 
36
+ describe('getOrthographicProjectionTexelSize', () => {
37
+ it('uses the larger world-space texel footprint for a non-square projection and map', () => {
38
+ const projection = createOrthographicProjection({ halfHeight: 20, halfWidth: 10 });
39
+
40
+ expect(getOrthographicProjectionTexelSize(projection, 100, 100)).toBe(0.4);
41
+ expect(getOrthographicProjectionTexelSize(projection, 50, 200)).toBe(0.4);
42
+ });
43
+ });
44
+
35
45
  describe('isOrthographicProjection', () => {
36
46
  it('is true for orthographic and false for perspective', () => {
37
47
  const ortho = createOrthographicProjection({ halfHeight: 1, halfWidth: 1 });
@@ -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
+ });