@codexo/exojs-particles 0.15.3 → 0.16.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 (211) hide show
  1. package/README.md +21 -43
  2. package/dist/esm/ParticleStorage.d.ts +188 -0
  3. package/dist/esm/ParticleStorage.d.ts.map +1 -0
  4. package/dist/esm/ParticleStorage.js +208 -0
  5. package/dist/esm/ParticleStorage.js.map +1 -0
  6. package/dist/esm/ParticleSystem.d.ts +189 -60
  7. package/dist/esm/ParticleSystem.d.ts.map +1 -0
  8. package/dist/esm/ParticleSystem.js +660 -560
  9. package/dist/esm/ParticleSystem.js.map +1 -1
  10. package/dist/esm/distributions/BoxArea.d.ts +1 -0
  11. package/dist/esm/distributions/BoxArea.d.ts.map +1 -0
  12. package/dist/esm/distributions/BoxArea.js +38 -44
  13. package/dist/esm/distributions/BoxArea.js.map +1 -1
  14. package/dist/esm/distributions/CircleArea.d.ts +1 -0
  15. package/dist/esm/distributions/CircleArea.d.ts.map +1 -0
  16. package/dist/esm/distributions/CircleArea.js +30 -28
  17. package/dist/esm/distributions/CircleArea.js.map +1 -1
  18. package/dist/esm/distributions/ColorGradient.d.ts +3 -2
  19. package/dist/esm/distributions/ColorGradient.d.ts.map +1 -0
  20. package/dist/esm/distributions/ColorGradient.js +70 -75
  21. package/dist/esm/distributions/ColorGradient.js.map +1 -1
  22. package/dist/esm/distributions/ConeDirection.d.ts +2 -1
  23. package/dist/esm/distributions/ConeDirection.d.ts.map +1 -0
  24. package/dist/esm/distributions/ConeDirection.js +41 -39
  25. package/dist/esm/distributions/ConeDirection.js.map +1 -1
  26. package/dist/esm/distributions/Constant.d.ts +1 -0
  27. package/dist/esm/distributions/Constant.d.ts.map +1 -0
  28. package/dist/esm/distributions/Constant.js +32 -32
  29. package/dist/esm/distributions/Constant.js.map +1 -1
  30. package/dist/esm/distributions/Curve.d.ts +2 -1
  31. package/dist/esm/distributions/Curve.d.ts.map +1 -0
  32. package/dist/esm/distributions/Curve.js +46 -55
  33. package/dist/esm/distributions/Curve.js.map +1 -1
  34. package/dist/esm/distributions/Distribution.d.ts +4 -3
  35. package/dist/esm/distributions/Distribution.d.ts.map +1 -0
  36. package/dist/esm/distributions/LineSegment.d.ts +1 -0
  37. package/dist/esm/distributions/LineSegment.d.ts.map +1 -0
  38. package/dist/esm/distributions/LineSegment.js +25 -23
  39. package/dist/esm/distributions/LineSegment.js.map +1 -1
  40. package/dist/esm/distributions/Range.d.ts +1 -0
  41. package/dist/esm/distributions/Range.d.ts.map +1 -0
  42. package/dist/esm/distributions/Range.js +18 -16
  43. package/dist/esm/distributions/Range.js.map +1 -1
  44. package/dist/esm/distributions/VectorRange.d.ts +1 -0
  45. package/dist/esm/distributions/VectorRange.d.ts.map +1 -0
  46. package/dist/esm/distributions/VectorRange.js +29 -27
  47. package/dist/esm/distributions/VectorRange.js.map +1 -1
  48. package/dist/esm/distributions/index.d.ts +1 -0
  49. package/dist/esm/distributions/index.d.ts.map +1 -0
  50. package/dist/esm/gpu/ParticleGpuState.d.ts +143 -18
  51. package/dist/esm/gpu/ParticleGpuState.d.ts.map +1 -0
  52. package/dist/esm/gpu/ParticleGpuState.js +655 -497
  53. package/dist/esm/gpu/ParticleGpuState.js.map +1 -1
  54. package/dist/esm/gpu/shaders/particle-simulate.wgsl.js +6 -0
  55. package/dist/esm/gpu/shaders/particle-simulate.wgsl.js.map +1 -0
  56. package/dist/esm/index.d.ts +1 -0
  57. package/dist/esm/index.d.ts.map +1 -0
  58. package/dist/esm/index.js +39 -32
  59. package/dist/esm/modules/AlphaFadeOverLifetime.d.ts +4 -3
  60. package/dist/esm/modules/AlphaFadeOverLifetime.d.ts.map +1 -0
  61. package/dist/esm/modules/AlphaFadeOverLifetime.js +68 -55
  62. package/dist/esm/modules/AlphaFadeOverLifetime.js.map +1 -1
  63. package/dist/esm/modules/ApplyForce.d.ts +4 -3
  64. package/dist/esm/modules/ApplyForce.d.ts.map +1 -0
  65. package/dist/esm/modules/ApplyForce.js +49 -43
  66. package/dist/esm/modules/ApplyForce.js.map +1 -1
  67. package/dist/esm/modules/AttractToPoint.d.ts +4 -3
  68. package/dist/esm/modules/AttractToPoint.d.ts.map +1 -0
  69. package/dist/esm/modules/AttractToPoint.js +73 -62
  70. package/dist/esm/modules/AttractToPoint.js.map +1 -1
  71. package/dist/esm/modules/BurstSpawn.d.ts +6 -18
  72. package/dist/esm/modules/BurstSpawn.d.ts.map +1 -0
  73. package/dist/esm/modules/BurstSpawn.js +56 -91
  74. package/dist/esm/modules/BurstSpawn.js.map +1 -1
  75. package/dist/esm/modules/ColorOverLifetime.d.ts +4 -3
  76. package/dist/esm/modules/ColorOverLifetime.d.ts.map +1 -0
  77. package/dist/esm/modules/ColorOverLifetime.js +65 -53
  78. package/dist/esm/modules/ColorOverLifetime.js.map +1 -1
  79. package/dist/esm/modules/ColorOverSpeed.d.ts +5 -4
  80. package/dist/esm/modules/ColorOverSpeed.d.ts.map +1 -0
  81. package/dist/esm/modules/ColorOverSpeed.js +90 -75
  82. package/dist/esm/modules/ColorOverSpeed.js.map +1 -1
  83. package/dist/esm/modules/DeathModule.d.ts +26 -10
  84. package/dist/esm/modules/DeathModule.d.ts.map +1 -0
  85. package/dist/esm/modules/DeathModule.js +38 -22
  86. package/dist/esm/modules/DeathModule.js.map +1 -1
  87. package/dist/esm/modules/Drag.d.ts +3 -2
  88. package/dist/esm/modules/Drag.d.ts.map +1 -0
  89. package/dist/esm/modules/Drag.js +43 -37
  90. package/dist/esm/modules/Drag.js.map +1 -1
  91. package/dist/esm/modules/OrbitalForce.d.ts +4 -3
  92. package/dist/esm/modules/OrbitalForce.d.ts.map +1 -0
  93. package/dist/esm/modules/OrbitalForce.js +70 -58
  94. package/dist/esm/modules/OrbitalForce.js.map +1 -1
  95. package/dist/esm/modules/RateSpawn.d.ts +7 -22
  96. package/dist/esm/modules/RateSpawn.d.ts.map +1 -0
  97. package/dist/esm/modules/RateSpawn.js +39 -71
  98. package/dist/esm/modules/RateSpawn.js.map +1 -1
  99. package/dist/esm/modules/RepelFromPoint.d.ts +4 -3
  100. package/dist/esm/modules/RepelFromPoint.d.ts.map +1 -0
  101. package/dist/esm/modules/RepelFromPoint.js +72 -62
  102. package/dist/esm/modules/RepelFromPoint.js.map +1 -1
  103. package/dist/esm/modules/RotateOverLifetime.d.ts +3 -2
  104. package/dist/esm/modules/RotateOverLifetime.d.ts.map +1 -0
  105. package/dist/esm/modules/RotateOverLifetime.js +40 -36
  106. package/dist/esm/modules/RotateOverLifetime.js.map +1 -1
  107. package/dist/esm/modules/ScaleOverLifetime.d.ts +5 -4
  108. package/dist/esm/modules/ScaleOverLifetime.d.ts.map +1 -0
  109. package/dist/esm/modules/ScaleOverLifetime.js +60 -50
  110. package/dist/esm/modules/ScaleOverLifetime.js.map +1 -1
  111. package/dist/esm/modules/SpawnModule.d.ts +15 -12
  112. package/dist/esm/modules/SpawnModule.d.ts.map +1 -0
  113. package/dist/esm/modules/SpawnModule.js +32 -28
  114. package/dist/esm/modules/SpawnModule.js.map +1 -1
  115. package/dist/esm/modules/SpawnOnDeath.d.ts +13 -9
  116. package/dist/esm/modules/SpawnOnDeath.d.ts.map +1 -0
  117. package/dist/esm/modules/SpawnOnDeath.js +41 -43
  118. package/dist/esm/modules/SpawnOnDeath.js.map +1 -1
  119. package/dist/esm/modules/Turbulence.d.ts +4 -3
  120. package/dist/esm/modules/Turbulence.d.ts.map +1 -0
  121. package/dist/esm/modules/Turbulence.js +101 -89
  122. package/dist/esm/modules/Turbulence.js.map +1 -1
  123. package/dist/esm/modules/UpdateModule.d.ts +21 -13
  124. package/dist/esm/modules/UpdateModule.d.ts.map +1 -0
  125. package/dist/esm/modules/UpdateModule.js +65 -63
  126. package/dist/esm/modules/UpdateModule.js.map +1 -1
  127. package/dist/esm/modules/VelocityOverLifetime.d.ts +7 -6
  128. package/dist/esm/modules/VelocityOverLifetime.d.ts.map +1 -0
  129. package/dist/esm/modules/VelocityOverLifetime.js +75 -73
  130. package/dist/esm/modules/VelocityOverLifetime.js.map +1 -1
  131. package/dist/esm/modules/WgslContribution.d.ts +16 -15
  132. package/dist/esm/modules/WgslContribution.d.ts.map +1 -0
  133. package/dist/esm/modules/WgslContribution.js +37 -29
  134. package/dist/esm/modules/WgslContribution.js.map +1 -1
  135. package/dist/esm/modules/index.d.ts +3 -1
  136. package/dist/esm/modules/index.d.ts.map +1 -0
  137. package/dist/esm/modules/spawnFields.d.ts +32 -0
  138. package/dist/esm/modules/spawnFields.d.ts.map +1 -0
  139. package/dist/esm/modules/spawnFields.js +31 -0
  140. package/dist/esm/modules/spawnFields.js.map +1 -0
  141. package/dist/esm/particlesBuildInfo.d.ts +1 -0
  142. package/dist/esm/particlesBuildInfo.d.ts.map +1 -0
  143. package/dist/esm/particlesBuildInfo.js +6 -4
  144. package/dist/esm/particlesBuildInfo.js.map +1 -1
  145. package/dist/esm/particlesExtension.d.ts +13 -5
  146. package/dist/esm/particlesExtension.d.ts.map +1 -0
  147. package/dist/esm/particlesExtension.js +38 -42
  148. package/dist/esm/particlesExtension.js.map +1 -1
  149. package/dist/esm/public.d.ts +11 -0
  150. package/dist/esm/public.d.ts.map +1 -0
  151. package/dist/esm/renderModes/MeshParticles.d.ts +128 -0
  152. package/dist/esm/renderModes/MeshParticles.d.ts.map +1 -0
  153. package/dist/esm/renderModes/MeshParticles.js +263 -0
  154. package/dist/esm/renderModes/MeshParticles.js.map +1 -0
  155. package/dist/esm/renderModes/ParticleBufferLayout.d.ts +59 -0
  156. package/dist/esm/renderModes/ParticleBufferLayout.d.ts.map +1 -0
  157. package/dist/esm/renderModes/ParticleBufferLayout.js +99 -0
  158. package/dist/esm/renderModes/ParticleBufferLayout.js.map +1 -0
  159. package/dist/esm/renderModes/ParticleInstanceWriter.d.ts +58 -0
  160. package/dist/esm/renderModes/ParticleInstanceWriter.d.ts.map +1 -0
  161. package/dist/esm/renderModes/ParticleInstanceWriter.js +173 -0
  162. package/dist/esm/renderModes/ParticleInstanceWriter.js.map +1 -0
  163. package/dist/esm/renderModes/ParticleMaterial.d.ts +18 -0
  164. package/dist/esm/renderModes/ParticleMaterial.d.ts.map +1 -0
  165. package/dist/esm/renderModes/ParticleMaterial.js +24 -0
  166. package/dist/esm/renderModes/ParticleMaterial.js.map +1 -0
  167. package/dist/esm/renderModes/ParticleRenderMode.d.ts +114 -0
  168. package/dist/esm/renderModes/ParticleRenderMode.d.ts.map +1 -0
  169. package/dist/esm/renderModes/ParticleRenderMode.js +99 -0
  170. package/dist/esm/renderModes/ParticleRenderMode.js.map +1 -0
  171. package/dist/esm/renderModes/QuadParticles.d.ts +55 -0
  172. package/dist/esm/renderModes/QuadParticles.d.ts.map +1 -0
  173. package/dist/esm/renderModes/QuadParticles.js +95 -0
  174. package/dist/esm/renderModes/QuadParticles.js.map +1 -0
  175. package/dist/esm/renderModes/RibbonParticles.d.ts +96 -0
  176. package/dist/esm/renderModes/RibbonParticles.d.ts.map +1 -0
  177. package/dist/esm/renderModes/RibbonParticles.js +202 -0
  178. package/dist/esm/renderModes/RibbonParticles.js.map +1 -0
  179. package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js +6 -0
  180. package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js.map +1 -0
  181. package/dist/esm/renderModes/shaders/mesh.vert.js +6 -0
  182. package/dist/esm/renderModes/shaders/mesh.vert.js.map +1 -0
  183. package/dist/esm/renderModes/shaders/quad-particles.wgsl.js +6 -0
  184. package/dist/esm/renderModes/shaders/quad-particles.wgsl.js.map +1 -0
  185. package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js +6 -0
  186. package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js.map +1 -0
  187. package/dist/esm/renderModes/shaders/ribbon.frag.js +6 -0
  188. package/dist/esm/renderModes/shaders/ribbon.frag.js.map +1 -0
  189. package/dist/esm/renderModes/shaders/ribbon.vert.js +6 -0
  190. package/dist/esm/renderModes/shaders/ribbon.vert.js.map +1 -0
  191. package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts +48 -20
  192. package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts.map +1 -0
  193. package/dist/esm/renderers/WebGl2ParticleRenderer.js +317 -321
  194. package/dist/esm/renderers/WebGl2ParticleRenderer.js.map +1 -1
  195. package/dist/esm/renderers/WebGpuParticleRenderer.d.ts +99 -18
  196. package/dist/esm/renderers/WebGpuParticleRenderer.d.ts.map +1 -0
  197. package/dist/esm/renderers/WebGpuParticleRenderer.js +542 -551
  198. package/dist/esm/renderers/WebGpuParticleRenderer.js.map +1 -1
  199. package/dist/esm/renderers/shaders/particle.frag.js +6 -0
  200. package/dist/esm/renderers/shaders/particle.frag.js.map +1 -0
  201. package/dist/esm/renderers/shaders/particle.vert.js +6 -0
  202. package/dist/esm/renderers/shaders/particle.vert.js.map +1 -0
  203. package/package.json +8 -15
  204. package/dist/esm/index.js.map +0 -1
  205. package/dist/esm/register.d.ts +0 -1
  206. package/dist/esm/register.js +0 -43
  207. package/dist/esm/register.js.map +0 -1
  208. package/dist/esm/renderers/glsl/particle.frag.js +0 -4
  209. package/dist/esm/renderers/glsl/particle.frag.js.map +0 -1
  210. package/dist/esm/renderers/glsl/particle.vert.js +0 -4
  211. package/dist/esm/renderers/glsl/particle.vert.js.map +0 -1
