@expofp/renderer 3.38.0 → 3.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/controls/camera-api.d.ts +28 -0
  2. package/dist/controls/camera-api.js +33 -10
  3. package/dist/controls/hooks.js +2 -0
  4. package/dist/core/InstanceUniforms.d.ts +15 -0
  5. package/dist/core/InstanceUniforms.js +32 -0
  6. package/dist/core/batch/BatchedMesh.js +6 -0
  7. package/dist/core/instanced/instanced-material.js +5 -1
  8. package/dist/core/lines/BatchedLines.d.ts +9 -7
  9. package/dist/core/lines/BatchedLines.js +9 -7
  10. package/dist/core/lines/{FrustumCulling.d.ts → FrustumCullingLines.d.ts} +7 -6
  11. package/dist/core/lines/{FrustumCulling.js → FrustumCullingLines.js} +2 -2
  12. package/dist/core/lines/{Raycasting.d.ts → RaycastingLines.d.ts} +7 -6
  13. package/dist/core/lines/{Raycasting.js → RaycastingLines.js} +2 -2
  14. package/dist/core/lines/lines-material.js +10 -1
  15. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  16. package/dist/core/sprites/BatchedSprites.js +205 -0
  17. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  18. package/dist/core/sprites/RaycastingSprites.js +71 -0
  19. package/dist/core/sprites/sprite-material.d.ts +59 -0
  20. package/dist/core/sprites/sprite-material.js +192 -0
  21. package/dist/core/text/BatchedText.d.ts +6 -0
  22. package/dist/core/text/BatchedText.js +6 -0
  23. package/dist/core/text/MemberTexture.js +55 -1
  24. package/dist/core/text/Text.d.ts +16 -1
  25. package/dist/core/text/Text.js +15 -0
  26. package/dist/core/text/batched-text-material.js +4 -2
  27. package/dist/objects/containers/hooks.d.ts +7 -2
  28. package/dist/objects/containers/hooks.js +20 -5
  29. package/dist/objects/containers/mesh-writes.js +3 -3
  30. package/dist/objects/images/ImageInstance.js +103 -35
  31. package/dist/objects/images/Images.d.ts +8 -7
  32. package/dist/objects/images/Images.js +94 -28
  33. package/dist/objects/images/image-container.d.ts +14 -3
  34. package/dist/objects/images/image-container.js +19 -5
  35. package/dist/objects/layer/Layer.d.ts +7 -6
  36. package/dist/objects/layer/Layer.js +40 -30
  37. package/dist/objects/layer/layer-context.d.ts +41 -0
  38. package/dist/objects/layer/layer-context.js +45 -0
  39. package/dist/objects/materials/ColorMaterial.js +16 -3
  40. package/dist/objects/materials/LineMaterial.js +4 -2
  41. package/dist/objects/materials/TextureMaterial.js +4 -2
  42. package/dist/objects/materials/effects/color-dim.d.ts +1 -10
  43. package/dist/objects/materials/effects/color-dim.js +4 -12
  44. package/dist/objects/materials/effects/effects-context.d.ts +6 -5
  45. package/dist/objects/materials/shared.d.ts +18 -0
  46. package/dist/objects/materials/shared.js +21 -0
  47. package/dist/objects/models/Models.js +1 -8
  48. package/dist/objects/shapes/Shapes.js +2 -9
  49. package/dist/objects/text/TextInstance.js +1 -1
  50. package/dist/objects/text/text-layout.d.ts +13 -4
  51. package/dist/objects/text/text-layout.js +30 -4
  52. package/dist/space/Space.d.ts +15 -0
  53. package/dist/space/Space.js +16 -10
  54. package/dist/types/defs.d.ts +37 -0
  55. package/dist/utils/logging.d.ts +6 -3
  56. package/dist/utils/logging.js +15 -7
  57. package/dist/utils/math.d.ts +11 -0
  58. package/dist/utils/math.js +14 -0
  59. package/package.json +2 -2
@@ -147,6 +147,14 @@ interface CameraContext {
147
147
  height: number;
148
148
  /** Device pixels per css pixel. */
149
149
  dpr: number;
150
+ /**
151
+ * Asks React Three Fiber to draw another frame.
152
+ *
153
+ * A command changes the controls directly. That change happens outside a React commit. React
154
+ * Three Fiber cannot see it on its own. Calling this schedules the frame that shows the change,
155
+ * under a demand frame loop that would otherwise never draw one.
156
+ */
157
+ invalidate: () => void;
150
158
  }
151
159
  /**
152
160
  * The camera commands, over whatever the scene currently holds.
@@ -181,6 +189,26 @@ export declare class CameraCommands implements CameraAPI {
181
189
  pitchBy(degrees: number, options?: TransitionOptions): Promise<void>;
182
190
  /** @inheritdoc */
183
191
  reset(axes?: CameraAxes, options?: TransitionOptions): Promise<void>;
