@aethervtt/shard-particles 0.0.1

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.
@@ -0,0 +1,347 @@
1
+ import { MODULES } from "./modules.js";
2
+ import { wgslFloat as f, WGSL_RANDOM } from "./values.js";
3
+ const PARTICLE_FLOATS = 16;
4
+ const SIM_BYTES = 336;
5
+ const DRAW_BYTES = 112;
6
+ const PARTICLE = `
7
+ struct Particle {
8
+ /** position, age */
9
+ p0: vec4f,
10
+ /** velocity, lifetime (0: never spawned) */
11
+ p1: vec4f,
12
+ /** size, rotation, rotation speed, seed bits */
13
+ p2: vec4f,
14
+ /** initial color (linear, alpha) */
15
+ p3: vec4f,
16
+ }`;
17
+ const SIM = `
18
+ struct Sim {
19
+ /** The entity's transform. */
20
+ model: mat4x4f,
21
+ /** Where spawns go: the transform (world space) or identity (local). */
22
+ spawn: mat4x4f,
23
+ view_proj: mat4x4f,
24
+ inv_view_proj: mat4x4f,
25
+ camera: vec4f,
26
+ viewport: vec4f,
27
+ dt: f32,
28
+ time: f32,
29
+ capacity: u32,
30
+ spawn_base: u32,
31
+ spawn_count: u32,
32
+ seed: u32,
33
+ local: u32,
34
+ has_depth: u32,
35
+ /** A floating-origin shift to add to particles alive before this frame (spec 0040); else 0. */
36
+ origin_offset: vec4f,
37
+ }`;
38
+ function modules(def) {
39
+ return def.update.map((p, i) => ({ def: MODULES[p.module], p, id: `m${i}` }));
40
+ }
41
+ function emitterKey(def) {
42
+ return JSON.stringify([def.shape, def.init, def.update, def.render.mode, def.render.flipbook]);
43
+ }
44
+ function simulationShader(def) {
45
+ const ms = modules(def);
46
+ const s = def.shape;
47
+ const i = def.init;
48
+ const lo = (r) => f(r[0]);
49
+ const hi = (r) => f(r[1]);
50
+ const c0 = `vec4f(${i.color[0].map(f).join(", ")})`;
51
+ const c1 = `vec4f(${i.color[1].map(f).join(", ")})`;
52
+ const shape = {
53
+ point: "dir = random_dir(seed, 6u);",
54
+ sphere: `dir = random_dir(seed, 6u); p = dir * ${f(s.radius)} * pow(rand(seed, 9u), 0.3333333);`,
55
+ cone: `{
56
+ let a = radians(${f(s.angle)}) * sqrt(rand(seed, 6u));
57
+ let phi = 6.2831853 * rand(seed, 7u);
58
+ dir = vec3f(sin(a) * cos(phi), cos(a), sin(a) * sin(phi));
59
+ let r = ${f(s.radius)} * sqrt(rand(seed, 8u));
60
+ p = vec3f(cos(phi) * r, 0.0, sin(phi) * r);
61
+ }`,
62
+ box: `p = (vec3f(rand(seed, 6u), rand(seed, 7u), rand(seed, 8u)) - 0.5) * vec3f(${s.size.map(f).join(", ")}); dir = vec3f(0.0, 1.0, 0.0);`
63
+ }[s.type];
64
+ return `${WGSL_RANDOM}
65
+ ${PARTICLE}
66
+ ${SIM}
67
+
68
+ @group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
69
+ @group(0) @binding(1) var<uniform> sim: Sim;
70
+ @group(0) @binding(2) var depth_texture: texture_depth_2d;
71
+ @group(0) @binding(3) var<storage, read_write> counter: array<atomic<u32>, 4>;
72
+
73
+ fn random_dir(seed: u32, k: u32) -> vec3f {
74
+ let z = rand(seed, k) * 2.0 - 1.0;
75
+ let phi = 6.2831853 * rand(seed, k + 1u);
76
+ let r = sqrt(max(1.0 - z * z, 0.0));
77
+ return vec3f(r * cos(phi), z, r * sin(phi));
78
+ }
79
+
80
+ fn surface_at(px: vec2i) -> vec3f {
81
+ let d = textureLoad(depth_texture, px, 0);
82
+ let uv = (vec2f(px) + 0.5) / sim.viewport.xy;
83
+ let w = sim.inv_view_proj * vec4f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, d, 1.0);
84
+ return w.xyz / w.w;
85
+ }
86
+
87
+ /** Collision with what the camera sees: the surface normal (xyz) and 1 when the next step hits. */
88
+ fn depth_collision(pos: vec3f, vel: vec3f, thickness: f32) -> vec4f {
89
+ if (sim.has_depth == 0u) { return vec4f(0.0); }
90
+ var world = pos + vel * sim.dt;
91
+ if (sim.local != 0u) { world = (sim.model * vec4f(world, 1.0)).xyz; }
92
+ let clip = sim.view_proj * vec4f(world, 1.0);
93
+ if (clip.w <= 0.0) { return vec4f(0.0); }
94
+ let ndc = clip.xyz / clip.w;
95
+ let uv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
96
+ if (any(uv < vec2f(0.0)) || any(uv >= vec2f(1.0))) { return vec4f(0.0); }
97
+ let size = vec2i(sim.viewport.xy);
98
+ let px = min(vec2i(uv * sim.viewport.xy), size - 2);
99
+ let d = textureLoad(depth_texture, px, 0);
100
+ // Reversed-Z: the surface is in front of the particle's next position, and close to it.
101
+ if (d <= 0.0 || d <= ndc.z) { return vec4f(0.0); }
102
+ let surface = surface_at(px);
103
+ if (distance(surface, world) > thickness) { return vec4f(0.0); }
104
+ var n = normalize(cross(surface_at(px + vec2i(0, 1)) - surface, surface_at(px + vec2i(1, 0)) - surface));
105
+ if (dot(n, sim.camera.xyz - surface) < 0.0) { n = -n; }
106
+ if (sim.local != 0u) { n = normalize((transpose(sim.model) * vec4f(n, 0.0)).xyz); }
107
+ return vec4f(n, 1.0);
108
+ }
109
+
110
+ ${ms.map((m) => m.def.functions?.(m.p, m.id) ?? "").join("\n")}
111
+
112
+ /** One thread per new particle: slot k mod capacity, so allocation is deterministic. */
113
+ @compute @workgroup_size(64)
114
+ fn spawn(@builtin(global_invocation_id) gid: vec3u) {
115
+ if (gid.x >= sim.spawn_count) { return; }
116
+ let k = sim.spawn_base + gid.x;
117
+ let slot = k % sim.capacity;
118
+ let seed = pcg(sim.seed ^ pcg(k));
119
+ var p = vec3f(0.0);
120
+ var dir = vec3f(0.0, 1.0, 0.0);
121
+ ${shape}
122
+ var pos = p;
123
+ var vel = dir * mix(${lo(i.speed)}, ${hi(i.speed)}, rand(seed, 1u));
124
+ let life = max(mix(${lo(i.lifetime)}, ${hi(i.lifetime)}, rand(seed, 2u)), 1e-3);
125
+ let size = mix(${lo(i.size)}, ${hi(i.size)}, rand(seed, 3u));
126
+ let rot = radians(mix(${lo(i.rotation)}, ${hi(i.rotation)}, rand(seed, 4u)));
127
+ let color = mix(${c0}, ${c1}, rand(seed, 5u));
128
+ var rot_speed = 0.0;
129
+ ${ms.map((m) => m.def.init?.(m.p) ?? "").join("\n ")}
130
+ if (sim.local == 0u) {
131
+ pos = (sim.spawn * vec4f(pos, 1.0)).xyz;
132
+ vel = (sim.spawn * vec4f(vel, 0.0)).xyz;
133
+ }
134
+ particles[slot] = Particle(vec4f(pos, 0.0), vec4f(vel, life), vec4f(size, rot, rot_speed, bitcast<f32>(seed)), color);
135
+ }
136
+
137
+ var<workgroup> alive: atomic<u32>;
138
+
139
+ /** One thread per slot: age, the modules, integrate; counts the living. */
140
+ @compute @workgroup_size(64)
141
+ fn update(@builtin(global_invocation_id) gid: vec3u, @builtin(local_invocation_index) li: u32) {
142
+ if (li == 0u) { atomicStore(&alive, 0u); }
143
+ workgroupBarrier();
144
+ if (gid.x < sim.capacity) {
145
+ var q = particles[gid.x];
146
+ let life = q.p1.w;
147
+ var age = q.p0.w;
148
+ if (life > 0.0 && age < life) {
149
+ let dt = sim.dt;
150
+ let time = sim.time;
151
+ let seed = bitcast<u32>(q.p2.w);
152
+ let rot_speed = q.p2.z;
153
+ var pos = q.p0.xyz;
154
+ // Slots spawned this frame are already in the new origin frame; older ones catch up.
155
+ let origin_fresh = (gid.x + sim.capacity - sim.spawn_base % sim.capacity) % sim.capacity < sim.spawn_count;
156
+ if (!origin_fresh) { pos += sim.origin_offset.xyz; }
157
+ var vel = q.p1.xyz;
158
+ var rot = q.p2.y;
159
+ age += dt;
160
+ ${ms.map((m) => m.def.update?.(m.p, m.id) ?? "").join("\n ")}
161
+ pos += vel * dt;
162
+ q.p0 = vec4f(pos, age);
163
+ q.p1 = vec4f(vel, life);
164
+ q.p2.y = rot;
165
+ particles[gid.x] = q;
166
+ if (age < life) { atomicAdd(&alive, 1u); }
167
+ }
168
+ }
169
+ workgroupBarrier();
170
+ if (li == 0u) {
171
+ let n = atomicLoad(&alive);
172
+ if (n > 0u) { atomicAdd(&counter[0], n); }
173
+ }
174
+ }`;
175
+ }
176
+ function renderShader(def) {
177
+ const ms = modules(def);
178
+ const mode = def.render.mode;
179
+ const orient = {
180
+ billboard: `let right = cam_right; let up = cam_up;`,
181
+ stretched: `let axis = select(cam_up, normalize(vel_w), length(vel_w) > 1e-4);
182
+ let right = normalize(cross(axis, normalize(view.cameraPosition - center)));
183
+ let up = axis * (1.0 + length(vel_w) * draw.params.z / max(size, 1e-4));`,
184
+ axis: `let up = vec3f(0.0, 1.0, 0.0);
185
+ let right = normalize(cross(up, normalize(view.cameraPosition - center)));`,
186
+ mesh: ""
187
+ }[mode];
188
+ const corner = mode === "mesh" ? `let r = mat2x2f(cos(rot), sin(rot), -sin(rot), cos(rot));
189
+ let local = position * size;
190
+ let xz = r * local.xz;
191
+ let world = center + vec3f(xz.x, local.y, xz.y);
192
+ out.uv = vec2f(0.5);` : `let c = corner(v);
193
+ let off = (c * 2.0 - 1.0) * size * 0.5;
194
+ let rr = vec2f(off.x * cos(rot) - off.y * sin(rot), off.x * sin(rot) + off.y * cos(rot));
195
+ ${orient}
196
+ let world = center + right * rr.x + up * rr.y;
197
+ out.uv = vec2f(c.x, 1.0 - c.y);`;
198
+ return `import shard::view::view;
199
+ ${PARTICLE}
200
+
201
+ struct Draw {
202
+ model: mat4x4f,
203
+ /** emissive (cd/m\xB2), softness (m), stretch, local */
204
+ params: vec4f,
205
+ /** flipbook columns, rows, fps (0: over life), 0 */
206
+ flipbook: vec4f,
207
+ /** blend (0 additive, 1 alpha, 2 premultiplied), sorted, 0, 0 */
208
+ mode: vec4u,
209
+ }
210
+
211
+ @group(1) @binding(0) var<storage, read> particles: array<Particle>;
212
+ @group(1) @binding(1) var<uniform> draw: Draw;
213
+ @group(1) @binding(2) var<storage, read> order: array<u32>;
214
+ @group(2) @binding(0) var sprite: texture_2d<f32>;
215
+ @group(2) @binding(1) var sprite_sampler: sampler;
216
+ @group(2) @binding(2) var scene_depth: texture_depth_2d;
217
+
218
+ struct ParticleOutput {
219
+ @builtin(position) clip: vec4f,
220
+ @location(0) uv: vec2f,
221
+ @location(1) color: vec4f,
222
+ @location(2) view_depth: f32,
223
+ }
224
+
225
+ fn corner(k: u32) -> vec2f {
226
+ let c = array<vec2f, 6>(vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0), vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0));
227
+ return c[k];
228
+ }
229
+
230
+ ${ms.map((m) => m.def.functions?.(m.p, m.id) ?? "").join("\n")}
231
+
232
+ @vertex fn vs(
233
+ @builtin(vertex_index) v: u32,
234
+ @builtin(instance_index) instance: u32,
235
+ ${mode === "mesh" ? "@location(0) position: vec3f," : ""}
236
+ ) -> ParticleOutput {
237
+ var out: ParticleOutput;
238
+ let index = select(instance, order[instance], draw.mode.y != 0u);
239
+ let q = particles[index];
240
+ let life = q.p1.w;
241
+ let age = q.p0.w;
242
+ if (life <= 0.0 || age >= life) {
243
+ out.clip = vec4f(0.0, 0.0, 0.0, 0.0);
244
+ return out;
245
+ }
246
+ let t = age / life;
247
+ var color = q.p3;
248
+ var size = q.p2.x;
249
+ let rot = q.p2.y;
250
+ ${ms.map((m) => m.def.render?.(m.p, m.id) ?? "").join("\n ")}
251
+ var center = q.p0.xyz;
252
+ var vel_w = q.p1.xyz;
253
+ if (draw.params.w != 0.0) {
254
+ center = (draw.model * vec4f(center, 1.0)).xyz;
255
+ vel_w = (draw.model * vec4f(vel_w, 0.0)).xyz;
256
+ }
257
+ let cam_right = vec3f(view.view[0][0], view.view[1][0], view.view[2][0]);
258
+ let cam_up = vec3f(view.view[0][1], view.view[1][1], view.view[2][1]);
259
+ {
260
+ ${corner}
261
+ out.clip = view.viewProj * vec4f(world, 1.0);
262
+ out.view_depth = -(view.view * vec4f(world, 1.0)).z;
263
+ }
264
+ // Flipbook: frames across columns, then rows, by time or over life.
265
+ let frames = max(draw.flipbook.x * draw.flipbook.y, 1.0);
266
+ var frame = floor(t * frames);
267
+ if (draw.flipbook.z > 0.0) { frame = floor(age * draw.flipbook.z) % frames; }
268
+ let cell = vec2f(frame % draw.flipbook.x, floor(frame / draw.flipbook.x));
269
+ out.uv = (cell + out.uv) / max(draw.flipbook.xy, vec2f(1.0));
270
+ out.color = color;
271
+ return out;
272
+ }
273
+
274
+ @fragment fn fs(in: ParticleOutput) -> @location(0) vec4f {
275
+ var t = textureSample(sprite, sprite_sampler, in.uv);
276
+ var fade = 1.0;
277
+ if (draw.params.y > 0.0) {
278
+ // Soft particles: fade where the particle meets the scene behind it.
279
+ let d = textureLoad(scene_depth, vec2i(in.clip.xy), 0);
280
+ if (d > 0.0) {
281
+ let ndc = vec2f(in.clip.x * view.viewport.z * 2.0 - 1.0, 1.0 - in.clip.y * view.viewport.w * 2.0);
282
+ let w = view.invViewProj * vec4f(ndc, d, 1.0);
283
+ let scene = -(view.view * vec4f(w.xyz / w.w, 1.0)).z;
284
+ fade = clamp((scene - in.view_depth) / draw.params.y, 0.0, 1.0);
285
+ }
286
+ }
287
+ let blend = draw.mode.x;
288
+ let a = select(t.a * in.color.a, in.color.a, blend == 2u) * fade;
289
+ // Premultiplied textures already carry alpha in color.
290
+ let rgb = select(t.rgb * t.a, t.rgb, blend == 2u) * in.color.rgb * in.color.a * fade * draw.params.x * view.exposure;
291
+ if (blend == 0u) { return vec4f(rgb, 0.0); }
292
+ return vec4f(rgb, a);
293
+ }`;
294
+ }
295
+ const SORT_SHADER = `
296
+ import shard::view::view;
297
+ ${PARTICLE}
298
+
299
+ struct Step { j: u32, k: u32, n: u32, capacity: u32 }
300
+
301
+ @group(0) @binding(10) var<storage, read> particles: array<Particle>;
302
+ @group(0) @binding(11) var<storage, read_write> keys: array<f32>;
303
+ @group(0) @binding(12) var<storage, read_write> order: array<u32>;
304
+ @group(0) @binding(13) var<uniform> step: Step;
305
+ @group(0) @binding(14) var<uniform> model: mat4x4f;
306
+
307
+ @compute @workgroup_size(64)
308
+ fn keys_pass(@builtin(global_invocation_id) gid: vec3u) {
309
+ let i = gid.x;
310
+ if (i >= step.n) { return; }
311
+ order[i] = i;
312
+ var key = 3.0e38;
313
+ if (i < step.capacity) {
314
+ let q = particles[i];
315
+ if (q.p1.w > 0.0 && q.p0.w < q.p1.w) {
316
+ let world = (model * vec4f(q.p0.xyz, 1.0)).xyz;
317
+ key = (view.view * vec4f(world, 1.0)).z;
318
+ }
319
+ }
320
+ keys[i] = key;
321
+ }
322
+
323
+ @compute @workgroup_size(64)
324
+ fn sort_pass(@builtin(global_invocation_id) gid: vec3u) {
325
+ let i = gid.x;
326
+ let l = i ^ step.j;
327
+ if (i >= step.n || l <= i) { return; }
328
+ let ascending = (i & step.k) == 0u;
329
+ let a = keys[i];
330
+ let b = keys[l];
331
+ if ((a > b) == ascending) {
332
+ keys[i] = b;
333
+ keys[l] = a;
334
+ let t = order[i];
335
+ order[i] = order[l];
336
+ order[l] = t;
337
+ }
338
+ }`;
339
+ export {
340
+ DRAW_BYTES,
341
+ PARTICLE_FLOATS,
342
+ SIM_BYTES,
343
+ SORT_SHADER,
344
+ emitterKey,
345
+ renderShader,
346
+ simulationShader
347
+ };
package/dist/sim.d.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
3
+ import { type CameraData, type NodeDescriptor } from '@aethervtt/shard-render';
4
+ import { type EmitterDef, type ParticleEffect } from './effect.js';
5
+ /** Alpha emitters up to this capacity sort by depth; larger ones draw unsorted. */
6
+ export declare const MAX_SORTED = 65536;
7
+ /** One emitter of a running system: its particles, spawn schedule, and GPU objects. */
8
+ export interface EmitterState {
9
+ def: EmitterDef;
10
+ key: string;
11
+ capacity: number;
12
+ particles: GpuBuffer;
13
+ /** CPU backend: the particles, uploaded whole each frame. */
14
+ cpu: Float32Array | undefined;
15
+ sim: GpuBuffer;
16
+ draw: GpuBuffer;
17
+ counter: GPUBuffer;
18
+ readbacks: {
19
+ buffer: GPUBuffer;
20
+ busy: boolean;
21
+ }[];
22
+ /** Spawns so far: the next particle's slot is spawned % capacity. */
23
+ spawned: number;
24
+ carry: number;
25
+ /** Bursts fired so far, per burst. */
26
+ fired: number[];
27
+ spawnCount: number;
28
+ /** Spawns waiting for a dispatch (the GPU skipped a frame while compiling). */
29
+ backlog: number;
30
+ /** Alive particles from the last readback (a frame or two late). */
31
+ alive: number;
32
+ /** Sorting (alpha, small enough): keys, order, and per-step uniforms. */
33
+ sort: {
34
+ keys: GPUBuffer;
35
+ order: GPUBuffer;
36
+ steps: GPUBuffer;
37
+ count: number;
38
+ n: number;
39
+ } | undefined;
40
+ /** Simulated this frame (culled emitters may pause or step less often). */
41
+ simulate: boolean;
42
+ dt: number;
43
+ visible: boolean;
44
+ /**
45
+ * Floating-origin offset (spec 0040) the GPU hasn't applied yet: world-space particles move by it
46
+ * in the next update dispatch. Accumulates while the emitter isn't simulated.
47
+ */
48
+ originShift: Float64Array;
49
+ }
50
+ export interface SystemState {
51
+ entity: Entity;
52
+ effect: ParticleEffect;
53
+ version: number;
54
+ emitters: EmitterState[];
55
+ time: number;
56
+ seed: number;
57
+ local: boolean;
58
+ cpu: boolean;
59
+ model: Float32Array;
60
+ /** Bounding sphere (world) of the system, for culling. */
61
+ center: Float32Array;
62
+ radius: number;
63
+ frame: number;
64
+ seen: number;
65
+ }
66
+ /** Running particle systems by entity. */
67
+ export declare class ParticleStore {
68
+ readonly systems: Map<number, SystemState>;
69
+ frame: number;
70
+ /** The view whose camera simulation uses (depth collision, culling, sorting). */
71
+ primary: string | undefined;
72
+ primaryCamera: CameraData | undefined;
73
+ }
74
+ export declare const Particles: import("@aethervtt/shard-core").ResourceDef<ParticleStore>;
75
+ /**
76
+ * Advances every ParticleSystem: spawn counts, uniforms, culling against the main camera, and the
77
+ * CPU backend. The GPU work runs in the `particles/simulate` node.
78
+ */
79
+ export declare const prepareParticles: import("@aethervtt/shard-core").SystemDef<{
80
+ q: import("@aethervtt/shard-core").Query;
81
+ }>;
82
+ interface Caches {
83
+ generation: number;
84
+ layouts?: {
85
+ sim: GPUBindGroupLayout;
86
+ sort: GPUBindGroupLayout;
87
+ data: GPUBindGroupLayout;
88
+ texture: GPUBindGroupLayout;
89
+ view: GPUBindGroupLayout;
90
+ };
91
+ compute: Map<string, {
92
+ spawn: GPUComputePipeline;
93
+ update: GPUComputePipeline;
94
+ }>;
95
+ render: Map<string, GPURenderPipeline>;
96
+ sort?: {
97
+ keys: GPUComputePipeline;
98
+ sort: GPUComputePipeline;
99
+ };
100
+ groups: Map<string, {
101
+ key: string;
102
+ group: GPUBindGroup;
103
+ }>;
104
+ white?: GPUTexture;
105
+ noDepth?: GPUTexture;
106
+ noOrder?: GPUBuffer;
107
+ sampler?: GPUSampler;
108
+ }
109
+ export declare const ParticleCaches: import("@aethervtt/shard-core").ResourceDef<Caches>;
110
+ /** Spawn, update, and (alpha emitters) depth sort, for the primary camera's view. */
111
+ export declare function simulateNode(): NodeDescriptor;
112
+ /** Particles over the resolved HDR image, depth-tested against the scene without writing depth. */
113
+ export declare function drawNode(): NodeDescriptor;
114
+ /** Reads an emitter's particle buffer (tests: determinism, CPU vs GPU). */
115
+ export declare function readParticles(gpu: GpuContext, e: EmitterState): Promise<Float32Array>;
116
+ /**
117
+ * The floating origin moved by `offset` (spec 0040): world-space particles move with it so trails
118
+ * stay continuous. The CPU backend shifts its particles now; GPU emitters add it in their next
119
+ * update dispatch. Local-space systems move with their entity and need nothing.
120
+ */
121
+ export declare function shiftParticles(store: ParticleStore, x: number, y: number, z: number): void;
122
+ /** The particles section of `render.describe`. */
123
+ export declare function describeParticles(world: World): {
124
+ gpuMs: number | undefined;
125
+ systems: {
126
+ entity: number;
127
+ backend: string;
128
+ emitters: {
129
+ name: string;
130
+ alive: number;
131
+ capacity: number;
132
+ sorted: boolean;
133
+ visible: boolean;
134
+ spawned: number;
135
+ unsortedAlpha: boolean;
136
+ }[];
137
+ }[];
138
+ } | undefined;
139
+ export {};