@@ -0,0 +1,99 @@
1
+ //#region src/renderModes/ParticleBufferLayout.ts
2
+ const attributeTypeByteSizes = {
3
+ f32: 4,
4
+ u8: 1,
5
+ u16: 2,
6
+ u32: 4,
7
+ i32: 4
8
+ };
9
+ const validTopologies = /* @__PURE__ */ new Set(["triangle-list", "triangle-strip"]);
10
+ const validUsages = /* @__PURE__ */ new Set([
11
+ "static",
12
+ "dynamic",
13
+ "stream"
14
+ ]);
15
+ /**
16
+ * Check that an attribute set describes a bindable interleaved record: unique
17
+ * names, sane sizes and offsets, and ranges that neither overlap each other nor
18
+ * reach past the stride.
19
+ */
20
+ const validateAttributes = (attributes, stride) => {
21
+ const ranges = [];
22
+ const names = /* @__PURE__ */ new Set();
23
+ for (const attribute of attributes) {
24
+ if (typeof attribute.name !== "string" || attribute.name.length === 0) throw new Error("ParticleBufferLayout attribute name must be a non-empty string.");
25
+ if (names.has(attribute.name)) throw new Error(`ParticleBufferLayout attribute "${attribute.name}" is declared more than once.`);
26
+ names.add(attribute.name);
27
+ if (!Number.isInteger(attribute.size) || attribute.size === 0 || attribute.size > 4) throw new Error(`ParticleBufferLayout attribute "${attribute.name}" size must be an integer in [1..4] (got ${attribute.size}).`);
28
+ if (!Number.isInteger(attribute.offset) || attribute.offset < 0) throw new Error(`ParticleBufferLayout attribute "${attribute.name}" offset must be a non-negative integer (got ${attribute.offset}).`);
29
+ const end = attribute.offset + attributeTypeByteSizes[attribute.type] * attribute.size;
30
+ if (end > stride) throw new Error(`ParticleBufferLayout attribute "${attribute.name}" range [${attribute.offset}, ${end}) exceeds stride ${stride}.`);
31
+ for (const range of ranges) if (attribute.offset < range.end && end > range.start) throw new Error(`ParticleBufferLayout attribute "${attribute.name}" overlaps attribute "${range.name}" in the interleaved layout.`);
32
+ ranges.push({
33
+ name: attribute.name,
34
+ start: attribute.offset,
35
+ end
36
+ });
37
+ }
38
+ };
39
+ /**
40
+ * Describes the interleaved buffer a render mode's `build()` fills each frame.
41
+ *
42
+ * Deliberately not a `Geometry`. A `Geometry` requires a position attribute,
43
+ * requires that attribute to carry at least two components, and validates any
44
+ * index list against the vertex count implied by its `vertexData` - three rules
45
+ * that are meaningful for geometry and meaningless for a per-instance record.
46
+ * Satisfying them used to cost the instanced modes a zero-filled placeholder
47
+ * buffer that was never uploaded, sized purely to keep the index check quiet.
48
+ *
49
+ * What is checked here is the subset that protects a real GPU binding: a
50
+ * positive stride, at least one attribute, unique names, and ranges that
51
+ * neither overlap each other nor reach past the stride.
52
+ *
53
+ * A layout is a description rather than a resource: it owns no GPU handle and
54
+ * needs no disposal.
55
+ */
56
+ var ParticleBufferLayout = class {
57
+ attributes;
58
+ stride;
59
+ usage;
60
+ topology;
61
+ indices;
62
+ constructor(options) {
63
+ const { attributes, stride, usage = "stream", topology = "triangle-list", indices = null } = options;
64
+ if (attributes.length === 0) throw new Error("ParticleBufferLayout attributes must be a non-empty array.");
65
+ if (!Number.isInteger(stride) || stride <= 0) throw new Error(`ParticleBufferLayout stride must be a positive integer (got ${stride}).`);
66
+ if (!validTopologies.has(topology)) throw new Error(`ParticleBufferLayout topology must be one of: triangle-list, triangle-strip (got ${String(topology)}).`);
67
+ if (!validUsages.has(usage)) throw new Error(`ParticleBufferLayout usage must be one of: static, dynamic, stream (got ${String(usage)}).`);
68
+ validateAttributes(attributes, stride);
69
+ this.attributes = attributes.map((attribute) => ({ ...attribute }));
70
+ this.stride = stride;
71
+ this.usage = usage;
72
+ this.topology = topology;
73
+ this.indices = indices;
74
+ }
75
+ /** Elements one draw consumes from {@link indices}, or 0 when there are none. */
76
+ get indexCount() {
77
+ return this.indices?.length ?? 0;
78
+ }
79
+ };
80
+ /**
81
+ * Guard the two seam combinations a mode must not declare.
82
+ *
83
+ * Called by a mode's own constructor, so the error names the mistake where it
84
+ * was made, and repeated by each executor when it first realises a mode, so a
85
+ * mode written outside this package cannot slip past. Both checks are cheap and
86
+ * run once per mode rather than per frame.
87
+ *
88
+ * @param modeName Class name of the mode, used to prefix the error.
89
+ */
90
+ const assertVertexGeometryCompatible = (dataLayout, vertexGeometry, instanced, modeName) => {
91
+ if (vertexGeometry === null) return;
92
+ if (!instanced) throw new Error(`${modeName}: a per-vertex geometry requires an instanced mode. Without instancing both buffers would step per vertex, which no draw can express.`);
93
+ const declared = new Set(dataLayout.attributes.map((attribute) => attribute.name));
94
+ for (const attribute of vertexGeometry.attributes) if (declared.has(attribute.name)) throw new Error(`${modeName}: attribute "${attribute.name}" is declared by both the per-frame buffer and the per-vertex geometry. Every attribute name must be unique across the two buffers.`);
95
+ };
96
+
97
+ //#endregion
98
+ export { ParticleBufferLayout, assertVertexGeometryCompatible };
99
+ //# sourceMappingURL=ParticleBufferLayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParticleBufferLayout.js","names":[],"sources":["../../../src/renderModes/ParticleBufferLayout.ts"],"sourcesContent":["import type { AttributeType, Geometry, GeometryAttribute, GeometryUsage, Topology } from '@codexo/exojs';\n\nconst attributeTypeByteSizes: Record<AttributeType, number> = {\n f32: 4,\n u8: 1,\n u16: 2,\n u32: 4,\n i32: 4,\n};\n\nconst validTopologies = new Set<Topology>(['triangle-list', 'triangle-strip']);\nconst validUsages = new Set<GeometryUsage>(['static', 'dynamic', 'stream']);\n\ninterface AttributeRange {\n readonly name: string;\n readonly start: number;\n readonly end: number;\n}\n\n/**\n * Check that an attribute set describes a bindable interleaved record: unique\n * names, sane sizes and offsets, and ranges that neither overlap each other nor\n * reach past the stride.\n */\nconst validateAttributes = (attributes: readonly GeometryAttribute[], stride: number): void => {\n const ranges: AttributeRange[] = [];\n const names = new Set<string>();\n\n for (const attribute of attributes) {\n if (typeof attribute.name !== 'string' || attribute.name.length === 0) {\n throw new Error('ParticleBufferLayout attribute name must be a non-empty string.');\n }\n\n if (names.has(attribute.name)) {\n throw new Error(`ParticleBufferLayout attribute \"${attribute.name}\" is declared more than once.`);\n }\n\n names.add(attribute.name);\n\n if (!Number.isInteger(attribute.size) || attribute.size === 0 || attribute.size > 4) {\n throw new Error(`ParticleBufferLayout attribute \"${attribute.name}\" size must be an integer in [1..4] (got ${attribute.size}).`);\n }\n\n if (!Number.isInteger(attribute.offset) || attribute.offset < 0) {\n throw new Error(`ParticleBufferLayout attribute \"${attribute.name}\" offset must be a non-negative integer (got ${attribute.offset}).`);\n }\n\n const end = attribute.offset + attributeTypeByteSizes[attribute.type] * attribute.size;\n\n if (end > stride) {\n throw new Error(`ParticleBufferLayout attribute \"${attribute.name}\" range [${attribute.offset}, ${end}) exceeds stride ${stride}.`);\n }\n\n for (const range of ranges) {\n if (attribute.offset < range.end && end > range.start) {\n throw new Error(`ParticleBufferLayout attribute \"${attribute.name}\" overlaps attribute \"${range.name}\" in the interleaved layout.`);\n }\n }\n\n ranges.push({ name: attribute.name, start: attribute.offset, end });\n }\n};\n\n/** Construction options for {@link ParticleBufferLayout}. */\nexport interface ParticleBufferLayoutOptions {\n /** Interleaved attributes, in declaration order. Shader locations follow this order. */\n readonly attributes: readonly GeometryAttribute[];\n\n /** Bytes one record occupies. Must be a positive integer. */\n readonly stride: number;\n\n /** Upload hint for the buffer this layout describes. Defaults to `stream`. */\n readonly usage?: GeometryUsage;\n\n /**\n * Topology of the draw, used only when the mode declares no per-vertex\n * geometry to take it from. Defaults to `triangle-list`.\n */\n readonly topology?: Topology;\n\n /**\n * Index list for the draw, used only when the mode declares no per-vertex\n * geometry to take it from. Defaults to none.\n */\n readonly indices?: Uint16Array | Uint32Array | null;\n}\n\n/**\n * Describes the interleaved buffer a render mode's `build()` fills each frame.\n *\n * Deliberately not a `Geometry`. A `Geometry` requires a position attribute,\n * requires that attribute to carry at least two components, and validates any\n * index list against the vertex count implied by its `vertexData` - three rules\n * that are meaningful for geometry and meaningless for a per-instance record.\n * Satisfying them used to cost the instanced modes a zero-filled placeholder\n * buffer that was never uploaded, sized purely to keep the index check quiet.\n *\n * What is checked here is the subset that protects a real GPU binding: a\n * positive stride, at least one attribute, unique names, and ranges that\n * neither overlap each other nor reach past the stride.\n *\n * A layout is a description rather than a resource: it owns no GPU handle and\n * needs no disposal.\n */\nexport class ParticleBufferLayout {\n public readonly attributes: readonly GeometryAttribute[];\n public readonly stride: number;\n public readonly usage: GeometryUsage;\n public readonly topology: Topology;\n public readonly indices: Uint16Array | Uint32Array | null;\n\n public constructor(options: ParticleBufferLayoutOptions) {\n const { attributes, stride, usage = 'stream', topology = 'triangle-list', indices = null } = options;\n\n if (attributes.length === 0) {\n throw new Error('ParticleBufferLayout attributes must be a non-empty array.');\n }\n\n if (!Number.isInteger(stride) || stride <= 0) {\n throw new Error(`ParticleBufferLayout stride must be a positive integer (got ${stride}).`);\n }\n\n if (!validTopologies.has(topology)) {\n throw new Error(`ParticleBufferLayout topology must be one of: triangle-list, triangle-strip (got ${String(topology)}).`);\n }\n\n if (!validUsages.has(usage)) {\n throw new Error(`ParticleBufferLayout usage must be one of: static, dynamic, stream (got ${String(usage)}).`);\n }\n\n validateAttributes(attributes, stride);\n\n this.attributes = attributes.map(attribute => ({ ...attribute }));\n this.stride = stride;\n this.usage = usage;\n this.topology = topology;\n this.indices = indices;\n }\n\n /** Elements one draw consumes from {@link indices}, or 0 when there are none. */\n public get indexCount(): number {\n return this.indices?.length ?? 0;\n }\n}\n\n/**\n * Guard the two seam combinations a mode must not declare.\n *\n * Called by a mode's own constructor, so the error names the mistake where it\n * was made, and repeated by each executor when it first realises a mode, so a\n * mode written outside this package cannot slip past. Both checks are cheap and\n * run once per mode rather than per frame.\n *\n * @param modeName Class name of the mode, used to prefix the error.\n */\nexport const assertVertexGeometryCompatible = (\n dataLayout: ParticleBufferLayout,\n vertexGeometry: Geometry | null,\n instanced: boolean,\n modeName: string,\n): void => {\n if (vertexGeometry === null) {\n return;\n }\n\n if (!instanced) {\n throw new Error(\n `${modeName}: a per-vertex geometry requires an instanced mode. Without instancing both buffers would step per vertex, which no draw can express.`,\n );\n }\n\n const declared = new Set(dataLayout.attributes.map(attribute => attribute.name));\n\n for (const attribute of vertexGeometry.attributes) {\n if (declared.has(attribute.name)) {\n throw new Error(\n `${modeName}: attribute \"${attribute.name}\" is declared by both the per-frame buffer and the per-vertex geometry. Every attribute name must be unique across the two buffers.`,\n );\n }\n }\n};\n"],"mappings":";AAEA,MAAM,yBAAwD;CAC5D,KAAK;CACL,IAAI;CACJ,KAAK;CACL,KAAK;CACL,KAAK;AACP;AAEA,MAAM,kCAAkB,IAAI,IAAc,CAAC,iBAAiB,gBAAgB,CAAC;AAC7E,MAAM,8BAAc,IAAI,IAAmB;CAAC;CAAU;CAAW;AAAQ,CAAC;;;;;;AAa1E,MAAM,sBAAsB,YAA0C,WAAyB;CAC7F,MAAM,SAA2B,CAAC;CAClC,MAAM,wBAAQ,IAAI,IAAY;CAE9B,KAAK,MAAM,aAAa,YAAY;EAClC,IAAI,OAAO,UAAU,SAAS,YAAY,UAAU,KAAK,WAAW,GAClE,MAAM,IAAI,MAAM,iEAAiE;EAGnF,IAAI,MAAM,IAAI,UAAU,IAAI,GAC1B,MAAM,IAAI,MAAM,mCAAmC,UAAU,KAAK,8BAA8B;EAGlG,MAAM,IAAI,UAAU,IAAI;EAExB,IAAI,CAAC,OAAO,UAAU,UAAU,IAAI,KAAK,UAAU,SAAS,KAAK,UAAU,OAAO,GAChF,MAAM,IAAI,MAAM,mCAAmC,UAAU,KAAK,2CAA2C,UAAU,KAAK,GAAG;EAGjI,IAAI,CAAC,OAAO,UAAU,UAAU,MAAM,KAAK,UAAU,SAAS,GAC5D,MAAM,IAAI,MAAM,mCAAmC,UAAU,KAAK,+CAA+C,UAAU,OAAO,GAAG;EAGvI,MAAM,MAAM,UAAU,SAAS,uBAAuB,UAAU,QAAQ,UAAU;EAElF,IAAI,MAAM,QACR,MAAM,IAAI,MAAM,mCAAmC,UAAU,KAAK,WAAW,UAAU,OAAO,IAAI,IAAI,mBAAmB,OAAO,EAAE;EAGpI,KAAK,MAAM,SAAS,QAClB,IAAI,UAAU,SAAS,MAAM,OAAO,MAAM,MAAM,OAC9C,MAAM,IAAI,MAAM,mCAAmC,UAAU,KAAK,wBAAwB,MAAM,KAAK,6BAA6B;EAItI,OAAO,KAAK;GAAE,MAAM,UAAU;GAAM,OAAO,UAAU;GAAQ;EAAI,CAAC;CACpE;AACF;;;;;;;;;;;;;;;;;;AA2CA,IAAa,uBAAb,MAAkC;CAChC,AAAgB;CAChB,AAAgB;CAChB,AAAgB;CAChB,AAAgB;CAChB,AAAgB;CAEhB,AAAO,YAAY,SAAsC;EACvD,MAAM,EAAE,YAAY,QAAQ,QAAQ,UAAU,WAAW,iBAAiB,UAAU,SAAS;EAE7F,IAAI,WAAW,WAAW,GACxB,MAAM,IAAI,MAAM,4DAA4D;EAG9E,IAAI,CAAC,OAAO,UAAU,MAAM,KAAK,UAAU,GACzC,MAAM,IAAI,MAAM,+DAA+D,OAAO,GAAG;EAG3F,IAAI,CAAC,gBAAgB,IAAI,QAAQ,GAC/B,MAAM,IAAI,MAAM,oFAAoF,OAAO,QAAQ,EAAE,GAAG;EAG1H,IAAI,CAAC,YAAY,IAAI,KAAK,GACxB,MAAM,IAAI,MAAM,2EAA2E,OAAO,KAAK,EAAE,GAAG;EAG9G,mBAAmB,YAAY,MAAM;EAErC,KAAK,aAAa,WAAW,KAAI,eAAc,EAAE,GAAG,UAAU,EAAE;EAChE,KAAK,SAAS;EACd,KAAK,QAAQ;EACb,KAAK,WAAW;EAChB,KAAK,UAAU;CACjB;;CAGA,IAAW,aAAqB;EAC9B,OAAO,KAAK,SAAS,UAAU;CACjC;AACF;;;;;;;;;;;AAYA,MAAa,kCACX,YACA,gBACA,WACA,aACS;CACT,IAAI,mBAAmB,MACrB;CAGF,IAAI,CAAC,WACH,MAAM,IAAI,MACR,GAAG,SAAS,sIACd;CAGF,MAAM,WAAW,IAAI,IAAI,WAAW,WAAW,KAAI,cAAa,UAAU,IAAI,CAAC;CAE/E,KAAK,MAAM,aAAa,eAAe,YACrC,IAAI,SAAS,IAAI,UAAU,IAAI,GAC7B,MAAM,IAAI,MACR,GAAG,SAAS,eAAe,UAAU,KAAK,oIAC5C;AAGN"}
@@ -0,0 +1,58 @@
1
+ import type { GeometryAttribute } from '@codexo/exojs';
2
+ import type { ParticleBatch } from '#ParticleStorage';
3
+ import type { ParticleSystem } from '#ParticleSystem';
4
+ /** Bytes one particle occupies in the interleaved per-instance buffer. */
5
+ export declare const instanceStrideBytes = 40;
6
+ /** 32-bit words one particle occupies in that buffer. */
7
+ export declare const wordsPerInstance: number;
8
+ /**
9
+ * The per-instance attribute layout every instanced render mode declares, and
10
+ * the layout the GPU compute pipeline emits into its instance buffer:
11
+ *
12
+ * ```
13
+ * a_position f32x2 (offset 0, 8 bytes) particle position (system-local)
14
+ * a_scale f32x2 (offset 8, 8 bytes)
15
+ * a_rotation f32 (offset 16, 4 bytes) degrees
16
+ * a_color u8x4 (offset 20, 4 bytes) RGBA tint, normalised
17
+ * a_uvMin f32x2 (offset 24, 8 bytes) pre-resolved frame uvMin
18
+ * a_uvMax f32x2 (offset 32, 8 bytes) pre-resolved frame uvMax
19
+ * ```
20
+ *
21
+ * Shared rather than copied per mode so a mode using it cannot drift out of
22
+ * step with the compute shader and quietly lose its GPU eligibility. `Geometry`
23
+ * copies the attribute objects it is handed, so one frozen list serves every
24
+ * mode.
25
+ */
26
+ export declare const instanceAttributes: readonly GeometryAttribute[];
27
+ /**
28
+ * Fills the interleaved per-instance buffer shared by the instanced render
29
+ * modes - one 40-byte record per live particle, in {@link instanceAttributes}
30
+ * order.
31
+ *
32
+ * A class rather than a free function because the frame-UV table it resolves
33
+ * per frame is scratch state that must survive between calls: the arithmetic
34
+ * runs once per declared frame, not once per particle.
35
+ *
36
+ * This is the CPU counterpart of the compute pipeline's pack-instances step;
37
+ * the two must agree byte for byte, since a GPU-eligible mode's system swaps
38
+ * between them silently depending on the backend.
39
+ */
40
+ export declare class ParticleInstanceWriter {
41
+ private _uvMinsScratch;
42
+ private _uvMaxsScratch;
43
+ /**
44
+ * Pack the live particles into the two views over one buffer, which must
45
+ * already hold `particles.count * instanceStrideBytes` bytes. Returns the
46
+ * number of instances written - dead slots in a GPU-mode system's live range
47
+ * are skipped, so that can be below the count.
48
+ */
49
+ write(system: ParticleSystem, particles: ParticleBatch, f32: Float32Array, u32: Uint32Array): number;
50
+ /**
51
+ * Compute (uvMin, uvMax) pairs for every declared frame on the system.
52
+ * Pulled out of the hot pack loop so the arithmetic runs once per frame
53
+ * rather than once per particle. Falls back to a single entry from
54
+ * `system.textureFrame` when no atlas is declared.
55
+ */
56
+ private _computeFrameUvs;
57
+ }
58
+ //# sourceMappingURL=ParticleInstanceWriter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParticleInstanceWriter.d.ts","sourceRoot":"","sources":["../../../src/renderModes/ParticleInstanceWriter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,0EAA0E;AAC1E,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,yDAAyD;AACzD,eAAO,MAAM,gBAAgB,QAAsD,CAAC;AAEpF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,iBAAiB,EAO1D,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,cAAc,CAAuB;IAE7C;;;;;OAKG;IACI,KAAK,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW,GAAG,MAAM;IA8C3G;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;CAiDzB"}
@@ -0,0 +1,173 @@
1
+ //#region src/renderModes/ParticleInstanceWriter.ts
2
+ /** Bytes one particle occupies in the interleaved per-instance buffer. */
3
+ const instanceStrideBytes = 40;
4
+ /** 32-bit words one particle occupies in that buffer. */
5
+ const wordsPerInstance = 40 / Uint32Array.BYTES_PER_ELEMENT;
6
+ /**
7
+ * The per-instance attribute layout every instanced render mode declares, and
8
+ * the layout the GPU compute pipeline emits into its instance buffer:
9
+ *
10
+ * ```
11
+ * a_position f32x2 (offset 0, 8 bytes) particle position (system-local)
12
+ * a_scale f32x2 (offset 8, 8 bytes)
13
+ * a_rotation f32 (offset 16, 4 bytes) degrees
14
+ * a_color u8x4 (offset 20, 4 bytes) RGBA tint, normalised
15
+ * a_uvMin f32x2 (offset 24, 8 bytes) pre-resolved frame uvMin
16
+ * a_uvMax f32x2 (offset 32, 8 bytes) pre-resolved frame uvMax
17
+ * ```
18
+ *
19
+ * Shared rather than copied per mode so a mode using it cannot drift out of
20
+ * step with the compute shader and quietly lose its GPU eligibility. `Geometry`
21
+ * copies the attribute objects it is handed, so one frozen list serves every
22
+ * mode.
23
+ */
24
+ const instanceAttributes = [
25
+ {
26
+ name: "a_position",
27
+ size: 2,
28
+ type: "f32",
29
+ normalized: false,
30
+ offset: 0
31
+ },
32
+ {
33
+ name: "a_scale",
34
+ size: 2,
35
+ type: "f32",
36
+ normalized: false,
37
+ offset: 8
38
+ },
39
+ {
40
+ name: "a_rotation",
41
+ size: 1,
42
+ type: "f32",
43
+ normalized: false,
44
+ offset: 16
45
+ },
46
+ {
47
+ name: "a_color",
48
+ size: 4,
49
+ type: "u8",
50
+ normalized: true,
51
+ offset: 20
52
+ },
53
+ {
54
+ name: "a_uvMin",
55
+ size: 2,
56
+ type: "f32",
57
+ normalized: false,
58
+ offset: 24
59
+ },
60
+ {
61
+ name: "a_uvMax",
62
+ size: 2,
63
+ type: "f32",
64
+ normalized: false,
65
+ offset: 32
66
+ }
67
+ ];
68
+ /**
69
+ * Fills the interleaved per-instance buffer shared by the instanced render
70
+ * modes - one 40-byte record per live particle, in {@link instanceAttributes}
71
+ * order.
72
+ *
73
+ * A class rather than a free function because the frame-UV table it resolves
74
+ * per frame is scratch state that must survive between calls: the arithmetic
75
+ * runs once per declared frame, not once per particle.
76
+ *
77
+ * This is the CPU counterpart of the compute pipeline's pack-instances step;
78
+ * the two must agree byte for byte, since a GPU-eligible mode's system swaps
79
+ * between them silently depending on the backend.
80
+ */
81
+ var ParticleInstanceWriter = class {
82
+ _uvMinsScratch = /* @__PURE__ */ new Float32Array(2);
83
+ _uvMaxsScratch = /* @__PURE__ */ new Float32Array(2);
84
+ /**
85
+ * Pack the live particles into the two views over one buffer, which must
86
+ * already hold `particles.count * instanceStrideBytes` bytes. Returns the
87
+ * number of instances written - dead slots in a GPU-mode system's live range
88
+ * are skipped, so that can be below the count.
89
+ */
90
+ write(system, particles, f32, u32) {
91
+ const limit = particles.count;
92
+ const { x: posX, y: posY } = particles.position;
93
+ const { x: scaleX, y: scaleY } = particles.scale;
94
+ const { angle: rotations } = particles.rotation;
95
+ const { color, frame: textureIndex } = particles;
96
+ const { uvMins, uvMaxs } = this._computeFrameUvs(system);
97
+ const frameCount = uvMins.length / 2;
98
+ const fallbackFrame = 0;
99
+ let writeIndex = 0;
100
+ for (let i = 0; i < limit; i++) {
101
+ if (!particles.isAlive(i)) continue;
102
+ const offset = writeIndex * wordsPerInstance;
103
+ const uvBase = (textureIndex[i] < frameCount ? textureIndex[i] : fallbackFrame) * 2;
104
+ f32[offset + 0] = posX[i];
105
+ f32[offset + 1] = posY[i];
106
+ f32[offset + 2] = scaleX[i];
107
+ f32[offset + 3] = scaleY[i];
108
+ f32[offset + 4] = rotations[i];
109
+ u32[offset + 5] = color[i];
110
+ f32[offset + 6] = uvMins[uvBase + 0];
111
+ f32[offset + 7] = uvMins[uvBase + 1];
112
+ f32[offset + 8] = uvMaxs[uvBase + 0];
113
+ f32[offset + 9] = uvMaxs[uvBase + 1];
114
+ writeIndex++;
115
+ }
116
+ return writeIndex;
117
+ }
118
+ /**
119
+ * Compute (uvMin, uvMax) pairs for every declared frame on the system.
120
+ * Pulled out of the hot pack loop so the arithmetic runs once per frame
121
+ * rather than once per particle. Falls back to a single entry from
122
+ * `system.textureFrame` when no atlas is declared.
123
+ */
124
+ _computeFrameUvs(system) {
125
+ const frames = system.frames;
126
+ const tex = system.texture;
127
+ const texW = tex.width;
128
+ const texH = tex.height;
129
+ const flipY = tex.flipY;
130
+ const count = frames.length === 0 ? 1 : frames.length;
131
+ if (this._uvMinsScratch.length < count * 2) {
132
+ this._uvMinsScratch = new Float32Array(count * 2);
133
+ this._uvMaxsScratch = new Float32Array(count * 2);
134
+ }
135
+ const mins = this._uvMinsScratch;
136
+ const maxs = this._uvMaxsScratch;
137
+ if (frames.length === 0) {
138
+ const f = system.textureFrame;
139
+ const minU = f.left / texW;
140
+ const maxU = f.right / texW;
141
+ const topV = f.top / texH;
142
+ const bottomV = f.bottom / texH;
143
+ mins[0] = minU;
144
+ mins[1] = flipY ? bottomV : topV;
145
+ maxs[0] = maxU;
146
+ maxs[1] = flipY ? topV : bottomV;
147
+ return {
148
+ uvMins: mins,
149
+ uvMaxs: maxs
150
+ };
151
+ }
152
+ for (let i = 0; i < frames.length; i++) {
153
+ const f = frames[i];
154
+ const o = i * 2;
155
+ const minU = f.left / texW;
156
+ const maxU = f.right / texW;
157
+ const topV = f.top / texH;
158
+ const bottomV = f.bottom / texH;
159
+ mins[o + 0] = minU;
160
+ mins[o + 1] = flipY ? bottomV : topV;
161
+ maxs[o + 0] = maxU;
162
+ maxs[o + 1] = flipY ? topV : bottomV;
163
+ }
164
+ return {
165
+ uvMins: mins,
166
+ uvMaxs: maxs
167
+ };
168
+ }
169
+ };
170
+
171
+ //#endregion
172
+ export { ParticleInstanceWriter, instanceAttributes, wordsPerInstance };
173
+ //# sourceMappingURL=ParticleInstanceWriter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParticleInstanceWriter.js","names":[],"sources":["../../../src/renderModes/ParticleInstanceWriter.ts"],"sourcesContent":["import type { GeometryAttribute } from '@codexo/exojs';\n\nimport type { ParticleBatch } from '#ParticleStorage';\nimport type { ParticleSystem } from '#ParticleSystem';\n\n/** Bytes one particle occupies in the interleaved per-instance buffer. */\nexport const instanceStrideBytes = 40;\n\n/** 32-bit words one particle occupies in that buffer. */\nexport const wordsPerInstance = instanceStrideBytes / Uint32Array.BYTES_PER_ELEMENT;\n\n/**\n * The per-instance attribute layout every instanced render mode declares, and\n * the layout the GPU compute pipeline emits into its instance buffer:\n *\n * ```\n * a_position f32x2 (offset 0, 8 bytes) particle position (system-local)\n * a_scale f32x2 (offset 8, 8 bytes)\n * a_rotation f32 (offset 16, 4 bytes) degrees\n * a_color u8x4 (offset 20, 4 bytes) RGBA tint, normalised\n * a_uvMin f32x2 (offset 24, 8 bytes) pre-resolved frame uvMin\n * a_uvMax f32x2 (offset 32, 8 bytes) pre-resolved frame uvMax\n * ```\n *\n * Shared rather than copied per mode so a mode using it cannot drift out of\n * step with the compute shader and quietly lose its GPU eligibility. `Geometry`\n * copies the attribute objects it is handed, so one frozen list serves every\n * mode.\n */\nexport const instanceAttributes: readonly GeometryAttribute[] = [\n { name: 'a_position', size: 2, type: 'f32', normalized: false, offset: 0 },\n { name: 'a_scale', size: 2, type: 'f32', normalized: false, offset: 8 },\n { name: 'a_rotation', size: 1, type: 'f32', normalized: false, offset: 16 },\n { name: 'a_color', size: 4, type: 'u8', normalized: true, offset: 20 },\n { name: 'a_uvMin', size: 2, type: 'f32', normalized: false, offset: 24 },\n { name: 'a_uvMax', size: 2, type: 'f32', normalized: false, offset: 32 },\n];\n\n/**\n * Fills the interleaved per-instance buffer shared by the instanced render\n * modes - one 40-byte record per live particle, in {@link instanceAttributes}\n * order.\n *\n * A class rather than a free function because the frame-UV table it resolves\n * per frame is scratch state that must survive between calls: the arithmetic\n * runs once per declared frame, not once per particle.\n *\n * This is the CPU counterpart of the compute pipeline's pack-instances step;\n * the two must agree byte for byte, since a GPU-eligible mode's system swaps\n * between them silently depending on the backend.\n */\nexport class ParticleInstanceWriter {\n private _uvMinsScratch = new Float32Array(2);\n private _uvMaxsScratch = new Float32Array(2);\n\n /**\n * Pack the live particles into the two views over one buffer, which must\n * already hold `particles.count * instanceStrideBytes` bytes. Returns the\n * number of instances written - dead slots in a GPU-mode system's live range\n * are skipped, so that can be below the count.\n */\n public write(system: ParticleSystem, particles: ParticleBatch, f32: Float32Array, u32: Uint32Array): number {\n const limit = particles.count;\n const { x: posX, y: posY } = particles.position;\n const { x: scaleX, y: scaleY } = particles.scale;\n const { angle: rotations } = particles.rotation;\n const { color, frame: textureIndex } = particles;\n\n // Pre-compute frame UVs from system.frames + texture; falls back\n // to the system.textureFrame when no atlas is declared.\n const { uvMins, uvMaxs } = this._computeFrameUvs(system);\n const frameCount = uvMins.length / 2;\n // Anything that is not a valid explicit index shows frame 0, matching both\n // the zero-initialised default and the compute pipeline. See\n // `ParticleSystem.textureIndex`.\n const fallbackFrame = 0;\n\n let writeIndex = 0;\n\n for (let i = 0; i < limit; i++) {\n // Skip dead slots in GPU-mode systems where the live range can\n // contain holes.\n if (!particles.isAlive(i)) {\n continue;\n }\n\n const offset = writeIndex * wordsPerInstance;\n const frame = textureIndex[i]! < frameCount ? textureIndex[i]! : fallbackFrame;\n const uvBase = frame * 2;\n\n f32[offset + 0] = posX[i]!;\n f32[offset + 1] = posY[i]!;\n f32[offset + 2] = scaleX[i]!;\n f32[offset + 3] = scaleY[i]!;\n f32[offset + 4] = rotations[i]!;\n u32[offset + 5] = color[i]!;\n f32[offset + 6] = uvMins[uvBase + 0]!;\n f32[offset + 7] = uvMins[uvBase + 1]!;\n f32[offset + 8] = uvMaxs[uvBase + 0]!;\n f32[offset + 9] = uvMaxs[uvBase + 1]!;\n\n writeIndex++;\n }\n\n return writeIndex;\n }\n\n /**\n * Compute (uvMin, uvMax) pairs for every declared frame on the system.\n * Pulled out of the hot pack loop so the arithmetic runs once per frame\n * rather than once per particle. Falls back to a single entry from\n * `system.textureFrame` when no atlas is declared.\n */\n private _computeFrameUvs(system: ParticleSystem): { uvMins: Float32Array; uvMaxs: Float32Array } {\n const frames = system.frames;\n const tex = system.texture;\n const texW = tex.width;\n const texH = tex.height;\n const flipY = tex.flipY;\n\n const count = frames.length === 0 ? 1 : frames.length;\n\n // Re-allocate scratch when capacity grows.\n if (this._uvMinsScratch.length < count * 2) {\n this._uvMinsScratch = new Float32Array(count * 2);\n this._uvMaxsScratch = new Float32Array(count * 2);\n }\n\n const mins = this._uvMinsScratch;\n const maxs = this._uvMaxsScratch;\n\n if (frames.length === 0) {\n const f = system.textureFrame;\n const minU = f.left / texW;\n const maxU = f.right / texW;\n const topV = f.top / texH;\n const bottomV = f.bottom / texH;\n\n mins[0] = minU;\n mins[1] = flipY ? bottomV : topV;\n maxs[0] = maxU;\n maxs[1] = flipY ? topV : bottomV;\n\n return { uvMins: mins, uvMaxs: maxs };\n }\n\n for (let i = 0; i < frames.length; i++) {\n const f = frames[i]!;\n const o = i * 2;\n const minU = f.left / texW;\n const maxU = f.right / texW;\n const topV = f.top / texH;\n const bottomV = f.bottom / texH;\n\n mins[o + 0] = minU;\n mins[o + 1] = flipY ? bottomV : topV;\n maxs[o + 0] = maxU;\n maxs[o + 1] = flipY ? topV : bottomV;\n }\n\n return { uvMins: mins, uvMaxs: maxs };\n }\n}\n"],"mappings":";;AAMA,MAAa,sBAAsB;;AAGnC,MAAa,wBAAyC,YAAY;;;;;;;;;;;;;;;;;;;AAoBlE,MAAa,qBAAmD;CAC9D;EAAE,MAAM;EAAc,MAAM;EAAG,MAAM;EAAO,YAAY;EAAO,QAAQ;CAAE;CACzE;EAAE,MAAM;EAAW,MAAM;EAAG,MAAM;EAAO,YAAY;EAAO,QAAQ;CAAE;CACtE;EAAE,MAAM;EAAc,MAAM;EAAG,MAAM;EAAO,YAAY;EAAO,QAAQ;CAAG;CAC1E;EAAE,MAAM;EAAW,MAAM;EAAG,MAAM;EAAM,YAAY;EAAM,QAAQ;CAAG;CACrE;EAAE,MAAM;EAAW,MAAM;EAAG,MAAM;EAAO,YAAY;EAAO,QAAQ;CAAG;CACvE;EAAE,MAAM;EAAW,MAAM;EAAG,MAAM;EAAO,YAAY;EAAO,QAAQ;CAAG;AACzE;;;;;;;;;;;;;;AAeA,IAAa,yBAAb,MAAoC;CAClC,AAAQ,iCAAiB,IAAI,aAAa,CAAC;CAC3C,AAAQ,iCAAiB,IAAI,aAAa,CAAC;;;;;;;CAQ3C,AAAO,MAAM,QAAwB,WAA0B,KAAmB,KAA0B;EAC1G,MAAM,QAAQ,UAAU;EACxB,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,UAAU;EACvC,MAAM,EAAE,GAAG,QAAQ,GAAG,WAAW,UAAU;EAC3C,MAAM,EAAE,OAAO,cAAc,UAAU;EACvC,MAAM,EAAE,OAAO,OAAO,iBAAiB;EAIvC,MAAM,EAAE,QAAQ,WAAW,KAAK,iBAAiB,MAAM;EACvD,MAAM,aAAa,OAAO,SAAS;EAInC,MAAM,gBAAgB;EAEtB,IAAI,aAAa;EAEjB,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAG9B,IAAI,CAAC,UAAU,QAAQ,CAAC,GACtB;GAGF,MAAM,SAAS,aAAa;GAE5B,MAAM,UADQ,aAAa,KAAM,aAAa,aAAa,KAAM,iBAC1C;GAEvB,IAAI,SAAS,KAAK,KAAK;GACvB,IAAI,SAAS,KAAK,KAAK;GACvB,IAAI,SAAS,KAAK,OAAO;GACzB,IAAI,SAAS,KAAK,OAAO;GACzB,IAAI,SAAS,KAAK,UAAU;GAC5B,IAAI,SAAS,KAAK,MAAM;GACxB,IAAI,SAAS,KAAK,OAAO,SAAS;GAClC,IAAI,SAAS,KAAK,OAAO,SAAS;GAClC,IAAI,SAAS,KAAK,OAAO,SAAS;GAClC,IAAI,SAAS,KAAK,OAAO,SAAS;GAElC;EACF;EAEA,OAAO;CACT;;;;;;;CAQA,AAAQ,iBAAiB,QAAwE;EAC/F,MAAM,SAAS,OAAO;EACtB,MAAM,MAAM,OAAO;EACnB,MAAM,OAAO,IAAI;EACjB,MAAM,OAAO,IAAI;EACjB,MAAM,QAAQ,IAAI;EAElB,MAAM,QAAQ,OAAO,WAAW,IAAI,IAAI,OAAO;EAG/C,IAAI,KAAK,eAAe,SAAS,QAAQ,GAAG;GAC1C,KAAK,iBAAiB,IAAI,aAAa,QAAQ,CAAC;GAChD,KAAK,iBAAiB,IAAI,aAAa,QAAQ,CAAC;EAClD;EAEA,MAAM,OAAO,KAAK;EAClB,MAAM,OAAO,KAAK;EAElB,IAAI,OAAO,WAAW,GAAG;GACvB,MAAM,IAAI,OAAO;GACjB,MAAM,OAAO,EAAE,OAAO;GACtB,MAAM,OAAO,EAAE,QAAQ;GACvB,MAAM,OAAO,EAAE,MAAM;GACrB,MAAM,UAAU,EAAE,SAAS;GAE3B,KAAK,KAAK;GACV,KAAK,KAAK,QAAQ,UAAU;GAC5B,KAAK,KAAK;GACV,KAAK,KAAK,QAAQ,OAAO;GAEzB,OAAO;IAAE,QAAQ;IAAM,QAAQ;GAAK;EACtC;EAEA,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;GACtC,MAAM,IAAI,OAAO;GACjB,MAAM,IAAI,IAAI;GACd,MAAM,OAAO,EAAE,OAAO;GACtB,MAAM,OAAO,EAAE,QAAQ;GACvB,MAAM,OAAO,EAAE,MAAM;GACrB,MAAM,UAAU,EAAE,SAAS;GAE3B,KAAK,IAAI,KAAK;GACd,KAAK,IAAI,KAAK,QAAQ,UAAU;GAChC,KAAK,IAAI,KAAK;GACd,KAAK,IAAI,KAAK,QAAQ,OAAO;EAC/B;EAEA,OAAO;GAAE,QAAQ;GAAM,QAAQ;EAAK;CACtC;AACF"}
@@ -0,0 +1,18 @@
1
+ import type { MaterialOptions } from '@codexo/exojs';
2
+ import { Material } from '@codexo/exojs';
3
+ /**
4
+ * Material specialization for particle render modes.
5
+ *
6
+ * `Material`'s constructor is protected, so every material family declares a
7
+ * thin subclass that fixes {@link target} and opens construction up - this is
8
+ * the particle counterpart of core's `MeshMaterial` and `SpriteMaterial`.
9
+ *
10
+ * Particle draws bind their system-level state (transform, local bounds,
11
+ * texture) through the renderer rather than through the material, so this
12
+ * subclass adds no behaviour of its own; it exists to name the contract.
13
+ */
14
+ export declare class ParticleMaterial extends Material {
15
+ readonly target = "particle";
16
+ constructor(options: MaterialOptions);
17
+ }
18
+ //# sourceMappingURL=ParticleMaterial.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParticleMaterial.d.ts","sourceRoot":"","sources":["../../../src/renderModes/ParticleMaterial.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC;;;;;;;;;;GAUG;AACH,qBAAa,gBAAiB,SAAQ,QAAQ;IAC5C,SAAgB,MAAM,cAAc;gBAEjB,OAAO,EAAE,eAAe;CAG5C"}
@@ -0,0 +1,24 @@
1
+ import { Material } from "@codexo/exojs";
2
+
3
+ //#region src/renderModes/ParticleMaterial.ts
4
+ /**
5
+ * Material specialization for particle render modes.
6
+ *
7
+ * `Material`'s constructor is protected, so every material family declares a
8
+ * thin subclass that fixes {@link target} and opens construction up - this is
9
+ * the particle counterpart of core's `MeshMaterial` and `SpriteMaterial`.
10
+ *
11
+ * Particle draws bind their system-level state (transform, local bounds,
12
+ * texture) through the renderer rather than through the material, so this
13
+ * subclass adds no behaviour of its own; it exists to name the contract.
14
+ */
15
+ var ParticleMaterial = class extends Material {
16
+ target = "particle";
17
+ constructor(options) {
18
+ super(options);
19
+ }
20
+ };
21
+
22
+ //#endregion
23
+ export { ParticleMaterial };
24
+ //# sourceMappingURL=ParticleMaterial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParticleMaterial.js","names":[],"sources":["../../../src/renderModes/ParticleMaterial.ts"],"sourcesContent":["import type { MaterialOptions } from '@codexo/exojs';\nimport { Material } from '@codexo/exojs';\n\n/**\n * Material specialization for particle render modes.\n *\n * `Material`'s constructor is protected, so every material family declares a\n * thin subclass that fixes {@link target} and opens construction up - this is\n * the particle counterpart of core's `MeshMaterial` and `SpriteMaterial`.\n *\n * Particle draws bind their system-level state (transform, local bounds,\n * texture) through the renderer rather than through the material, so this\n * subclass adds no behaviour of its own; it exists to name the contract.\n */\nexport class ParticleMaterial extends Material {\n public readonly target = 'particle';\n\n public constructor(options: MaterialOptions) {\n super(options);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,IAAa,mBAAb,cAAsC,SAAS;CAC7C,AAAgB,SAAS;CAEzB,AAAO,YAAY,SAA0B;EAC3C,MAAM,OAAO;CACf;AACF"}
@@ -0,0 +1,114 @@
1
+ import type { Geometry, Material } from '@codexo/exojs';
2
+ import type { ParticleBatch } from '#ParticleStorage';
3
+ import type { ParticleSystem } from '#ParticleSystem';
4
+ import type { ParticleBufferLayout } from './ParticleBufferLayout';
5
+ /**
6
+ * Turns a particle system's SoA storage into drawable vertex data.
7
+ *
8
+ * A mode owns the whole "how": the vertex layout, the shader pair, the draw
9
+ * model, and the loop that fills the buffer. The backend renderers only upload
10
+ * what it produces and issue the draw it declares, so a new primitive is a new
11
+ * mode rather than a renderer change.
12
+ *
13
+ * Implementations are fixed at system construction (see
14
+ * `ParticleSystemOptions.render`) and default to `QuadParticles`.
15
+ *
16
+ * **Limits of the seam.** The executors are deliberately thin, so a new mode
17
+ * has to stay inside what they can express:
18
+ *
19
+ * - The geometry's `stride` must be a multiple of 4. WebGPU's
20
+ * `queue.writeBuffer` validates its copy size against that alignment and
21
+ * rejects anything else, so a mode with, say, a 38-byte stride draws on
22
+ * WebGL2 and fails validation on WebGPU.
23
+ * - An **indexed, non-instanced** mode always draws the geometry's fixed
24
+ * `indexCount` and ignores {@link count} on both backends. That is right for
25
+ * a fixed-topology mode and wrong for one whose element count varies per
26
+ * frame; such a mode needs the executors taught to derive an index count
27
+ * from {@link count} first. No mode ships in that shape today.
28
+ * - An **instanced** mode that declares no {@link vertexGeometry} has to derive
29
+ * its vertices in the shader from the vertex index, and its
30
+ * {@link dataLayout} must carry the indices that address them. Without either
31
+ * there is nothing to tell the executors how many vertices one instance
32
+ * spans.
33
+ */
34
+ export declare abstract class ParticleRenderMode {
35
+ /**
36
+ * Layout of the buffer {@link build} fills each frame: attributes, stride,
37
+ * upload hint, and - when this mode declares no {@link vertexGeometry} - the
38
+ * topology and index list the draw uses.
39
+ *
40
+ * Named after {@link data}, the buffer it describes. It holds per-instance
41
+ * records for an instanced mode and per-vertex records otherwise, so a name
42
+ * fixed to either would be wrong for one of the two draw models.
43
+ */
44
+ abstract readonly dataLayout: ParticleBufferLayout;
45
+ /**
46
+ * Fixed per-vertex geometry this mode instances, or `null` when it derives
47
+ * its vertices in the shader from the vertex index - which is what
48
+ * `QuadParticles` and `RibbonParticles` do.
49
+ *
50
+ * When set, it supplies the topology, index list and index count for the draw
51
+ * in place of {@link dataLayout}'s, and the executors bind its `vertexData`
52
+ * as a second vertex buffer stepping per vertex beside the per-instance one.
53
+ * Only meaningful on an instanced mode: without instancing both buffers would
54
+ * step per vertex and no draw is expressible.
55
+ *
56
+ * Attribute names must not collide with {@link dataLayout}'s, since both sets
57
+ * bind into the same shader. Mutating the geometry is picked up on the next
58
+ * draw through its `version`, so `invalidate()` after an in-place edit is
59
+ * enough to reach the GPU.
60
+ */
61
+ readonly vertexGeometry: Geometry | null;
62
+ /** Shader pair plus uniforms/textures for this mode. */
63
+ abstract readonly material: Material;
64
+ /**
65
+ * Draw model. `true` issues an instanced draw, `false` a plain draw over the
66
+ * built vertex buffer. Declared here because a layout carries topology but
67
+ * has no instancing concept.
68
+ */
69
+ abstract readonly instanced: boolean;
70
+ /**
71
+ * Whether this mode can run while the system is in GPU compute mode.
72
+ * Mirrors `UpdateModule.wgsl()`: a mode that cannot forces the whole system
73
+ * onto the CPU path, silently and observably via `ParticleSystem.gpuMode`.
74
+ *
75
+ * A GPU-eligible mode's layout must match what the compute pipeline emits
76
+ * into `gpuState.instanceBuffer` - the shared 40-byte per-instance layout
77
+ * (`instanceAttributes`). `QuadParticles` and `MeshParticles` both declare
78
+ * exactly that layout and differ only in the shape they expand it into,
79
+ * which is why both are eligible without the compute shader knowing either.
80
+ */
81
+ readonly gpuEligible: boolean;
82
+ private _data;
83
+ private _count;
84
+ /**
85
+ * Element count of the draw call this mode's last {@link build} produced.
86
+ * Draw-model relative: instance count when {@link instanced}, vertex count
87
+ * otherwise. Do not assume one meaning.
88
+ */
89
+ get count(): number;
90
+ /** The buffer the renderer uploads. Valid until the next {@link build}. */
91
+ get data(): ArrayBuffer;
92
+ /**
93
+ * Fill the scratch buffer from the system's live particles.
94
+ *
95
+ * `particles` is the simulation's own channel storage, handed in rather than
96
+ * read off the system: a render mode is the one consumer that legitimately
97
+ * works in bulk, and it runs at the point in the frame where those values are
98
+ * the ones being drawn.
99
+ */
100
+ abstract build(system: ParticleSystem, particles: ParticleBatch): void;
101
+ /** Optional cleanup, called from `ParticleSystem.destroy`. */
102
+ destroy(): void;
103
+ /** Record the element count produced by a {@link build}. */
104
+ protected _setCount(count: number): void;
105
+ /**
106
+ * Grow the scratch buffer to hold at least `byteLength`. Grow-only: a
107
+ * shrinking particle count reuses the larger buffer rather than
108
+ * reallocating, matching the renderers' existing buffer policy.
109
+ */
110
+ protected _ensureCapacity(byteLength: number): void;
111
+ /** Re-create typed-array views after {@link _ensureCapacity} reallocates. */
112
+ protected _onBufferGrown(_data: ArrayBuffer): void;
113
+ }
114
+ //# sourceMappingURL=ParticleRenderMode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParticleRenderMode.d.ts","sourceRoot":"","sources":["../../../src/renderModes/ParticleRenderMode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,8BAAsB,kBAAkB;IACtC;;;;;;;;OAQG;IACH,kBAAyB,UAAU,EAAE,oBAAoB,CAAC;IAE1D;;;;;;;;;;;;;;;OAeG;IACH,SAAgB,cAAc,EAAE,QAAQ,GAAG,IAAI,CAAQ;IAEvD,wDAAwD;IACxD,kBAAyB,QAAQ,EAAE,QAAQ,CAAC;IAE5C;;;;OAIG;IACH,kBAAyB,SAAS,EAAE,OAAO,CAAC;IAE5C;;;;;;;;;;OAUG;IACH,SAAgB,WAAW,EAAE,OAAO,CAAS;IAE7C,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,MAAM,CAAK;IAEnB;;;;OAIG;IACH,IAAW,KAAK,IAAI,MAAM,CAEzB;IAED,2EAA2E;IAC3E,IAAW,IAAI,IAAI,WAAW,CAE7B;IAED;;;;;;;OAOG;aACa,KAAK,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,aAAa,GAAG,IAAI;IAE7E,8DAA8D;IACvD,OAAO,IAAI,IAAI;IAEtB,4DAA4D;IAC5D,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIxC;;;;OAIG;IACH,SAAS,CAAC,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAenD,6EAA6E;IAC7E,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;CACnD"}
@@ -0,0 +1,99 @@
1
+ //#region src/renderModes/ParticleRenderMode.ts
2
+ /**
3
+ * Turns a particle system's SoA storage into drawable vertex data.
4
+ *
5
+ * A mode owns the whole "how": the vertex layout, the shader pair, the draw
6
+ * model, and the loop that fills the buffer. The backend renderers only upload
7
+ * what it produces and issue the draw it declares, so a new primitive is a new
8
+ * mode rather than a renderer change.
9
+ *
10
+ * Implementations are fixed at system construction (see
11
+ * `ParticleSystemOptions.render`) and default to `QuadParticles`.
12
+ *
13
+ * **Limits of the seam.** The executors are deliberately thin, so a new mode
14
+ * has to stay inside what they can express:
15
+ *
16
+ * - The geometry's `stride` must be a multiple of 4. WebGPU's
17
+ * `queue.writeBuffer` validates its copy size against that alignment and
18
+ * rejects anything else, so a mode with, say, a 38-byte stride draws on
19
+ * WebGL2 and fails validation on WebGPU.
20
+ * - An **indexed, non-instanced** mode always draws the geometry's fixed
21
+ * `indexCount` and ignores {@link count} on both backends. That is right for
22
+ * a fixed-topology mode and wrong for one whose element count varies per
23
+ * frame; such a mode needs the executors taught to derive an index count
24
+ * from {@link count} first. No mode ships in that shape today.
25
+ * - An **instanced** mode that declares no {@link vertexGeometry} has to derive
26
+ * its vertices in the shader from the vertex index, and its
27
+ * {@link dataLayout} must carry the indices that address them. Without either
28
+ * there is nothing to tell the executors how many vertices one instance
29
+ * spans.
30
+ */
31
+ var ParticleRenderMode = class {
32
+ /**
33
+ * Fixed per-vertex geometry this mode instances, or `null` when it derives
34
+ * its vertices in the shader from the vertex index - which is what
35
+ * `QuadParticles` and `RibbonParticles` do.
36
+ *
37
+ * When set, it supplies the topology, index list and index count for the draw
38
+ * in place of {@link dataLayout}'s, and the executors bind its `vertexData`
39
+ * as a second vertex buffer stepping per vertex beside the per-instance one.
40
+ * Only meaningful on an instanced mode: without instancing both buffers would
41
+ * step per vertex and no draw is expressible.
42
+ *
43
+ * Attribute names must not collide with {@link dataLayout}'s, since both sets
44
+ * bind into the same shader. Mutating the geometry is picked up on the next
45
+ * draw through its `version`, so `invalidate()` after an in-place edit is
46
+ * enough to reach the GPU.
47
+ */
48
+ vertexGeometry = null;
49
+ /**
50
+ * Whether this mode can run while the system is in GPU compute mode.
51
+ * Mirrors `UpdateModule.wgsl()`: a mode that cannot forces the whole system
52
+ * onto the CPU path, silently and observably via `ParticleSystem.gpuMode`.
53
+ *
54
+ * A GPU-eligible mode's layout must match what the compute pipeline emits
55
+ * into `gpuState.instanceBuffer` - the shared 40-byte per-instance layout
56
+ * (`instanceAttributes`). `QuadParticles` and `MeshParticles` both declare
57
+ * exactly that layout and differ only in the shape they expand it into,
58
+ * which is why both are eligible without the compute shader knowing either.
59
+ */
60
+ gpuEligible = false;
61
+ _data = /* @__PURE__ */ new ArrayBuffer(0);
62
+ _count = 0;
63
+ /**
64
+ * Element count of the draw call this mode's last {@link build} produced.
65
+ * Draw-model relative: instance count when {@link instanced}, vertex count
66
+ * otherwise. Do not assume one meaning.
67
+ */
68
+ get count() {
69
+ return this._count;
70
+ }
71
+ /** The buffer the renderer uploads. Valid until the next {@link build}. */
72
+ get data() {
73
+ return this._data;
74
+ }
75
+ /** Optional cleanup, called from `ParticleSystem.destroy`. */
76
+ destroy() {}
77
+ /** Record the element count produced by a {@link build}. */
78
+ _setCount(count) {
79
+ this._count = count;
80
+ }
81
+ /**
82
+ * Grow the scratch buffer to hold at least `byteLength`. Grow-only: a
83
+ * shrinking particle count reuses the larger buffer rather than
84
+ * reallocating, matching the renderers' existing buffer policy.
85
+ */
86
+ _ensureCapacity(byteLength) {
87
+ if (this._data.byteLength >= byteLength) return;
88
+ let next = Math.max(this._data.byteLength, 1);
89
+ while (next < byteLength) next *= 2;
90
+ this._data = new ArrayBuffer(next);
91
+ this._onBufferGrown(this._data);
92
+ }
93
+ /** Re-create typed-array views after {@link _ensureCapacity} reallocates. */
94
+ _onBufferGrown(_data) {}
95
+ };
96
+
97
+ //#endregion
98
+ export { ParticleRenderMode };
99
+ //# sourceMappingURL=ParticleRenderMode.js.map