192
+ /**
193
+ * Runs a move against the controls and asks for the frame that shows it.
194
+ *
195
+ * Every method above that takes {@link TransitionOptions} funnels its move through this one
196
+ * helper. A method that calls `controls` directly instead skips it, and looks obviously unlike
197
+ * its siblings.
198
+ *
199
+ * The invalidate happens before the move settles, not after, and every time, not only when the
200
+ * move is immediate. An eased move already triggers its own invalidate through `transitionstart`.
201
+ * An immediate move fires no event at all. `camera-controls` only applies an immediate move inside
202
+ * its own `update`, which runs from a frame that only an invalidate schedules. Invalidating after
203
+ * the move's promise settles would be too late to schedule that frame.
204
+ *
205
+ * A field and not a method, so it never lands on `CameraCommands.prototype`. The guard spec walks
206
+ * that prototype and calls everything it finds there, and an implementation detail would fail that
207
+ * walk for no reason.
208
+ * @param move - Issues the move against the controls and returns its promise.
209
+ * @returns The same promise `move` returned.
210
+ */
211
+ private withInvalidate;
184
212
  }
185
213
  /**
186
214
  * How far the plan is turned from north: clockwise, in radians, in `[0, 2π)`.
@@ -35,17 +35,17 @@ export class CameraCommands {
35
35
  if (!fit)
36
36
  return;
37
37
  const transition = !options?.immediate;
38
- await Promise.all([
38
+ await this.withInvalidate(() => Promise.all([
39
39
  controls.moveTo(fit.center.x, fit.center.y, 0, transition),
40
40
  controls.dollyTo(camera.zoomFactorToDistance(fit.zoom), transition),
41
- ]);
41
+ ]));
42
42
  }
43
43
  /** @inheritdoc */
44
44
  zoomBy(factor, options) {
45
45
  const { controls, camera } = this.context;
46
46
  if (!controls || !camera)
47
47
  return RESOLVED;
48
- return controls.dollyTo(camera.zoomFactorToDistance(camera.zoomFactor * factor), !options?.immediate);
48
+ return this.withInvalidate(() => controls.dollyTo(camera.zoomFactorToDistance(camera.zoomFactor * factor), !options?.immediate));
49
49
  }
50
50
  /** @inheritdoc */
