insomni 0.2.0-alpha.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.
@@ -0,0 +1,816 @@
1
+ import { d as CustomDrawableContext, ln as LayerSpace, r as Renderer2D, u as CustomDrawable } from "./renderer-DzZqd1bY.mjs";
2
+ import { g as GPUOwner } from "./text-font-D7GGDtTK.mjs";
3
+ import { TgpuRoot } from "typegpu";
4
+
5
+ //#region src/particles/codegen/pipeline-cache.d.ts
6
+ type PipelineFactory = () => GPUComputePipeline;
7
+ declare class PipelineCache {
8
+ private readonly cache;
9
+ /**
10
+ * Return the pipeline for `key`, calling `factory()` to build it on miss.
11
+ * Factories are only invoked on cold lookups.
12
+ */
13
+ getOrCreate(key: string, factory: PipelineFactory): GPUComputePipeline;
14
+ /** Current cache size — useful for tests and introspection. */
15
+ get size(): number;
16
+ /**
17
+ * Drop every cached pipeline. WebGPU pipelines are reference-counted and
18
+ * released when the JS handle becomes unreferenced; nothing extra needed.
19
+ */
20
+ clear(): void;
21
+ }
22
+ //#endregion
23
+ //#region src/particles/types.d.ts
24
+ /** WGSL-mappable scalar / vector types a user attribute may take. */
25
+ type AttributeType = "f32" | "u32" | "i32" | "vec2f" | "vec3f" | "vec4f" | "vec2u" | "vec4u";
26
+ /** Declared at system construction — name → WGSL type. */
27
+ type AttributeCatalog = Record<string, AttributeType>;
28
+ type BoundsMode = {
29
+ kind: "none";
30
+ } | {
31
+ kind: "clamp";
32
+ rect: [minX: number, minY: number, maxX: number, maxY: number];
33
+ } | {
34
+ kind: "wrap";
35
+ rect: [minX: number, minY: number, maxX: number, maxY: number];
36
+ } | {
37
+ kind: "bounce";
38
+ rect: [minX: number, minY: number, maxX: number, maxY: number];
39
+ restitution?: number;
40
+ };
41
+ interface ParticleSystemOptions {
42
+ /**
43
+ * Hard cap on simultaneously live particles. Bounded above by the
44
+ * compaction module's single-pass-B scan limit (262144 in v1). Buffers
45
+ * are sized to this; there is no auto-grow.
46
+ */
47
+ capacity: number;
48
+ /** User-declared extra attributes beyond the always-present built-ins. */
49
+ attributes?: AttributeCatalog;
50
+ /** Fixed simulation timestep. Default 1 / 60. */
51
+ dt?: number;
52
+ bounds?: BoundsMode;
53
+ }
54
+ //#endregion
55
+ //#region src/particles/emitter.d.ts
56
+ /** Local tuple form used by emitter samplers and specs. The math module's
57
+ * `Vec2` is object-shaped (`{x, y}`); this subsystem prefers tuples so
58
+ * samplers can produce/consume array literals without allocation churn. */
59
+ type Vec2 = readonly [x: number, y: number];
60
+ /** Scalar that can be sampled as a point in 2D space. */
61
+ type PositionShape = {
62
+ kind: "point";
63
+ at: Vec2 | (() => Vec2);
64
+ } | {
65
+ kind: "disc";
66
+ center: Vec2;
67
+ radius: number;
68
+ } | {
69
+ kind: "ring";
70
+ center: Vec2;
71
+ radius: number;
72
+ thickness?: number;
73
+ } | {
74
+ kind: "rect";
75
+ min: Vec2;
76
+ max: Vec2;
77
+ } | {
78
+ kind: "line";
79
+ a: Vec2;
80
+ b: Vec2;
81
+ } | {
82
+ kind: "grid";
83
+ origin: Vec2;
84
+ cols: number;
85
+ rows: number;
86
+ spacing: number | Vec2;
87
+ } | {
88
+ kind: "fn";
89
+ fn: (i: number, count: number) => Vec2;
90
+ };
91
+ type VelocityShape = {
92
+ kind: "zero";
93
+ } | {
94
+ kind: "uniform";
95
+ value: Vec2;
96
+ } | {
97
+ kind: "random";
98
+ speed: [min: number, max: number];
99
+ } | {
100
+ kind: "cone";
101
+ dir: Vec2 | (() => Vec2);
102
+ spread: number;
103
+ speed: [number, number];
104
+ } | {
105
+ kind: "tangential";
106
+ center: Vec2;
107
+ speed: [number, number];
108
+ ccw?: boolean;
109
+ } | {
110
+ kind: "fn";
111
+ fn: (i: number, count: number) => Vec2;
112
+ };
113
+ /** Either a fixed value or a `[min, max]` range to sample uniformly. */
114
+ type ScalarRange = number | readonly [number, number];
115
+ /**
116
+ * Per-attribute value at emit time. Scalar types accept a fixed number, a
117
+ * `[min, max]` range, or a sampler `(i, count) => number`. Vector types
118
+ * accept a fixed array, or a sampler `(i, count) => readonly number[]`.
119
+ *
120
+ * For integer attribute types (`u32`, `i32`, `vec2u`, `vec4u`) floats are
121
+ * truncated via `Math.floor` when packing. Omitted attributes default to
122
+ * zero.
123
+ */
124
+ type AttributeSampler = number | readonly [number, number] | readonly number[] | ((i: number, count: number) => number | readonly number[]);
125
+ interface EmitSpec {
126
+ count: number;
127
+ position: PositionShape;
128
+ velocity?: VelocityShape;
129
+ /** Per-particle lifetime in seconds. Default `Infinity` (immortal). */
130
+ lifetime?: ScalarRange;
131
+ /** Per-particle user-attribute values. Unmentioned attrs default to zero. */
132
+ attributes?: Readonly<Record<string, AttributeSampler>>;
133
+ }
134
+ /**
135
+ * RNG interface. Defaults to `Math.random`. Tests inject a deterministic
136
+ * source for reproducible assertions.
137
+ */
138
+ type Rng = () => number;
139
+ //#endregion
140
+ //#region src/particles/state.d.ts
141
+ interface BuiltinBuffers {
142
+ readonly positions: GPUBuffer;
143
+ readonly velocities: GPUBuffer;
144
+ readonly accelPrev: GPUBuffer;
145
+ readonly accel: GPUBuffer;
146
+ readonly ages: GPUBuffer;
147
+ readonly lifetimes: GPUBuffer;
148
+ readonly alive: GPUBuffer;
149
+ readonly aliveCount: GPUBuffer;
150
+ }
151
+ interface IndirectBuffers {
152
+ /** dispatchWorkgroupsIndirect args: [workgroupCountX, 1, 1] */
153
+ readonly dispatchArgs: GPUBuffer;
154
+ /** drawIndexedIndirect args: [indexCount, instanceCount, firstIndex, baseVertex, firstInstance] */
155
+ readonly drawArgs: GPUBuffer;
156
+ }
157
+ interface UserAttributeBuffer {
158
+ readonly name: string;
159
+ readonly type: AttributeType;
160
+ readonly strideBytes: number;
161
+ readonly buffer: GPUBuffer;
162
+ /**
163
+ * Declaration-order index. Matches `@group(1) @binding(<index>)` in the
164
+ * prelude — so caller code that needs to resolve an attribute's binding
165
+ * reads this field.
166
+ */
167
+ readonly bindingIndex: number;
168
+ }
169
+ /** One attribute's main + scratch buffers + codegen metadata. */
170
+ interface ScatteredAttribute {
171
+ readonly name: string;
172
+ /** WGSL element type (e.g. `vec2f`, `f32`, `u32`). Used to pick a scatter pipeline. */
173
+ readonly wgslType: string;
174
+ /** Bytes per element on the GPU (std430-aligned). Used for copyBufferToBuffer. */
175
+ readonly strideBytes: number;
176
+ /** The main (live) attribute buffer. Forces / integrator / render read & write it. */
177
+ readonly main: GPUBuffer;
178
+ /** Compaction scratch — scatter writes here, then copyBufferToBuffer lifts it back. */
179
+ readonly scratch: GPUBuffer;
180
+ }
181
+ interface CompactionBuffers {
182
+ /**
183
+ * Per-slot exclusive prefix sum within its 1024-element block. Written by
184
+ * scan-local, read by scatter. Sized to `capacity` u32.
185
+ */
186
+ readonly scanLocal: GPUBuffer;
187
+ /**
188
+ * Per-block total alive count. Written by scan-local (one entry per block),
189
+ * read by scan-blocks. Sized to `blockCount` u32.
190
+ */
191
+ readonly blockTotals: GPUBuffer;
192
+ /**
193
+ * Per-block exclusive prefix of blockTotals. Written by scan-blocks, read
194
+ * by scatter. Sized to `blockCount` u32.
195
+ */
196
+ readonly blockOffsets: GPUBuffer;
197
+ /**
198
+ * Two u32s: `[base, actualCount]`. Written by emit-reserve (single thread),
199
+ * read by the emit kernel. Exposing this lets the reserve + emit kernels
200
+ * share a GPU-resident hand-off with no CPU round trip.
201
+ */
202
+ readonly emitReserve: GPUBuffer;
203
+ /**
204
+ * Number of scan blocks = `ceil(capacity / ELEMENTS_PER_BLOCK)`. Bounded by
205
+ * `COMPACT_BLOCK_SIZE` (= 256) by the capacity cap so pass B stays single-
206
+ * workgroup.
207
+ */
208
+ readonly blockCount: number;
209
+ }
210
+ interface ParticleState {
211
+ readonly capacity: number;
212
+ readonly catalog: AttributeCatalog;
213
+ readonly builtins: BuiltinBuffers;
214
+ readonly indirect: IndirectBuffers;
215
+ readonly compaction: CompactionBuffers;
216
+ /** Declaration-ordered — `userAttrs[i].bindingIndex === i`. */
217
+ readonly userAttrs: readonly UserAttributeBuffer[];
218
+ /**
219
+ * Every attribute that must survive compaction — built-ins (sans `accel`
220
+ * and `alive`) followed by user attributes in declaration order. The
221
+ * compaction pipeline iterates this list once per step.
222
+ */
223
+ readonly scatteredAttributes: readonly ScatteredAttribute[];
224
+ readonly stateBindGroupLayout: GPUBindGroupLayout;
225
+ readonly userAttrBindGroupLayout: GPUBindGroupLayout;
226
+ readonly stateBindGroup: GPUBindGroup;
227
+ readonly userAttrBindGroup: GPUBindGroup;
228
+ destroy(): void;
229
+ }
230
+ //#endregion
231
+ //#region src/particles/spatial-hash.d.ts
232
+ interface ParticleSpatialHashOptions {
233
+ readonly root: TgpuRoot;
234
+ readonly state: ParticleState;
235
+ /** World-space maximum pair distance. Drives cell size (= 2 × reach). */
236
+ readonly reach: number;
237
+ readonly label?: string;
238
+ }
239
+ declare class ParticleSpatialHash {
240
+ readonly reach: number;
241
+ readonly cellSize: number;
242
+ readonly bucketCount: number;
243
+ /** Buffers consumers bind in their accumulate kernels (heads/next/cells/params). */
244
+ readonly consumeBindGroupLayout: GPUBindGroupLayout;
245
+ readonly consumeBindGroup: GPUBindGroup;
246
+ private readonly device;
247
+ private readonly heads;
248
+ private readonly next;
249
+ private readonly cells;
250
+ private readonly paramsBuffer;
251
+ private readonly clearBindGroup;
252
+ private readonly clearPipeline;
253
+ private readonly clearWorkgroups;
254
+ private readonly buildBindGroup;
255
+ private readonly buildPipeline;
256
+ constructor(opts: ParticleSpatialHashOptions);
257
+ /**
258
+ * Record clear + indirect build onto the open compute pass. Caller supplies
259
+ * the same `dispatchArgs` buffer `prepareIndirect` wrote this frame.
260
+ */
261
+ record(pass: GPUComputePassEncoder, dispatchArgs: GPUBuffer): void;
262
+ destroy(): void;
263
+ }
264
+ //#endregion
265
+ //#region src/particles/forces/types.d.ts
266
+ interface ForceAttachContext {
267
+ readonly root: TgpuRoot;
268
+ readonly state: ParticleState;
269
+ /**
270
+ * Label prefix for GPU-object debug names. Forces should concatenate their
271
+ * own kind: e.g. `${ctx.label}.drag.pipeline`.
272
+ */
273
+ readonly label: string;
274
+ /**
275
+ * Request (or get the existing) spatial hash for this reach. Reaches within
276
+ * a small epsilon share an instance. Only pairwise forces use this; per-
277
+ * particle forces ignore it.
278
+ */
279
+ readonly requestSpatialHash: (reach: number) => ParticleSpatialHash;
280
+ }
281
+ interface ForceHandle {
282
+ /**
283
+ * Record this force's dispatch onto an open compute pass. Forces may record
284
+ * one or more dispatches (e.g. pairwise = build + accumulate); no structural
285
+ * requirement beyond not ending the pass.
286
+ */
287
+ record(pass: GPUComputePassEncoder, ctx: ForceStepContext): void;
288
+ destroy(): void;
289
+ }
290
+ interface ForceStepContext {
291
+ /** Buffer populated by `prepareIndirect` — 3×u32 `[wgCount, 1, 1]`. */
292
+ readonly dispatchArgs: GPUBuffer;
293
+ /** Fixed step size this frame, in seconds. */
294
+ readonly dt: number;
295
+ }
296
+ interface Force {
297
+ readonly kind: string;
298
+ attach(ctx: ForceAttachContext): ForceHandle;
299
+ /**
300
+ * Optional per-step CPU hook for forces that need to sample some JS-side
301
+ * state and write it into a uniform (e.g. pointer position). Called before
302
+ * `record()` each step.
303
+ */
304
+ prepare?(ctx: ForceStepContext): void;
305
+ }
306
+ /** Opaque ID the system returns from `addForce` to identify a force later. */
307
+ type ForceId = number;
308
+ //#endregion
309
+ //#region src/particles/render/shader.wgsl.d.ts
310
+ type ShapeKind = "circle" | "square" | "triangle" | "rounded-square";
311
+ type ShapeOption = ShapeKind | {
312
+ readonly sdf: string;
313
+ };
314
+ type ColorOption = "uniform" | {
315
+ readonly wgsl: string;
316
+ };
317
+ type FadeOption = "none" | "linear" | "lut";
318
+ //#endregion
319
+ //#region src/particles/render/drawable-v3.d.ts
320
+ interface Color4 {
321
+ r: number;
322
+ g: number;
323
+ b: number;
324
+ a: number;
325
+ }
326
+ type FadeByAgeOption = boolean | {
327
+ readonly curve: (t: number) => number;
328
+ };
329
+ interface DrawableV3Options {
330
+ /** Shape SDF. Default 'circle'. */
331
+ shape?: ShapeOption;
332
+ /** World-unit half-size (particles are quads of 2·size). Default 4. */
333
+ size?: number;
334
+ /**
335
+ * Uniform RGBA for every live particle. Default opaque white. Ignored
336
+ * when `colorWgsl` is supplied.
337
+ */
338
+ color?: Color4;
339
+ /**
340
+ * Custom fragment color WGSL. Must be a snippet that `return`s a `vec4f`.
341
+ * Receives a local `p: Particle` with fields `position, velocity, age,
342
+ * lifetime, index`. Mutually exclusive with `color`.
343
+ */
344
+ colorWgsl?: string;
345
+ /**
346
+ * Seconds of velocity-direction travel added to the quad's length. `0`
347
+ * (default) keeps the particle round.
348
+ */
349
+ velocityStretch?: number;
350
+ /**
351
+ * Fade each particle's alpha by its normalized age. `true` applies a linear
352
+ * 1→0 ramp. `{ curve }` samples the callback at 64 points in [0, 1] and
353
+ * LUTs it onto the GPU. Immortal particles (lifetime = Infinity) are
354
+ * unaffected.
355
+ */
356
+ fadeByAge?: FadeByAgeOption;
357
+ /**
358
+ * Coordinate space the drawable occupies. Particles are world-space by
359
+ * default; the renderer binds the matching per-space camera bind group.
360
+ */
361
+ space?: LayerSpace;
362
+ }
363
+ declare class ParticleDrawableV3 implements CustomDrawable {
364
+ readonly __customDrawable: true;
365
+ readonly space: LayerSpace;
366
+ readonly clipRect: undefined;
367
+ readonly opaque = false;
368
+ private readonly device;
369
+ private readonly bundle;
370
+ private readonly drawArgsBuffer;
371
+ private readonly scratch;
372
+ private lutTexture;
373
+ private lutSampler;
374
+ private lutBindGroup;
375
+ private size;
376
+ private color;
377
+ private velocityStretch;
378
+ private fadeByAge;
379
+ private destroyed;
380
+ constructor(renderer: Renderer2D, state: ParticleState, options?: DrawableV3Options);
381
+ /**
382
+ * Replace color / size / stretch / fade flag. Takes effect on the next
383
+ * frame. Does NOT rebuild the pipeline — shape and colorWgsl are baked in
384
+ * at construction. Swapping a curve function re-uploads the LUT texture.
385
+ */
386
+ setOptions(options: DrawableV3Options): void;
387
+ record(pass: GPURenderPassEncoder, ctx: CustomDrawableContext): void;
388
+ destroy(): void;
389
+ private buildLut;
390
+ private uploadParams;
391
+ }
392
+ //#endregion
393
+ //#region src/particles/system.d.ts
394
+ /**
395
+ * GPU particle system. Manages a fixed-capacity pool of particles with SoA
396
+ * attribute storage, a pluggable force chain, CPU-driven emission, and
397
+ * dedicated draw-indirect rendering.
398
+ *
399
+ * Typical usage:
400
+ *
401
+ * ```ts
402
+ * const system = new ParticleSystem(renderer.getRoot(), {
403
+ * capacity: 20_000,
404
+ * bounds: { kind: "wrap", rect: [-400, -300, 400, 300] },
405
+ * });
406
+ * system.addForce(drag({ coefficient: 0.2 }));
407
+ * system.addForce(gravity({ direction: [0, 1], strength: 300 }));
408
+ * const drawable = system.createDrawable(renderer, { fadeByAge: true });
409
+ *
410
+ * // per frame:
411
+ * system.emit({ count: 20, position: {...}, velocity: {...} });
412
+ * system.step(dt);
413
+ * renderer.render([drawable]);
414
+ * ```
415
+ *
416
+ * Capacity is bounded at construction (no auto-grow). The compaction
417
+ * module runs every step to keep the alive list dense; `getAliveCount()`
418
+ * returns the GPU-authoritative count via a non-blocking readback.
419
+ */
420
+ declare class ParticleSystem {
421
+ private readonly options;
422
+ readonly capacity: number;
423
+ /** Attribute names in declaration order. Binding indices match this order. */
424
+ readonly attributeNames: readonly string[];
425
+ /** @internal — exposed for kernel attach in later phases. */
426
+ readonly state: ParticleState;
427
+ /** @internal — shared across every pipeline this system compiles. */
428
+ readonly pipelineCache: PipelineCache;
429
+ private readonly root;
430
+ private readonly dt;
431
+ private readonly drawables;
432
+ private readonly emitQueue;
433
+ private readonly forces;
434
+ /** Spatial hashes keyed by quantized reach. Shared across pairwise forces. */
435
+ private readonly hashes;
436
+ private nextForceId;
437
+ private emitStagingCursor;
438
+ private emitPipeline;
439
+ private prepareIndirectPipeline;
440
+ private zeroAccelPipeline;
441
+ private integratorPipeline;
442
+ private boundsPipeline;
443
+ private compactionPipeline;
444
+ private aliveCountCache;
445
+ private aliveCountReadback;
446
+ private aliveCountReadbackInFlight;
447
+ private destroyed;
448
+ constructor(owner: GPUOwner, options: ParticleSystemOptions);
449
+ getAttributeNames(): readonly string[];
450
+ /**
451
+ * Write raw data into a user-declared attribute buffer at the given element
452
+ * offset. Intended for one-shot initialisation (e.g. seeding `species` after
453
+ * emitting a fixed population) and interactive tooling — not a hot-path API;
454
+ * the emit kernel does not yet write user attributes, so callers that need
455
+ * per-emission attribute values fill them here directly.
456
+ */
457
+ writeAttribute(name: string, data: ArrayBufferView, offsetElements?: number): void;
458
+ /**
459
+ * Most recent live-particle count read from the GPU. aliveCount is
460
+ * GPU-authoritative — the CPU does not mirror it exactly — so this returns
461
+ * a cached value from the last completed readback. Calling this schedules
462
+ * a fresh readback in the background (non-blocking); repeated callers
463
+ * (e.g. a per-frame HUD) converge within one frame of latency. Returns 0
464
+ * until the first readback completes, which normally takes 1-2 frames.
465
+ */
466
+ getAliveCount(): number;
467
+ private scheduleAliveCountReadback;
468
+ /**
469
+ * Build a `CustomDrawable` that renders this system's live particles.
470
+ * Pass to the v3 renderer's `render([...])`. Multiple drawables may share
471
+ * a system.
472
+ */
473
+ createV3Drawable(renderer: Renderer2D, options?: DrawableV3Options): ParticleDrawableV3;
474
+ /**
475
+ * Queue an emission. CPU-packs now; the staging upload + reserve + emit
476
+ * dispatch run at the start of the next `step()`. Overflow past capacity
477
+ * is dropped silently on the GPU (the reserve kernel clamps to the
478
+ * remaining room). Multiple emits in one frame coalesce into one upload.
479
+ */
480
+ emit(spec: EmitSpec): void;
481
+ /**
482
+ * Attach a force. Returns a numeric id that can later be passed to
483
+ * `removeForce`. Forces run in registration order during each `step()`,
484
+ * accumulating into the per-particle acceleration buffer before the
485
+ * integrator sweep.
486
+ */
487
+ addForce(force: Force): ForceId;
488
+ private getOrCreateHash;
489
+ removeForce(id: ForceId): void;
490
+ hasForce(id: ForceId): boolean;
491
+ /**
492
+ * Advance the simulation by one tick. Records the full pipeline
493
+ * (emission → prepareIndirect → zeroAccel → forces → integrate → bounds
494
+ * → compaction + copyback) into a single command encoder and submits.
495
+ *
496
+ * `dt` overrides the system's configured timestep for this call only.
497
+ * Useful for variable-rate rendering (e.g. reduce dt when catching up on
498
+ * multiple frames).
499
+ */
500
+ step(dt?: number): void;
501
+ destroy(): void;
502
+ private flushEmissions;
503
+ private ensureEmitPipeline;
504
+ private ensurePrepareIndirect;
505
+ private ensureZeroAccel;
506
+ private ensureIntegrator;
507
+ private ensureBounds;
508
+ private ensureCompaction;
509
+ private pendingEmitCount;
510
+ private assertAlive;
511
+ }
512
+ //#endregion
513
+ //#region src/particles/forces/attractor.d.ts
514
+ type AttractorFalloff = "inverseSquare" | "linear" | "spring";
515
+ interface AttractorOptions {
516
+ /** World-space anchor the attractor pulls toward (negative strength pushes). */
517
+ point: readonly [number, number];
518
+ /** Signed strength. Positive = pull, negative = push. */
519
+ strength: number;
520
+ /**
521
+ * Distance beyond which the force is zero. Omit / set to 0 for an
522
+ * unbounded field. Linear falloff interprets `reach` as the distance where
523
+ * the force has decayed to zero.
524
+ */
525
+ reach?: number;
526
+ /** Default: `'inverseSquare'`. */
527
+ falloff?: AttractorFalloff;
528
+ }
529
+ /**
530
+ * Radial attraction / repulsion around a fixed world point. Use negative
531
+ * `strength` for repulsion. Falloff kinds control how force decays with
532
+ * distance; `reach` optionally hard-clips beyond that radius.
533
+ */
534
+ declare function attractor(options: AttractorOptions): Force;
535
+ interface PointerOptions {
536
+ /** Callable returning the current world-space target each frame. */
537
+ target: () => readonly [number, number];
538
+ /**
539
+ * Attraction strength. A number is static; a callback is resampled every
540
+ * step — flip sign to toggle attract/repel, set to 0 to disable without
541
+ * pipeline churn.
542
+ */
543
+ strength: number | (() => number);
544
+ reach?: number;
545
+ falloff?: AttractorFalloff;
546
+ }
547
+ /**
548
+ * Like `attractor`, but the anchor point is resampled from `target()` on
549
+ * every step. Built on the same kernel — pointer dragging the canvas is the
550
+ * canonical use.
551
+ */
552
+ declare function pointer(options: PointerOptions): Force;
553
+ //#endregion
554
+ //#region src/particles/forces/boids.d.ts
555
+ interface BoidsOptions {
556
+ /** Neighbourhood radius. Also the cell size driver for the shared hash. */
557
+ reach: number;
558
+ /** Alignment weight — match neighbours' heading. Typical 0.5–1.5. */
559
+ alignment?: number;
560
+ /** Cohesion weight — steer toward local centroid. Typical 0.3–1.0. */
561
+ cohesion?: number;
562
+ /** Separation weight — push away from close neighbours. Typical 1.0–2.0. */
563
+ separation?: number;
564
+ /**
565
+ * Distance at which separation kicks in. Should be smaller than `reach`;
566
+ * default = reach × 0.4.
567
+ */
568
+ separationRadius?: number;
569
+ /**
570
+ * Target cruising speed used to build desired-velocity vectors. The kernel
571
+ * itself doesn't clamp speed — pair with a `drag` force or clamp in your
572
+ * integrator.
573
+ */
574
+ maxSpeed?: number;
575
+ }
576
+ /**
577
+ * Reynolds boids — alignment, cohesion, separation over a spatial-hash
578
+ * neighbourhood. Shares the system's hash at the given `reach`.
579
+ */
580
+ declare function boids(options: BoidsOptions): Force;
581
+ //#endregion
582
+ //#region src/particles/forces/drag.d.ts
583
+ interface DragOptions {
584
+ /**
585
+ * Per-second velocity-decay coefficient. Larger → more damping. Applied as
586
+ * an acceleration, so the integrator step is `v' = v + (a_other - c*v)·dt`.
587
+ */
588
+ coefficient: number;
589
+ }
590
+ /** Linear velocity damping. `a += -coefficient * v`. */
591
+ declare function drag(options: DragOptions): Force;
592
+ //#endregion
593
+ //#region src/particles/forces/field.d.ts
594
+ interface FieldWorldBounds {
595
+ /** World-space x of the texture's left edge. */
596
+ readonly x: number;
597
+ /** World-space y of the texture's bottom edge. */
598
+ readonly y: number;
599
+ /** World-space width the texture spans. */
600
+ readonly width: number;
601
+ /** World-space height the texture spans. */
602
+ readonly height: number;
603
+ }
604
+ /** Row-major 2×3 affine as six numbers: [m00, m01, m02, m10, m11, m12]. */
605
+ type FieldAffine2x3 = readonly [number, number, number, number, number, number];
606
+ type FieldWrap = "clamp" | "repeat";
607
+ interface FieldOptions {
608
+ /** 2D GPU texture encoding a vec2 field in its R and G channels. */
609
+ readonly texture: GPUTexture;
610
+ /** Acceleration scalar. Multiplies the sampled vector. */
611
+ readonly strength: number;
612
+ /**
613
+ * Rectangle in world space that the texture covers, mapping to uv [0..1]².
614
+ * Mutually exclusive with `worldToUv`. If neither is given, the texture is
615
+ * treated as covering `[0, width] × [0, height]` world units 1:1.
616
+ */
617
+ readonly worldBounds?: FieldWorldBounds;
618
+ /**
619
+ * Explicit world→uv affine matrix. Row-major 2×3:
620
+ * `uv = [[m00 m01 m02], [m10 m11 m12]] · [x, y, 1]ᵀ`.
621
+ */
622
+ readonly worldToUv?: FieldAffine2x3;
623
+ /** Sampler address mode outside the unit UV square. Default 'clamp'. */
624
+ readonly wrap?: FieldWrap;
625
+ /** Sampler filter. Default 'linear' (falls back to 'nearest' for non-filterable formats). */
626
+ readonly filter?: "linear" | "nearest";
627
+ }
628
+ /**
629
+ * Texture-sampled flow-field force. Particles receive
630
+ * `sampleRG(worldToUv·pos) * strength` as an acceleration each step.
631
+ */
632
+ declare function field(options: FieldOptions): Force;
633
+ declare class FieldHandle implements ForceHandle {
634
+ private readonly device;
635
+ private readonly paramsBuffer;
636
+ private readonly paramsScratch;
637
+ private readonly bindGroup;
638
+ private readonly pipeline;
639
+ private strength;
640
+ private matrix;
641
+ constructor(ctx: ForceAttachContext, opts: FieldOptions);
642
+ /** Swap strength at runtime without rebuilding the pipeline. */
643
+ setStrength(strength: number): void;
644
+ record(pass: GPUComputePassEncoder, ctx: ForceStepContext): void;
645
+ destroy(): void;
646
+ }
647
+ //#endregion
648
+ //#region src/particles/forces/gravity.d.ts
649
+ interface GravityOptions {
650
+ /** Direction of the gravity vector. Does not need to be normalized. */
651
+ direction: [number, number];
652
+ /** Magnitude in world-units / second². Applied along the normalized direction. */
653
+ strength: number;
654
+ }
655
+ /** Uniform acceleration in a fixed direction. Classic "falling" force. */
656
+ declare function gravity(options: GravityOptions): Force;
657
+ //#endregion
658
+ //#region src/particles/forces/noise.d.ts
659
+ type NoiseKind = "simplex" | "curl";
660
+ interface NoiseOptions {
661
+ /** Noise frequency (cycles per world unit). Typical: 0.002–0.05. */
662
+ scale: number;
663
+ /** Force magnitude scalar. Rough guideline: match your drag coefficient. */
664
+ strength: number;
665
+ /** Default `'curl'` — the divergence-free flow-field look. */
666
+ kind?: NoiseKind;
667
+ /**
668
+ * How fast the field evolves over time. 0 freezes it. Default 1 — each
669
+ * second of simulation advances the noise seed by 1 unit.
670
+ */
671
+ timeScale?: number;
672
+ }
673
+ /**
674
+ * Simplex-noise flow force. `curl` (default) gives a divergence-free field
675
+ * for wispy, flow-like motion; `simplex` applies the scalar gradient
676
+ * directly and looks more turbulent.
677
+ */
678
+ declare function noise(options: NoiseOptions): Force;
679
+ //#endregion
680
+ //#region src/particles/forces/pairwise.d.ts
681
+ interface PairwiseExtraBinding {
682
+ /**
683
+ * WGSL declaration line — e.g. `@group(3) @binding(1) var<storage, read> pairMatrix: array<f32>;`.
684
+ * Binding indices must be ≥ 1 (binding 0 is reserved for the params uniform).
685
+ * Note: `matrix` is a reserved WGSL identifier — use `pairMatrix` or similar.
686
+ */
687
+ readonly wgsl: string;
688
+ readonly layoutEntry: GPUBindGroupLayoutEntry;
689
+ readonly bindGroupEntry: GPUBindGroupEntry;
690
+ }
691
+ interface PairwiseParamsSpec {
692
+ /** Full WGSL `struct PairwiseParams { ... };` declaration. */
693
+ readonly structWgsl: string;
694
+ /** Byte size of the packed struct (std140 uniform rules). */
695
+ readonly bytes: number;
696
+ /**
697
+ * Optional initial pack — receives a DataView into a fresh ArrayBuffer of
698
+ * `bytes` length in little-endian. Callers can also upload later via
699
+ * `PairwiseHandle.writeParams`.
700
+ */
701
+ readonly write?: (view: DataView) => void;
702
+ }
703
+ interface PairwiseOptions {
704
+ /** Max pair distance in world units. Hash cell size = 2 × reach. */
705
+ readonly reach: number;
706
+ /** User attributes to expose on the `me` / `other` Particle structs. */
707
+ readonly reads?: readonly string[];
708
+ /**
709
+ * WGSL body run inside the neighbourhood loop. Has access to `me`, `other`,
710
+ * `addAccel(index, delta)`, `params.*`, `hashParams.*` and standard WGSL.
711
+ * Must not end the loop — a trailing `cursor = next[cursor];` is appended
712
+ * by the wrapper.
713
+ */
714
+ readonly wgsl: string;
715
+ readonly params?: PairwiseParamsSpec;
716
+ readonly extraBindings?: readonly PairwiseExtraBinding[];
717
+ /** Debug label suffix appended to `ctx.label`. Default `pairwise`. */
718
+ readonly label?: string;
719
+ }
720
+ /**
721
+ * Generic pairwise force. Typically invoked by presets (`particleLife`,
722
+ * `boids`) but also exposed directly for ad-hoc user kernels.
723
+ */
724
+ declare function pairwise(options: PairwiseOptions): Force;
725
+ declare class PairwiseHandle implements ForceHandle {
726
+ private readonly device;
727
+ private readonly hash;
728
+ private readonly pipeline;
729
+ private readonly stateBindGroup;
730
+ private readonly userAttrBindGroup;
731
+ private readonly hashBindGroup;
732
+ private readonly paramsGroup;
733
+ private readonly paramsBuffer;
734
+ private readonly paramsSize;
735
+ constructor(ctx: ForceAttachContext, opts: PairwiseOptions);
736
+ record(pass: GPUComputePassEncoder, ctx: ForceStepContext): void;
737
+ /**
738
+ * Overwrite the params uniform. Caller packs into a view matching the
739
+ * struct declared at construction. Bytes beyond `paramsSize` are ignored.
740
+ */
741
+ writeParams(data: ArrayBufferView): void;
742
+ destroy(): void;
743
+ }
744
+ //#endregion
745
+ //#region src/particles/forces/speed-limit.d.ts
746
+ interface SpeedLimitOptions {
747
+ /** Maximum allowed speed in world-units per second. */
748
+ maxSpeed: number;
749
+ }
750
+ /**
751
+ * Hard cap on particle speed. Applies a corrective acceleration so that
752
+ * after the next integrator step, speed ≤ maxSpeed. Register AFTER other
753
+ * forces to see the full accumulated excess.
754
+ */
755
+ declare function speedLimit(options: SpeedLimitOptions): Force;
756
+ //#endregion
757
+ //#region src/particles/forces/particle-life.d.ts
758
+ interface PairRule {
759
+ /** Signed long-range attraction coefficient. Negative = repel. */
760
+ attract: number;
761
+ /** Per-pair max reach (0 → fall back to global reach). */
762
+ reach?: number;
763
+ /** Per-pair short-range repel distance (0 → fall back to global minDist). */
764
+ minDist?: number;
765
+ }
766
+ declare class PairMatrix {
767
+ readonly speciesCount: number;
768
+ /** Flat f32 data, length = speciesCount² × 4. Row-major. */
769
+ readonly data: Float32Array;
770
+ constructor(speciesCount: number);
771
+ set(a: number, b: number, rule: PairRule): void;
772
+ get(a: number, b: number): PairRule;
773
+ }
774
+ interface RandomPairMatrixOptions {
775
+ /** `[min, max]` attract range, uniform. Default `[-1, 1]`. */
776
+ attract?: [number, number];
777
+ /** Optional per-pair reach range; omit to inherit the force's global reach. */
778
+ reach?: [number, number];
779
+ /** Optional per-pair minDist range; omit to inherit the force's global minDist. */
780
+ minDist?: [number, number];
781
+ /** PRNG — default `Math.random`. */
782
+ rand?: () => number;
783
+ }
784
+ declare function randomPairMatrix(speciesCount: number, opts?: RandomPairMatrixOptions): PairMatrix;
785
+ interface ParticleLifeOptions {
786
+ /** Global max pair distance. Per-pair `matrix[a,b].reach` may override per pair. */
787
+ reach: number;
788
+ /** Universal short-range repel distance. Per-pair override via matrix. */
789
+ minDist: number;
790
+ /** Repel strength when `d < minDist`. Default 20. */
791
+ repelStrength?: number;
792
+ /** Initial matrix. Size fixes `speciesCount`; swap via `updateMatrix`. */
793
+ matrix: PairMatrix;
794
+ /** Name of the declared `u32` species attribute. Default `species`. */
795
+ speciesAttribute?: string;
796
+ }
797
+ /**
798
+ * Create a particleLife force. The returned Force's handle exposes
799
+ * `updateMatrix(newMatrix)` so callers can regenerate the matrix without
800
+ * rebuilding the pipeline.
801
+ */
802
+ declare function particleLife(options: ParticleLifeOptions): ParticleLifeForce;
803
+ declare class ParticleLifeForce implements Force {
804
+ readonly kind = "particleLife";
805
+ private readonly opts;
806
+ private handle;
807
+ constructor(opts: ParticleLifeOptions);
808
+ attach(ctx: ForceAttachContext): ForceHandle;
809
+ /**
810
+ * Replace the matrix contents on the GPU. Attach first; no-op if called
811
+ * before. The matrix size must match the speciesCount fixed at attach time.
812
+ */
813
+ updateMatrix(matrix: PairMatrix): void;
814
+ }
815
+ //#endregion
816
+ export { type AttractorFalloff, type AttractorOptions, type AttributeCatalog, type AttributeSampler, type AttributeType, type BoidsOptions, type BoundsMode, type Color4, type ColorOption, type DragOptions, type DrawableV3Options, type EmitSpec, type FadeByAgeOption, type FadeOption, type FieldAffine2x3, FieldHandle, type FieldOptions, type FieldWorldBounds, type FieldWrap, type Force, type ForceAttachContext, type ForceHandle, type ForceId, type ForceStepContext, type GPUOwner, type GravityOptions, type NoiseKind, type NoiseOptions, PairMatrix, type PairRule, type PairwiseExtraBinding, PairwiseHandle, type PairwiseOptions, type PairwiseParamsSpec, ParticleDrawableV3, ParticleLifeForce, type ParticleLifeOptions, ParticleSpatialHash, ParticleSystem, type ParticleSystemOptions, type PointerOptions, type PositionShape, type RandomPairMatrixOptions, type Rng, type ScalarRange, type ShapeKind, type ShapeOption, type SpeedLimitOptions, type VelocityShape, attractor, boids, drag, field, gravity, noise, pairwise, particleLife, pointer, randomPairMatrix, speedLimit };