@expofp/renderer 3.28.0 → 3.28.2

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.
@@ -7,7 +7,8 @@ import { type Camera } from 'three';
7
7
  * (P′, V′) pairs multiply to the same M — but the *rigid* split is the one three's `Camera` class
8
8
  * assumes for `getWorldDirection`, frustum culling, raycasting and the lighting that reads the
9
9
  * camera's world matrix. Any extra scale or skew in M (mercator-unit conversion, say) ends up
10
- * absorbed into P, where it breaks none of those.
10
+ * absorbed into P, where it breaks none of those — a *reflection* being the exception, which is why
11
+ * the host's world is mirrored into three's before any of this runs.
11
12
  * @param camera - The external camera to publish onto; `matrixAutoUpdate` and
12
13
  * `matrixWorldAutoUpdate` must be off, since everything here is written by hand.
13
14
  * @param mainMatrix - The combined matrix mapping world space directly to clip space, sixteen
@@ -1,4 +1,5 @@
1
1
  import { Matrix3, Matrix4, Vector3 } from 'three';
2
+ import { HANDEDNESS_FLIP } from '../space/Space';
2
3
  /**
3
4
  * Decomposes the host's combined world-to-clip matrix into a rigid view matrix and a projection
4
5
  * matrix, publishing all four matrices (and their inverses) on the camera.
@@ -7,14 +8,19 @@ import { Matrix3, Matrix4, Vector3 } from 'three';
7
8
  * (P′, V′) pairs multiply to the same M — but the *rigid* split is the one three's `Camera` class
8
9
  * assumes for `getWorldDirection`, frustum culling, raycasting and the lighting that reads the
9
10
  * camera's world matrix. Any extra scale or skew in M (mercator-unit conversion, say) ends up
10
- * absorbed into P, where it breaks none of those.
11
+ * absorbed into P, where it breaks none of those — a *reflection* being the exception, which is why
12
+ * the host's world is mirrored into three's before any of this runs.
11
13
  * @param camera - The external camera to publish onto; `matrixAutoUpdate` and
12
14
  * `matrixWorldAutoUpdate` must be off, since everything here is written by hand.
13
15
  * @param mainMatrix - The combined matrix mapping world space directly to clip space, sixteen
14
16
  * elements column-major.
15
17
  */