51
51
  panTo(point, options) {
@@ -53,7 +53,7 @@ export class CameraCommands {
53
53
  if (!controls)
54
54
  return RESOLVED;
55
55
  const world = toWorld(space, point);
56
- return controls.moveTo(world.x, world.y, world.z, !options?.immediate);
56
+ return this.withInvalidate(() => controls.moveTo(world.x, world.y, world.z, !options?.immediate));
57
57
  }
58
58
  /** @inheritdoc */
59
59
  panBy(offset, options) {
@@ -65,7 +65,7 @@ export class CameraCommands {
65
65
  const origin = toWorld(space, ORIGIN, _origin);
66
66
  const shifted = toWorld(space, offset);
67
67
  const current = controls.getTarget(_current);
68
- return controls.moveTo(current.x + shifted.x - origin.x, current.y + shifted.y - origin.y, current.z + shifted.z - origin.z, !options?.immediate);
68
+ return this.withInvalidate(() => controls.moveTo(current.x + shifted.x - origin.x, current.y + shifted.y - origin.y, current.z + shifted.z - origin.z, !options?.immediate));
69
69
  }
70
70
  /** @inheritdoc */
71
71
  rollTo(degrees, options) {
@@ -75,14 +75,14 @@ export class CameraCommands {
75
75
  // Whichever way round is shorter, which is what keeps a turn from 350° to 10° going the long
76
76
  // way about.
77
77
  const delta = angleDiff(-degToRad(degrees), controls.azimuthAngle);
78
- return controls.rotateAzimuthTo(controls.azimuthAngle + delta, !options?.immediate);
78
+ return this.withInvalidate(() => controls.rotateAzimuthTo(controls.azimuthAngle + delta, !options?.immediate));
79
79
  }
80
80
  /** @inheritdoc */
81
81
  rollBy(degrees, options) {
82
82
  const { controls } = this.context;
83
83
  if (!controls)
84
84
  return RESOLVED;
85
- return controls.rotateAzimuthTo(controls.azimuthAngle - degToRad(degrees), !options?.immediate);
85
+ return this.withInvalidate(() => controls.rotateAzimuthTo(controls.azimuthAngle - degToRad(degrees), !options?.immediate));
86
86
  }
87
87
  /** @inheritdoc */
88
88
  pitchTo(degrees, options) {
@@ -90,14 +90,14 @@ export class CameraCommands {
90
90
  if (!controls)
91
91
  return RESOLVED;
92
92
  // Held inside the pitch limits by the controls themselves, which is where those limits live.
93
- return controls.rotatePolarTo(degToRad(degrees), !options?.immediate);
93
+ return this.withInvalidate(() => controls.rotatePolarTo(degToRad(degrees), !options?.immediate));
94
94
  }
95
95
  /** @inheritdoc */
96
96
  pitchBy(degrees, options) {
97
97
  const { controls } = this.context;
98
98
  if (!controls)
99
99
  return RESOLVED;
100
- return controls.rotatePolarTo(controls.polarAngle + degToRad(degrees), !options?.immediate);
100
+ return this.withInvalidate(() => controls.rotatePolarTo(controls.polarAngle + degToRad(degrees), !options?.immediate));
101
101
  }
102
102
  /** @inheritdoc */
103
103
  async reset(axes = ALL_AXES, options) {
@@ -119,8 +119,31 @@ export class CameraCommands {
119
119
  moves.push(controls.normalizeRotations().rotateAzimuthTo(0, transition));
120
120
  if (axes.pitch)
121
121
  moves.push(controls.rotatePolarTo(controls.minPolarAngle, transition));
122
- await Promise.all(moves);
122
+ await this.withInvalidate(() => Promise.all(moves));
123
123
  }
124
+ /**
125
+ * Runs a move against the controls and asks for the frame that shows it.
126
+ *
127
+ * Every method above that takes {@link TransitionOptions} funnels its move through this one
128
+ * helper. A method that calls `controls` directly instead skips it, and looks obviously unlike
129
+ * its siblings.
130
+ *
131
+ * The invalidate happens before the move settles, not after, and every time, not only when the
132
+ * move is immediate. An eased move already triggers its own invalidate through `transitionstart`.
133
+ * An immediate move fires no event at all. `camera-controls` only applies an immediate move inside
134
+ * its own `update`, which runs from a frame that only an invalidate schedules. Invalidating after
135
+ * the move's promise settles would be too late to schedule that frame.
136
+ *
137
+ * A field and not a method, so it never lands on `CameraCommands.prototype`. The guard spec walks
138
+ * that prototype and calls everything it finds there, and an implementation detail would fail that
139
+ * walk for no reason.
140
+ * @param move - Issues the move against the controls and returns its promise.
141
+ * @returns The same promise `move` returned.
142
+ */
143
+ withInvalidate = (move) => {
144
+ this.context.invalidate();
145
+ return move();
146
+ };
124
147
  }
125
148
  /**
126
149
  * How far the plan is turned from north: clockwise, in radians, in `[0, 2π)`.
@@ -182,6 +182,7 @@ const _target = new Vector3();
182
182
  */
183
183
  export function useCameraAPI() {
184
184
  const get = useThree((state) => state.get);
185
+ const invalidate = useThree((state) => state.invalidate);
185
186
  const controls = useCameraControls();
186
187
  const camera = useCamera();
187
188
  const space = useSpace();
@@ -194,5 +195,6 @@ export function useCameraAPI() {
194
195
  width,
195
196
  height,
196
197
  dpr,
198
+ invalidate,
197
199
  });
198
200
  }
@@ -29,6 +29,21 @@ export declare class InstanceUniforms {
29
29
  texture: SquareDataTexture | null;
30
30
  /** Everything declared so far, merged across calls, so a rebuild recomputes the whole layout. */
31
31
  private declared;
32
+ /** Backs {@link shaderKey}, rebuilt whenever {@link declared} changes. */
33
+ private key;
34
+ /**
35
+ * What the declared uniforms generate, as one string.
36
+ *
37
+ * A material that fetches per-instance uniforms compiles different GLSL for every schema, and
38
+ * three caches a compiled program under `customProgramCacheKey` alone, with no way to compare
39
+ * source. So every patcher that injects this store's prologue folds this into its own key.
40
+ * Without it, two meshes whose schemas differ share one program and the second silently draws
41
+ * against the first's layout.
42
+ *
43
+ * Empty while nothing is declared, which is itself a distinct key: that program has no prologue.
44
+ * @returns The key, stable for as long as the schema is.
45
+ */
46
+ get shaderKey(): string;
32
47
  /**
33
48
  * Declares uniforms, merging with any already declared and rebuilding the texture.
34
49
  *
@@ -21,6 +21,23 @@ export class InstanceUniforms {
21
21
  texture = null;
22
22
  /** Everything declared so far, merged across calls, so a rebuild recomputes the whole layout. */
23
23
  declared = {};
24
+ /** Backs {@link shaderKey}, rebuilt whenever {@link declared} changes. */
25
+ key = '';
26
+ /**
27
+ * What the declared uniforms generate, as one string.
28
+ *
29
+ * A material that fetches per-instance uniforms compiles different GLSL for every schema, and
30
+ * three caches a compiled program under `customProgramCacheKey` alone, with no way to compare
31
+ * source. So every patcher that injects this store's prologue folds this into its own key.
32
+ * Without it, two meshes whose schemas differ share one program and the second silently draws
33
+ * against the first's layout.
34
+ *
35
+ * Empty while nothing is declared, which is itself a distinct key: that program has no prologue.
36
+ * @returns The key, stable for as long as the schema is.
37
+ */
38
+ get shaderKey() {
39
+ return this.key;
40
+ }
24
41
  /**
25
42
  * Declares uniforms, merging with any already declared and rebuilding the texture.
26
43
  *
@@ -42,6 +59,7 @@ export class InstanceUniforms {
42
59
  }
43
60
  this.texture = texture;
44
61
  this.declared = { vertex, fragment };
62
+ this.key = schemaKey(this.declared);
45
63
  }
46
64
  /**
47
65
  * Reads one instance's uniform.
@@ -106,6 +124,7 @@ export class InstanceUniforms {
106
124
  vertex: { ...source.declared.vertex },
107
125
  fragment: { ...source.declared.fragment },
108
126
  };
127
+ this.key = source.key;
109
128
  this.texture = source.texture ? source.texture.clone() : null;
110
129
  }
111
130
  /** Frees the texture. */
@@ -210,6 +229,19 @@ function uniformSize(type) {
210
229
  throw new Error(`InstanceUniforms: invalid uniform type "${String(type)}".`);
211
230
  }
212
231
  }
232
+ /**
233
+ * Builds the key that stands for the GLSL a schema generates. See {@link InstanceUniforms.shaderKey}.
234
+ *
235
+ * Names, types and the stage each is read in, because all three reach the generated source. Left in
236
+ * declaration order rather than sorted, since that is the order the layout packs them in, so two
237
+ * schemas that differ only in order still pack differently and have to compile apart.
238
+ * @param schema - The declared uniforms.
239
+ * @returns The key.
240
+ */
241
+ function schemaKey(schema) {
242
+ const stage = (prefix, uniforms = {}) => Object.entries(uniforms).map(([name, type]) => `${prefix}${name}:${type}`);
243
+ return [...stage('v', schema.vertex), ...stage('f', schema.fragment)].join(',');
244
+ }
213
245
  /**
214
246
  * Flattens a schema into the name-to-type pairs a rebuild has to carry across.
215
247
  * @param schema - The declared uniforms.
@@ -131,6 +131,12 @@ export class BatchedMeshImpl extends BatchedMeshBase {
131
131
  const batchIndexGlsl = /* glsl */ `float ${batchIndex} = getIndirectIndex( gl_DrawID ) + 0.5;`;
132
132
  this.patchedMaterial = material;
133
133
  const onBeforeCompileBase = material.onBeforeCompile.bind(material);
134
+ // three caches a compiled program under this key alone and compares no source. The prologue
135
+ // below changes the GLSL, and the schema decides how, so both have to reach the key. Without
136
+ // it, two batches whose schemas differ share one program and the second draws against the
137
+ // first's layout. Bound rather than saved raw, because this patch is one-shot and never undone.
138
+ const cacheKeyBase = material.customProgramCacheKey.bind(material);
139
+ material.customProgramCacheKey = () => `efpBatched_${this.uniforms.shaderKey}_${cacheKeyBase()}`;
134
140
  material.onBeforeCompile = (shader, renderer) => {
135
141
  if (this.uniforms.patchShader(shader, batchIndex)) {
136
142
  // Declares the index the prologue just injected reads. Written after it precisely so it runs
@@ -154,7 +154,11 @@ export function patchInstancedMaterial(material, textures) {
154
154
  // before a texture was added, since each changes the generated GLSL. Every texture that can arrive
155
155
  // late is named: the matrices are allocated by the first transform written, and a program built
156
156
  // before that reads identity.
157
- const key = () => `${!!textures.matricesTexture}_${!!textures.colorsTexture}_${!!textures.uniforms.texture}`;
157
+ //
158
+ // The uniforms contribute their whole schema and not just their presence. Two meshes that declare
159
+ // different uniforms generate different GLSL, so a key that recorded only "has uniforms" would let
160
+ // the second draw against the first's layout.
161
+ const key = () => `${!!textures.matricesTexture}_${!!textures.colorsTexture}_${textures.uniforms.shaderKey}`;
158
162
  material.customProgramCacheKey = () => `efp_${key()}_${baseCacheKey.call(material)}`;
159
163
  material.onBeforeCompile = (shader, renderer) => {
160
164
  // Instancing first, the material's own handler after — the same order the batched meshes use,
@@ -2,8 +2,8 @@ import { type Box3, type BufferGeometry, type Camera, type Group, type Material,
2
2
  import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
3
3
  import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
4
4
  import { InstancedMeshImpl, type InstancedMeshImplParams } from '../instanced/InstancedMesh';
5
- import { FrustumCulling } from './FrustumCulling';
6
- import { Raycasting } from './Raycasting';
5
+ import { FrustumCullingLines } from './FrustumCullingLines';
6
+ import { RaycastingLines } from './RaycastingLines';
7
7
  /**
8
8
  * An instanced mesh whose instances are placed by two endpoints instead of a matrix.
9
9
  *
@@ -21,7 +21,7 @@ import { Raycasting } from './Raycasting';
21
21
  * names and needs no rewriting beyond dropping the declarations — see `objects/materials/LineMaterial`.
22
22
  * - **The bounds**, swept from the endpoints, because the template geometry's own are in the
23
23
  * parametric space the shader expands from and mean nothing in the plan.
24
- * - **Picking**, which has to run in screen space at each member's own width — see {@link Raycasting}.
24
+ * - **Picking**, which has to run in screen space at each member's own width — see {@link RaycastingLines}.
25
25
  *
26
26
  * And what it stops needing by being one: the endpoints are addressed by id, so paint order is a
27
27
  * `customSort` over the draw list like every other batch's rather than a packing of the buffer;
@@ -32,6 +32,8 @@ import { Raycasting } from './Raycasting';
32
32
  * Render it with {@link BatchedLines}.
33
33
  */
34
34
  export declare class BatchedLinesImpl extends InstancedMeshImpl {
35
+ /** Marks this as a line batch. three's brand pattern, so a holder can tell the batch kinds apart. */
36
+ readonly isBatchedLines = true;
35
37
  /**
36
38
  * The segment template every member is drawn from, and the whole of what the geometry carries: the
37
39
  * per-instance data three would put in instanced attributes is in the uniforms instead.
@@ -51,10 +53,10 @@ export declare class BatchedLinesImpl extends InstancedMeshImpl {
51
53
  material: LineMaterial;
52
54
  /** Draw order is paint order, which a container decides through `customSort` rather than depth. */
53
55
  sortObjects: boolean;
54
- /** Screen space, at each member's own width — see {@link Raycasting}. */
55
- protected raycasting: Raycasting;
56
- /** By the ribbon each member draws rather than the segment it runs — see {@link FrustumCulling}. */
57
- protected culling: FrustumCulling;
56
+ /** Screen space, at each member's own width — see {@link RaycastingLines}. */
57
+ protected raycasting: RaycastingLines;
58
+ /** By the ribbon each member draws rather than the segment it runs — see {@link FrustumCullingLines}. */
59
+ protected culling: FrustumCullingLines;
58
60
  /**
59
61
  * @param params - Instance capacity; the buffers grow past it on demand.
60
62
  */
@@ -3,9 +3,9 @@ import { Vector3, Vector4, } from 'three';
3
3
  import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
4
4
  import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
5
5
  import { InstancedMeshImpl } from '../instanced/InstancedMesh';
6
- import { FrustumCulling } from './FrustumCulling';
6
+ import { FrustumCullingLines } from './FrustumCullingLines';
7
7
  import { END_UNIFORM, patchLineMaterial, START_UNIFORM, WIDTH_UNIFORM } from './lines-material';
8
- import { Raycasting } from './Raycasting';
8
+ import { RaycastingLines } from './RaycastingLines';
9
9
  /**
10
10
  * An instanced mesh whose instances are placed by two endpoints instead of a matrix.
11
11
  *
@@ -23,7 +23,7 @@ import { Raycasting } from './Raycasting';
23
23
  * names and needs no rewriting beyond dropping the declarations — see `objects/materials/LineMaterial`.
24
24
  * - **The bounds**, swept from the endpoints, because the template geometry's own are in the
25
25
  * parametric space the shader expands from and mean nothing in the plan.
26
- * - **Picking**, which has to run in screen space at each member's own width — see {@link Raycasting}.
26
+ * - **Picking**, which has to run in screen space at each member's own width — see {@link RaycastingLines}.
27
27
  *
28
28
  * And what it stops needing by being one: the endpoints are addressed by id, so paint order is a
29
29
  * `customSort` over the draw list like every other batch's rather than a packing of the buffer;
@@ -34,6 +34,8 @@ import { Raycasting } from './Raycasting';
34
34
  * Render it with {@link BatchedLines}.
35
35
  */
36
36
  export class BatchedLinesImpl extends InstancedMeshImpl {
37
+ /** Marks this as a line batch. three's brand pattern, so a holder can tell the batch kinds apart. */
38
+ isBatchedLines = true;
37
39
  /**
38
40
  * The segment template every member is drawn from, and the whole of what the geometry carries: the
39
41
  * per-instance data three would put in instanced attributes is in the uniforms instead.
@@ -53,10 +55,10 @@ export class BatchedLinesImpl extends InstancedMeshImpl {
53
55
  material = new LineMaterial();
54
56
  /** Draw order is paint order, which a container decides through `customSort` rather than depth. */
55
57
  sortObjects = true;
56
- /** Screen space, at each member's own width — see {@link Raycasting}. */
57
- raycasting = new Raycasting();
58
- /** By the ribbon each member draws rather than the segment it runs — see {@link FrustumCulling}. */
59
- culling = new FrustumCulling();
58
+ /** Screen space, at each member's own width — see {@link RaycastingLines}. */
59
+ raycasting = new RaycastingLines();
60
+ /** By the ribbon each member draws rather than the segment it runs — see {@link FrustumCullingLines}. */
61
+ culling = new FrustumCullingLines();
60
62
  /**
61
63
  * @param params - Instance capacity; the buffers grow past it on demand.
62
64
  */
@@ -1,8 +1,8 @@
1
1
  import { type Camera, type Sphere } from 'three';
2
2
  import { type LineMaterial } from 'three/addons/lines/LineMaterial.js';
3
- import { type CullingHost as InstancedCullingHost, FrustumCulling as InstancedFrustumCulling } from '../instanced/FrustumCulling';
4
- /** What {@link FrustumCulling} reads off the batch it culls, beyond what every instanced mesh offers. */
5
- export interface CullingHost extends InstancedCullingHost {
3
+ import { type CullingHost, FrustumCulling } from '../instanced/FrustumCulling';
4
+ /** What {@link FrustumCullingLines} reads off the batch it culls, beyond what every instanced mesh offers. */
5
+ interface CullingHostLines extends CullingHost {
6
6
  /** The material, whose width a member with none of its own draws at, and whose units they are in. */
7
7
  material: LineMaterial;
8
8
  /**
@@ -25,8 +25,8 @@ export interface CullingHost extends InstancedCullingHost {
25
25
  * per member rather than one figure for the batch — a member already knows its own width, and a batch
26
26
  * of hairlines beside one wide route would otherwise be padded by the widest of them.
27
27
  */
28
- export declare class FrustumCulling extends InstancedFrustumCulling {
29
- protected instanceSphere(host: CullingHost, id: number, camera: Camera, target: Sphere): Sphere;
28
+ export declare class FrustumCullingLines extends FrustumCulling {
29
+ protected instanceSphere(host: CullingHostLines, id: number, camera: Camera, target: Sphere): Sphere;
30
30
  /**
31
31
  * How far past its endpoints one member draws, in the units its bounds are measured in.
32
32
  *
@@ -40,4 +40,5 @@ export declare class FrustumCulling extends InstancedFrustumCulling {
40
40
  */
41
41
  private drawnMargin;
42
42
  }
43
- //# sourceMappingURL=FrustumCulling.d.ts.map
43
+ export {};
44
+ //# sourceMappingURL=FrustumCullingLines.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { Vector3, Vector4 } from 'three';
2
- import { FrustumCulling as InstancedFrustumCulling, } from '../instanced/FrustumCulling';
2
+ import { FrustumCulling } from '../instanced/FrustumCulling';
3
3
  /**
4
4
  * Culling for a line batch: each member tested by where it is *drawn*, not where it runs.
5
5
  *
@@ -13,7 +13,7 @@ import { FrustumCulling as InstancedFrustumCulling, } from '../instanced/Frustum
13
13
  * per member rather than one figure for the batch — a member already knows its own width, and a batch
14
14
  * of hairlines beside one wide route would otherwise be padded by the widest of them.
15
15
  */
16
- export class FrustumCulling extends InstancedFrustumCulling {
16
+ export class FrustumCullingLines extends FrustumCulling {
17
17
  instanceSphere(host, id, camera, target) {
18
18
  const sphere = super.instanceSphere(host, id, camera, target);
19
19
  sphere.radius += this.drawnMargin(host, id, camera, sphere);
@@ -1,8 +1,8 @@
1
1
  import { type Object3D, Vector3 } from 'three';
2
2
  import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
3
- import { type RaycastHost as InstancedRaycastHost, Raycasting as InstancedRaycasting } from '../instanced/Raycasting';
4
- /** What {@link Raycasting} reads off the batch it tests, beyond what every instanced mesh offers. */
5
- export interface RaycastHost extends InstancedRaycastHost {
3
+ import { type RaycastHost, Raycasting } from '../instanced/Raycasting';
4
+ /** What {@link RaycastingLines} reads off the batch it tests, beyond what every instanced mesh offers. */
5
+ interface RaycastHostLines extends RaycastHost {
6
6
  /** The material, whose width a member with none of its own draws at. */
7
7
  material: LineMaterial;
8
8
  /**
@@ -38,7 +38,8 @@ export interface RaycastHost extends InstancedRaycastHost {
38
38
  * scratch carries a material of its own, written per test, and nothing touches the batch's — which
39
39
  * is in use by the draw the pointer is moving over.
40
40
  */
41
- export declare class Raycasting extends InstancedRaycasting {
42
- protected prepareInstance(host: RaycastHost, objectIndex: number): Object3D;
41
+ export declare class RaycastingLines extends Raycasting {
42
+ protected prepareInstance(host: RaycastHostLines, objectIndex: number): Object3D;
43
43
  }
44
- //# sourceMappingURL=Raycasting.d.ts.map
44
+ export {};
45
+ //# sourceMappingURL=RaycastingLines.d.ts.map
@@ -2,7 +2,7 @@ import { Box3, Sphere, Vector3 } from 'three';
2
2
  import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
3
3
  import { LineSegments2 } from 'three/addons/lines/LineSegments2.js';
4
4
  import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
5
- import { Raycasting as InstancedRaycasting, } from '../instanced/Raycasting';
5
+ import { Raycasting } from '../instanced/Raycasting';
6
6
  /**
7
7
  * Picking for a line batch: each member tested in screen space, at the width it actually draws at.
8
8
  *
@@ -22,7 +22,7 @@ import { Raycasting as InstancedRaycasting, } from '../instanced/Raycasting';
22
22
  * scratch carries a material of its own, written per test, and nothing touches the batch's — which
23
23
  * is in use by the draw the pointer is moving over.
24
24
  */
25
- export class Raycasting extends InstancedRaycasting {
25
+ export class RaycastingLines extends Raycasting {
26
26
  prepareInstance(host, objectIndex) {
27
27
  host.getPointsAt(objectIndex, _start, _end);
28
28
  // Written straight into the interleaved buffer the geometry already owns. `setPositions` would
@@ -28,14 +28,23 @@ export function patchLineMaterial(material) {
28
28
  seedColorChunk(material);
29
29
  // Saved raw rather than bound, for the reason `patchInstancedMaterial` spells out: restoring a
30
30
  // bound copy would put a *different* function back, and the next draw would bind that one in turn.
31
- /* eslint-disable-next-line @typescript-eslint/unbound-method -- restored verbatim; called with `.call`. */
31
+ /* eslint-disable @typescript-eslint/unbound-method -- restored verbatim; called with `.call`. */
32
32
  const baseOnBeforeCompile = material.onBeforeCompile;
33
+ const baseCacheKey = material.customProgramCacheKey;
34
+ /* eslint-enable @typescript-eslint/unbound-method */
35
+ // three caches a compiled program under this key alone. A patched line material compiles source a
36
+ // plain one does not, so it needs a key of its own. A constant suffix is enough, because what this
37
+ // patch injects is fixed. `seedColorChunk` happens to change the source three derives its own
38
+ // shader id from, which has masked the absence of this until now. That is incidental, and a
39
+ // material that stopped needing the seed would lose the protection with it.
40
+ material.customProgramCacheKey = () => `efpLine_${baseCacheKey.call(material)}`;
33
41
  material.onBeforeCompile = (shader, renderer) => {
34
42
  patchLineShader(shader);
35
43
  baseOnBeforeCompile.call(material, shader, renderer);
36
44
  };
37
45
  return () => {
38
46
  material.onBeforeCompile = baseOnBeforeCompile;
47
+ material.customProgramCacheKey = baseCacheKey;
39
48
  };
40
49
  }
41
50
  /**
@@ -0,0 +1,115 @@
1
+ import { type BufferGeometry, type Camera, type Group, type Material, PlaneGeometry, type Scene, type Sphere, Vector2, type WebGLRenderer } from 'three';
2
+ import { InstancedMeshImpl, type InstancedMeshImplParams } from '../instanced/InstancedMesh';
3
+ import { RaycastingSprites } from './RaycastingSprites';
4
+ /**
5
+ * Whether an instanced mesh is a sprite batch.
6
+ *
7
+ * three's own brand pattern, which `isLineMaterial` in `BatchedLines` already follows here. A holder
8
+ * that keeps one mesh field for several batch kinds narrows it with this, instead of keeping a
9
+ * second field that says which kind it stored.
10
+ * @param object - The instanced mesh to test.
11
+ * @returns Whether it is a sprite batch.
12
+ */
13
+ export declare function isBatchedSprites(object: InstancedMeshImpl): object is BatchedSpritesImpl;
14
+ /**
15
+ * An instanced mesh whose every member faces the camera.
16
+ *
17
+ * The name means what it says, which is why this is a class and not a flag. A batch of sprites is
18
+ * homogeneous. One atlas is then drawn by two meshes over two disjoint subsets of its images,
19
+ * instead of one mesh learning about orientation, and the packing never hears about the distinction.
20
+ *
21
+ * What facing the camera changes, and nothing else does:
22
+ *
23
+ * - The projection, in the shader. See `sprite-material.ts`. Everything else an instanced mesh
24
+ * does applies unchanged: slots, visibility, growth, per-instance uniforms, the draw list that is
25
+ * also the paint order, and the material wiring.
26
+ * - The anchor, as a per-instance uniform rather than a fold into the matrix. A sprite pivots
27
+ * about its anchor, so the matrix translation has to be the anchor.
28
+ * - The bounds, which enclose the quad about that anchor. See `getBoundingSphereAt`.
29
+ * - Picking, which has to run three's own sprite algorithm through a scratch object. See
30
+ * `RaycastingSprites`.
31
+ *
32
+ * Members are ids, not `Sprite` objects. `BatchedTextImpl` takes `TextImpl` members because troika's
33
+ * layout is object shaped and asynchronous, and the batch has to pack what those objects produce. A
34
+ * sprite carries none of that. It is a matrix, an anchor and a color, and every one of those
35
+ * already lives in a data texture this mesh owns. Reusing three's type as the member representation
36
+ * would reinstate the object-per-instance layer the port dropped, and would import `SpriteMaterial`
37
+ * semantics this package does not implement. Three's type is reused where it costs nothing, as
38
+ * picking scratch.
39
+ *
40
+ * This class knows nothing about textures. A uv region belongs to `TextureMaterial` and to the layer
41
+ * that packs an atlas. A batch drawn with a plain color material works exactly as well, and tints
42
+ * its members through `setColorAt` like any instanced mesh.
43
+ *
44
+ * Render it with `BatchedSprites`.
45
+ */
46
+ export declare class BatchedSpritesImpl extends InstancedMeshImpl {
47
+ /** Marks this as a sprite batch. See `isBatchedSprites`. */
48
+ readonly isBatchedSprites = true;
49
+ /**
50
+ * The quad every member is drawn from, shared by every sprite batch in the process. That is
51
+ * three's own arrangement, which builds one module-level geometry and lets every `Sprite` share it.
52
+ *
53
+ * Seeded here rather than mounted as a child element, because it is not a choice the way an atlas's
54
+ * quad is. A sprite is a quad, any z is flattened by the projection, and every non-quad
55
+ * silhouette is a matter of texture alpha. `InstancedMeshImpl` already seeds a throwaway geometry
56
+ * expecting R3F to replace it, and not rendering a replacement is what makes this one stand.
57
+ *
58
+ * Two consequences. Nothing may dispose it, so this class does not override `dispose`. Nothing may
59
+ * write its bounding sphere, so `getBoundingSphereAt` computes rather than inflating.
60
+ */
61
+ geometry: PlaneGeometry;
62
+ /** Three's own sprite algorithm, reached through a scratch object. See `RaycastingSprites`. */
63
+ protected raycasting: RaycastingSprites;
64
+ /**
65
+ * @param params - Instance capacity. The buffers grow past it on demand.
66
+ */
67
+ constructor(params?: InstancedMeshImplParams);
68
+ /**
69
+ * A member's anchor. It is how far the pivot sits from the quad's middle, in unit-quad lengths, y
70
+ * down, and zero is the middle.
71
+ * @param id - The member to read.
72
+ * @param target - Receives the result.
73
+ * @returns The anchor.
74
+ */
75
+ getAnchorAt(id: number, target?: Vector2): Vector2;
76
+ /**
77
+ * Sets a member's anchor, and invalidates the bounds it decides.
78
+ * @param id - The member to write.
79
+ * @param x - How far right of the middle the pivot sits, in unit-quad lengths.
80
+ * @param y - How far below the middle the pivot sits, in unit-quad lengths.
81
+ */
82
+ setAnchorAt(id: number, x: number, y: number): void;
83
+ /**
84
+ * Encloses one member: the sphere about its anchor that reaches the quad's furthest corner.
85
+ *
86
+ * The anchor is not folded into the matrix, so the matrix translation already is the center and
87
+ * only the radius needs correcting. That is the exact enclosing sphere for a quad pivoting about
88
+ * its anchor, up to the max-axis approximation `Sphere.applyMatrix4` already makes on the flat
89
+ * path. It also holds at every camera angle, which is what a sprite's bounds have to do.
90
+ *
91
+ * Two alternatives were rejected. Inflating the shared quad's own bounding sphere replaces a
92
+ * minimal sphere with a non-minimal one, needs a per-instance factor on a per-geometry field, and
93
+ * would propagate through every sprite batch in the process. Reading the anchor inside
94
+ * `InstancedMeshImpl.getBoundingSphereAt` is quad math in a class whose instances are arbitrary
95
+ * geometry, where `ModelContainer` is a live counter-example.
96
+ * @param id - The member.
97
+ * @param target - Receives the result.
98
+ * @returns The bounds.
99
+ */
100
+ getBoundingSphereAt(id: number, target: Sphere): Sphere;
101
+ onBeforeRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group): void;
102
+ protected patchMaterial(material: Material): () => void;
103
+ /**
104
+ * Clears a member as it is handed out, so a reused slot keeps nothing of the one that held it. The
105
+ * anchor included, which a caller that writes only a matrix would otherwise inherit.
106
+ * @param id - The member being handed out.
107
+ */
108
+ protected resetInstance(id: number): void;
109
+ }
110
+ /**
111
+ * Registers the class as an R3F element, by value so the generated key cannot collide with the
112
+ * `Sprite` three puts in the catalog when the canvas mounts.
113
+ */
114
+ export declare const BatchedSprites: import("react").ExoticComponent<import("@react-three/fiber/dist/declarations/src/core/utils").Mutable<import("@react-three/fiber/dist/declarations/src/core/utils").Overwrite<Partial<import("@react-three/fiber/dist/declarations/src/core/utils").Overwrite<BatchedSpritesImpl, import("@react-three/fiber").MathProps<BatchedSpritesImpl> & import("@react-three/fiber").ReactProps<BatchedSpritesImpl> & Partial<import("@react-three/fiber").EventHandlers>>>, Omit<import("@react-three/fiber").InstanceProps<BatchedSpritesImpl, typeof BatchedSpritesImpl>, "object">>>>;
115
+ //# sourceMappingURL=BatchedSprites.d.ts.map