16
18
  export function setExternalCameraProjection(camera, mainMatrix) {
17
- const M = _main.fromArray(mainMatrix);
19
+ // The host's matrix reads the host's world; the space mirrors that world into three's, so undo
20
+ // the mirror here. The two cancel: the plan lands exactly where the host put it, and what is left
21
+ // is a plan → world matrix that reverses orientation, which is the whole point — see
22
+ // {@link HANDEDNESS_FLIP}.
23
+ const M = _main.fromArray(mainMatrix).multiply(HANDEDNESS_FLIP);
18
24
  const e = M.elements; // column-major storage: M[row, col] = e[row + 4 * col]
19
25
  // Step 1: camera world position c.
20
26
  //
@@ -47,8 +47,8 @@ export function Lighting() {
47
47
  .setFromMatrixColumn(state.camera.matrixWorld, 2)
48
48
  .transformDirection(space.matrixInverse);
49
49
  // The plan's own up axis, as a point whose world position lands back on it — which is what three
50
- // reads a hemisphere light's sky axis from. Under the viewbox fit that axis is world -z, the
51
- // side the camera is on; under a caller's own transform it is wherever that transform puts it.
50
+ // reads a hemisphere light's sky axis from. It is world -z either way, the side the camera is
51
+ // on: every path into world space ends with handedness flip.
52
52
  //
53
53
  // Computed here rather than memoized against the space because the space is refitted in place
54
54
  // and so never changes identity — and it is four vector operations on a frame that was going to
@@ -22,8 +22,8 @@ export type PlanPointLike = PointLike & {
22
22
  __space: 'plan';
23
23
  };
24
24
  /**
25
- * A point in **world space**: three's own space, the one the camera is placed in. World z grows *away*
26
- * from the viewer under the viewbox fit, which is why {@link Space} mirrors it.
25
+ * A point in **world space**: three's own space, the one the camera is placed in. Right-handed, so
26
+ * world z grows *away* from the viewer — which is why {@link Space} mirrors the plan's z.
27
27
  *
28
28
  * World and NDC points are three's vectors instead of `@expofp/geometry`: neither is part of the
29
29
  * public API, and both exist only to be multiplied by matrices and unprojected through cameras.
@@ -81,9 +81,9 @@ export interface SpaceOptions {
81
81
  */
82
82
  dpr: number;
83
83
  /**
84
- * A caller-supplied plan → world matrix, 16 elements column-major. When present it *replaces* the
85
- * viewbox fit rather than composing with it, and carries its own z convention — the maplibre
86
- * transform scales z upward where the fit mirrors it.
84
+ * A caller-supplied plan → world matrix, 16 elements column-major, in the host's own coordinates.
85
+ * When present it *replaces* the viewbox fit rather than composing with it; the z mirror both
86
+ * paths end with is not the caller's to supply (see {@link HANDEDNESS_FLIP}).
87
87
  */
88
88
  staticTransform?: number[];
89
89
  }
@@ -97,13 +97,16 @@ export declare function useSpace(): Space;
97
97
  * conversions that ride on it.
98
98
  *
99
99
  * The mapping is one matrix. Without a `staticTransform` it is the **viewbox fit**: centre the
100
- * viewbox on the world origin and scale it uniformly to best fill the drawing buffer, mirroring z so a positive
101
- * elevation lands nearer the camera. That fit is what makes one world unit one device pixel when
102
- * the plan exactly fills the canvas, which in turn is what lets the camera's zoom math be written
103
- * without reference to the scene at all — see {@link Camera}.
100
+ * viewbox on the world origin and scale it uniformly to best fill the drawing buffer. That fit is
101
+ * what makes one world unit one device pixel when the plan exactly fills the canvas, which in turn
102
+ * is what lets the camera's zoom math be written without reference to the scene at all — see
103
+ * {@link Camera}.
104
104
  *
105
105
  * With a `staticTransform` the matrix is whatever the caller supplied, and the fit is not computed:
106
- * a host like maplibre places the plan itself, in its own space and with its own z convention.
106
+ * a host like maplibre places the plan itself, in its own space.
107
+ *
108
+ * Either way the matrix ends with {@link HANDEDNESS_FLIP}, which is what makes the space three's
109
+ * rather than the plan's or a host's — a positive elevation then lands nearer the camera.
107
110
  *
108
111
  * **Stable for the scene's lifetime.** A resize does not build a new space — {@link Scene} refits
109
112
  * this one in place — so every holder reads the current fit through the same object and none of
@@ -285,4 +288,23 @@ export declare const SpaceContext: import("react").Context<Space>;
285
288
  * @param camera - The camera to cast from.
286
289
  */
287
290
  export declare function castRay(raycaster: Raycaster, ndc: NDCPointLike, camera: Camera): void;
291
+ /**
292
+ * The last step of both paths into world space: mirror z.
293
+ *
294
+ * Plan space is left-handed — x right, y **down**, z up — where three's world is right-handed, so
295
+ * one axis has to flip and z is it: the plan's up is world `-z`, the side the camera is on.
296
+ *
297
+ * The flip belongs in the plan → world matrix and nowhere else. three reads a face's winding from
298
+ * that matrix's determinant alone (`matrixWorld.determinantAffine() < 0`) and never from the
299
+ * projection, so a reflection anywhere else is invisible to it: every `DoubleSide` lit surface
300
+ * negates its normal and shades from behind.
301
+ *
302
+ * **Assumes a host's world shares the plan's handedness**, which mercator does — x east, y south,
303
+ * z up — resolving the clash inside its own projection instead (`MercatorTransform._calcMatrices`
304
+ * scales by `[1, -1, 1]`). So the same flip serves both paths, and external mode undoes it on the
305
+ * host's own matrix (`setExternalCameraProjection`). A host with a right-handed world would
306
+ * need it skipped on both — a `leftHandedWorld` flag on the external contract is where that choice
307
+ * would go.
308
+ */
309
+ export declare const HANDEDNESS_FLIP: Matrix4;
288
310
  //# sourceMappingURL=Space.d.ts.map
@@ -17,13 +17,16 @@ export function useSpace() {
17
17
  * conversions that ride on it.
18
18
  *
19
19
  * The mapping is one matrix. Without a `staticTransform` it is the **viewbox fit**: centre the
20
- * viewbox on the world origin and scale it uniformly to best fill the drawing buffer, mirroring z so a positive
21
- * elevation lands nearer the camera. That fit is what makes one world unit one device pixel when
22
- * the plan exactly fills the canvas, which in turn is what lets the camera's zoom math be written
23
- * without reference to the scene at all — see {@link Camera}.
20
+ * viewbox on the world origin and scale it uniformly to best fill the drawing buffer. That fit is
21
+ * what makes one world unit one device pixel when the plan exactly fills the canvas, which in turn
22
+ * is what lets the camera's zoom math be written without reference to the scene at all — see
23
+ * {@link Camera}.
24
24
  *
25
25
  * With a `staticTransform` the matrix is whatever the caller supplied, and the fit is not computed:
26
- * a host like maplibre places the plan itself, in its own space and with its own z convention.
26
+ * a host like maplibre places the plan itself, in its own space.
27
+ *
28
+ * Either way the matrix ends with {@link HANDEDNESS_FLIP}, which is what makes the space three's
29
+ * rather than the plan's or a host's — a positive elevation then lands nearer the camera.
27
30
  *
28
31
  * **Stable for the scene's lifetime.** A resize does not build a new space — {@link Scene} refits
29
32
  * this one in place — so every holder reads the current fit through the same object and none of
@@ -91,8 +94,10 @@ export class Space {
91
94
  // product to come out as scale∘translate — the order the imperative renderer built it in.
92
95
  this.matrix
93
96
  .makeTranslation(-centerX * fit, -centerY * fit, 0)
94
- .scale(new Vector3(fit, fit, -fit));
97
+ .scale(new Vector3(fit, fit, fit));
95
98
  }
99
+ // Both paths land in a host's space or ours; this is what makes either one three's.
100
+ this.matrix.premultiply(HANDEDNESS_FLIP);
96
101
  this.matrixInverse.copy(this.matrix).invert();
97
102
  this._scale = Math.sqrt(axisLength(this.matrix, 0) * axisLength(this.matrix, 1));
98
103
  }
@@ -306,6 +311,25 @@ export function castRay(raycaster, ndc, camera) {
306
311
  function axisLength(matrix, axis) {
307
312
  return new Vector3().setFromMatrixColumn(matrix, axis).length();
308
313
  }
314
+ /**
315
+ * The last step of both paths into world space: mirror z.
316
+ *
317
+ * Plan space is left-handed — x right, y **down**, z up — where three's world is right-handed, so
318
+ * one axis has to flip and z is it: the plan's up is world `-z`, the side the camera is on.
319
+ *
320
+ * The flip belongs in the plan → world matrix and nowhere else. three reads a face's winding from
321
+ * that matrix's determinant alone (`matrixWorld.determinantAffine() < 0`) and never from the
322
+ * projection, so a reflection anywhere else is invisible to it: every `DoubleSide` lit surface
323
+ * negates its normal and shades from behind.
324
+ *
325
+ * **Assumes a host's world shares the plan's handedness**, which mercator does — x east, y south,
326
+ * z up — resolving the clash inside its own projection instead (`MercatorTransform._calcMatrices`
327
+ * scales by `[1, -1, 1]`). So the same flip serves both paths, and external mode undoes it on the
328
+ * host's own matrix (`setExternalCameraProjection`). A host with a right-handed world would
329
+ * need it skipped on both — a `leftHandedWorld` flag on the external contract is where that choice
330
+ * would go.
331
+ */
332
+ export const HANDEDNESS_FLIP = new Matrix4().makeScale(1, 1, -1);
309
333
  /** The plan's own plane in plan space, which every pointer position is reported on. */
310
334
  const PLAN_PLANE = new Plane(new Vector3(0, 0, 1), 0);
311
335
  /**
@@ -12,7 +12,11 @@ export interface SceneDef {
12
12
  background?: ColorInput;
13
13
  /** SVG viewbox */
14
14
  viewbox: Box;
15
- /** Static transform matrix */
15
+ /**
16
+ * Static transform matrix: plan → the host's world, in place of the viewbox fit and in the host's
17
+ * own coordinates. Assumed to share plan space's handedness — see `HANDEDNESS_FLIP` in Space.ts, which the
18
+ * renderer adds on top to reach three's.
19
+ */
16
20
  staticTransform?: number[];
17
21
  }
18
22
  /** Shared properties for all renderable defs */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/renderer",
3
- "version": "3.28.0",
3
+ "version": "3.28.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/expofp/efp-app.git",
@@ -42,7 +42,7 @@
42
42
  "troika-three-text": "^0.52.5",
43
43
  "tslib": "^2.3.0",
44
44
  "webgl-memory": "^1.1.3",
45
- "@expofp/geometry": "3.28.0"
45
+ "@expofp/geometry": "3.28.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@react-three/test-renderer": "^9.1.0",