@adatechnology/game-fluids-three 0.0.0-stage → 0.1.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,1117 @@
1
+ "use strict";
2
+ var GameFluidsThree = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
22
+
23
+ // src/index.ts
24
+ var index_exports = {};
25
+ __export(index_exports, {
26
+ createFluidRenderer: () => createFluidRenderer,
27
+ smoothOutline: () => smoothOutline
28
+ });
29
+
30
+ // src/clip.ts
31
+ var sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
32
+ var cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
33
+ var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
34
+ var norm = (a) => {
35
+ const l = Math.hypot(a[0], a[1], a[2]) || 1;
36
+ return [a[0] / l, a[1] / l, a[2] / l];
37
+ };
38
+ var orient = (p, n, outward) => {
39
+ if (dot(cross(sub(p[1], p[0]), sub(p[2], p[0])), outward) < 0) return { p: [...p].reverse(), n: [...n].reverse() };
40
+ return { p, n };
41
+ };
42
+ var finish = (faces) => {
43
+ let maxVertices = 0;
44
+ for (const f of faces) maxVertices += (f.p.length * 2 + 2) * 3;
45
+ maxVertices += faces.length * 3 * 2 + 64;
46
+ return { faces, maxVertices };
47
+ };
48
+ var cylinderSolid = (radius, height, segments) => {
49
+ const faces = [];
50
+ const ring = (y, i) => {
51
+ const a = i / segments * Math.PI * 2;
52
+ return [Math.cos(a) * radius, y, Math.sin(a) * radius];
53
+ };
54
+ const radial = (i) => {
55
+ const a = i / segments * Math.PI * 2;
56
+ return [Math.cos(a), 0, Math.sin(a)];
57
+ };
58
+ for (let i = 0; i < segments; i++) {
59
+ const j = (i + 1) % segments;
60
+ const mid = norm([radial(i)[0] + radial(j)[0], 0, radial(i)[2] + radial(j)[2]]);
61
+ faces.push(orient([ring(0, i), ring(0, j), ring(height, j), ring(height, i)], [radial(i), radial(j), radial(j), radial(i)], mid));
62
+ }
63
+ const bottom = [];
64
+ const top = [];
65
+ for (let i = 0; i < segments; i++) {
66
+ bottom.push(ring(0, i));
67
+ top.push(ring(height, i));
68
+ }
69
+ faces.push(orient(bottom, bottom.map(() => [0, -1, 0]), [0, -1, 0]));
70
+ faces.push(orient(top, top.map(() => [0, 1, 0]), [0, 1, 0]));
71
+ return finish(faces);
72
+ };
73
+ var boxSolid = (size) => {
74
+ const hx = size[0] / 2;
75
+ const hz = size[2] / 2;
76
+ const hy = size[1];
77
+ const c = (x, y, z) => [x * hx, y * hy, z * hz];
78
+ const quad = (a, b, cc, d, n) => orient([a, b, cc, d], [n, n, n, n], n);
79
+ return finish([
80
+ quad(c(1, 0, -1), c(1, 1, -1), c(1, 1, 1), c(1, 0, 1), [1, 0, 0]),
81
+ quad(c(-1, 0, -1), c(-1, 1, -1), c(-1, 1, 1), c(-1, 0, 1), [-1, 0, 0]),
82
+ quad(c(-1, 1, -1), c(1, 1, -1), c(1, 1, 1), c(-1, 1, 1), [0, 1, 0]),
83
+ quad(c(-1, 0, -1), c(1, 0, -1), c(1, 0, 1), c(-1, 0, 1), [0, -1, 0]),
84
+ quad(c(-1, 0, 1), c(1, 0, 1), c(1, 1, 1), c(-1, 1, 1), [0, 0, 1]),
85
+ quad(c(-1, 0, -1), c(1, 0, -1), c(1, 1, -1), c(-1, 1, -1), [0, 0, -1])
86
+ ]);
87
+ };
88
+ var polyP = [];
89
+ var polyN = [];
90
+ var cut = [];
91
+ var clipSolid = (solid, u, d, pos, nor) => {
92
+ let count = 0;
93
+ const capacity = Math.floor(pos.length / 3);
94
+ cut.length = 0;
95
+ const put = (px, py, pz, nx, ny, nz) => {
96
+ if (count >= capacity) return;
97
+ pos[count * 3] = px;
98
+ pos[count * 3 + 1] = py;
99
+ pos[count * 3 + 2] = pz;
100
+ nor[count * 3] = nx;
101
+ nor[count * 3 + 1] = ny;
102
+ nor[count * 3 + 2] = nz;
103
+ count++;
104
+ };
105
+ for (const face of solid.faces) {
106
+ const m = face.p.length;
107
+ polyP.length = 0;
108
+ polyN.length = 0;
109
+ for (let i = 0; i < m; i++) {
110
+ const a = face.p[i];
111
+ const b = face.p[(i + 1) % m];
112
+ const na = face.n[i];
113
+ const nb = face.n[(i + 1) % m];
114
+ const da = u[0] * a[0] + u[1] * a[1] + u[2] * a[2] - d;
115
+ const db = u[0] * b[0] + u[1] * b[1] + u[2] * b[2] - d;
116
+ const ina = da <= 0;
117
+ const inb = db <= 0;
118
+ if (ina) {
119
+ polyP.push(a[0], a[1], a[2]);
120
+ polyN.push(na[0], na[1], na[2]);
121
+ if (da > -1e-7) cut.push(a[0], a[1], a[2]);
122
+ }
123
+ if (ina !== inb) {
124
+ const t = da / (da - db);
125
+ const x = a[0] + (b[0] - a[0]) * t;
126
+ const y = a[1] + (b[1] - a[1]) * t;
127
+ const z = a[2] + (b[2] - a[2]) * t;
128
+ polyP.push(x, y, z);
129
+ polyN.push(na[0] + (nb[0] - na[0]) * t, na[1] + (nb[1] - na[1]) * t, na[2] + (nb[2] - na[2]) * t);
130
+ cut.push(x, y, z);
131
+ }
132
+ }
133
+ const n = polyP.length / 3;
134
+ for (let i = 1; i < n - 1; i++) {
135
+ for (const k of [0, i, i + 1]) put(polyP[k * 3], polyP[k * 3 + 1], polyP[k * 3 + 2], polyN[k * 3], polyN[k * 3 + 1], polyN[k * 3 + 2]);
136
+ }
137
+ }
138
+ const pts = [];
139
+ for (let i = 0; i < cut.length; i += 3) {
140
+ let dup = false;
141
+ for (const q of pts) {
142
+ if (Math.abs(q[0] - cut[i]) + Math.abs(q[1] - cut[i + 1]) + Math.abs(q[2] - cut[i + 2]) < 1e-6) {
143
+ dup = true;
144
+ break;
145
+ }
146
+ }
147
+ if (!dup) pts.push([cut[i], cut[i + 1], cut[i + 2]]);
148
+ }
149
+ if (pts.length >= 3) {
150
+ const c = [0, 0, 0];
151
+ for (const q of pts) {
152
+ c[0] += q[0] / pts.length;
153
+ c[1] += q[1] / pts.length;
154
+ c[2] += q[2] / pts.length;
155
+ }
156
+ const e1 = norm(cross(u, Math.abs(u[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]));
157
+ const e2 = cross(u, e1);
158
+ const keyed = pts.map((q) => ({ q, a: Math.atan2(dot(sub(q, c), e2), dot(sub(q, c), e1)) })).sort((p, r) => p.a - r.a);
159
+ for (let i = 0; i < keyed.length; i++) {
160
+ for (const q of [c, keyed[i].q, keyed[(i + 1) % keyed.length].q]) put(q[0], q[1], q[2], u[0], u[1], u[2]);
161
+ }
162
+ }
163
+ return count;
164
+ };
165
+
166
+ // src/liquid-body.ts
167
+ var INSET = 0.985;
168
+ var LiquidBody = class {
169
+ constructor(THREE, containerId, object, offset) {
170
+ __publicField(this, "THREE", THREE);
171
+ __publicField(this, "containerId", containerId);
172
+ __publicField(this, "object", object);
173
+ __publicField(this, "group");
174
+ __publicField(this, "mesh", null);
175
+ __publicField(this, "geometry", null);
176
+ __publicField(this, "pos", new Float32Array(0));
177
+ __publicField(this, "nor", new Float32Array(0));
178
+ __publicField(this, "solid", null);
179
+ __publicField(this, "shapeKey", "");
180
+ __publicField(this, "steamAcc", 0);
181
+ __publicField(this, "inverse");
182
+ __publicField(this, "tmpN");
183
+ __publicField(this, "tmpP");
184
+ __publicField(this, "vertices", 0);
185
+ this.group = new THREE.Group();
186
+ this.group.name = "fluid_liquid_body";
187
+ this.group.position.set(offset[0], offset[1], offset[2]);
188
+ this.inverse = new THREE.Matrix4();
189
+ this.tmpN = new THREE.Vector3();
190
+ this.tmpP = new THREE.Vector3();
191
+ object.add(this.group);
192
+ }
193
+ get visible() {
194
+ return !!this.mesh && this.mesh.visible;
195
+ }
196
+ build(state, quality) {
197
+ const shape = state.shape;
198
+ if (!shape) return;
199
+ const key = JSON.stringify(shape) + quality.bodySegments;
200
+ if (key === this.shapeKey) return;
201
+ this.shapeKey = key;
202
+ this.solid = shape.kind === "cylinder" ? cylinderSolid(shape.radius * INSET, shape.height * INSET, quality.bodySegments) : boxSolid([shape.size[0] * INSET, shape.size[1] * INSET, shape.size[2] * INSET]);
203
+ const T = this.THREE;
204
+ this.geometry?.dispose();
205
+ this.pos = new Float32Array(this.solid.maxVertices * 3);
206
+ this.nor = new Float32Array(this.solid.maxVertices * 3);
207
+ this.geometry = new T.BufferGeometry();
208
+ const p = new T.BufferAttribute(this.pos, 3);
209
+ const n = new T.BufferAttribute(this.nor, 3);
210
+ p.setUsage(T.DynamicDrawUsage);
211
+ n.setUsage(T.DynamicDrawUsage);
212
+ this.geometry.setAttribute("position", p);
213
+ this.geometry.setAttribute("normal", n);
214
+ if (!this.mesh) {
215
+ this.mesh = new T.Mesh(this.geometry);
216
+ this.mesh.name = "fluid_liquid";
217
+ this.mesh.frustumCulled = false;
218
+ this.mesh.renderOrder = 3;
219
+ this.group.add(this.mesh);
220
+ } else this.mesh.geometry = this.geometry;
221
+ }
222
+ update(dt, world, materials, quality, particles) {
223
+ const state = world.container(this.containerId);
224
+ const def = state?.liquid ? world.liquids.get(state.liquid) : void 0;
225
+ if (!state || !state.shape || !def || state.liters <= 1e-6) {
226
+ if (this.mesh) this.mesh.visible = false;
227
+ this.vertices = 0;
228
+ return;
229
+ }
230
+ this.build(state, quality);
231
+ if (!this.mesh || !this.solid) return;
232
+ this.mesh.material = materials.liquidBody(def);
233
+ this.object.updateWorldMatrix(true, false);
234
+ this.group.updateWorldMatrix(true, false);
235
+ this.inverse.copy(this.group.matrixWorld).invert();
236
+ this.tmpN.set(state.surfaceNormal[0], state.surfaceNormal[1], state.surfaceNormal[2]).transformDirection(this.inverse);
237
+ this.tmpP.set(state.surfacePoint[0], state.surfacePoint[1], state.surfacePoint[2]).applyMatrix4(this.inverse);
238
+ const u = [this.tmpN.x, this.tmpN.y, this.tmpN.z];
239
+ const d = u[0] * this.tmpP.x + u[1] * this.tmpP.y + u[2] * this.tmpP.z;
240
+ this.vertices = clipSolid(this.solid, u, d, this.pos, this.nor);
241
+ const geometry = this.geometry;
242
+ geometry.setDrawRange(0, this.vertices);
243
+ geometry.getAttribute("position").needsUpdate = true;
244
+ geometry.getAttribute("normal").needsUpdate = true;
245
+ this.mesh.visible = this.vertices > 0;
246
+ if (state.steaming && this.vertices > 0) {
247
+ this.steamAcc += dt * quality.steamRate;
248
+ while (this.steamAcc >= 1) {
249
+ this.steamAcc -= 1;
250
+ const o = this.tmpP.set(state.surfacePoint[0], state.surfacePoint[1], state.surfacePoint[2]);
251
+ particles.steam([o.x, o.y + 0.01, o.z]);
252
+ }
253
+ } else this.steamAcc = 0;
254
+ }
255
+ get triangles() {
256
+ return this.visible ? Math.floor(this.vertices / 3) : 0;
257
+ }
258
+ dispose() {
259
+ this.object.remove(this.group);
260
+ this.geometry?.dispose();
261
+ this.geometry = null;
262
+ this.mesh = null;
263
+ }
264
+ };
265
+
266
+ // src/util.ts
267
+ var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
268
+ var Rng = class {
269
+ constructor(seed) {
270
+ __publicField(this, "state");
271
+ this.state = seed >>> 0 || 1;
272
+ }
273
+ next() {
274
+ this.state = this.state + 1831565813 >>> 0;
275
+ let t = this.state;
276
+ t = Math.imul(t ^ t >>> 15, t | 1);
277
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
278
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
279
+ }
280
+ };
281
+ var hashPoint = (x, y, z) => {
282
+ let h = 2166136261;
283
+ for (const v of [x, y, z]) h = Math.imul(h ^ Math.round(v * 1e3), 16777619);
284
+ return h >>> 0;
285
+ };
286
+
287
+ // src/materials.ts
288
+ var stainColorOf = (def) => {
289
+ if (def.stainColor !== void 0) return def.stainColor;
290
+ const r = (def.color >> 16 & 255) * 0.65;
291
+ const g = (def.color >> 8 & 255) * 0.65;
292
+ const b = (def.color & 255) * 0.65;
293
+ return Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
294
+ };
295
+ var MaterialCache = class {
296
+ constructor(THREE, envMap) {
297
+ __publicField(this, "THREE", THREE);
298
+ __publicField(this, "envMap", envMap);
299
+ __publicField(this, "body", /* @__PURE__ */ new Map());
300
+ __publicField(this, "stream", /* @__PURE__ */ new Map());
301
+ __publicField(this, "film", /* @__PURE__ */ new Map());
302
+ __publicField(this, "wet", /* @__PURE__ */ new Map());
303
+ }
304
+ make(params) {
305
+ const m = new this.THREE.MeshStandardMaterial({ metalness: 0, ...params });
306
+ if (this.envMap) m.envMap = this.envMap;
307
+ return m;
308
+ }
309
+ /** The volume of liquid inside a container. */
310
+ liquidBody(def) {
311
+ let m = this.body.get(def.id);
312
+ if (!m) {
313
+ m = this.make({
314
+ color: def.color,
315
+ opacity: def.opacity,
316
+ transparent: def.opacity < 0.999,
317
+ roughness: clamp(1 - def.gloss * 0.9, 0.04, 1),
318
+ envMapIntensity: 0.5 + def.gloss * 0.9,
319
+ depthWrite: def.opacity >= 0.999
320
+ });
321
+ this.body.set(def.id, m);
322
+ }
323
+ return m;
324
+ }
325
+ /** A jet or a drop in the air. */
326
+ liquidStream(def) {
327
+ let m = this.stream.get(def.id);
328
+ if (!m) {
329
+ m = this.make({
330
+ color: def.color,
331
+ opacity: clamp(def.opacity + 0.1, 0.35, 1),
332
+ transparent: true,
333
+ roughness: clamp(1 - def.gloss * 0.95, 0.03, 1),
334
+ envMapIntensity: 0.8 + def.gloss,
335
+ side: this.THREE.DoubleSide,
336
+ depthWrite: false
337
+ });
338
+ this.stream.set(def.id, m);
339
+ }
340
+ return m;
341
+ }
342
+ /** The thin outer film of a stain (and the permanent mark): tinted by vertex colour, soft rim by vertex alpha, mostly matte. */
343
+ stainFilm(def) {
344
+ let m = this.film.get(def.id);
345
+ if (!m) {
346
+ m = this.make({
347
+ color: 16777215,
348
+ vertexColors: true,
349
+ transparent: true,
350
+ roughness: clamp(0.95 - def.gloss * 0.45, 0.3, 1),
351
+ envMapIntensity: 0.4 + def.gloss * 0.4,
352
+ depthWrite: false,
353
+ polygonOffset: true,
354
+ polygonOffsetFactor: -2,
355
+ polygonOffsetUnits: -2
356
+ });
357
+ this.film.set(def.id, m);
358
+ }
359
+ return m;
360
+ }
361
+ /** The glossy wet part of a stain, which shrinks while it dries. */
362
+ stainWet(def) {
363
+ let m = this.wet.get(def.id);
364
+ if (!m) {
365
+ m = this.make({
366
+ color: 16777215,
367
+ vertexColors: true,
368
+ transparent: true,
369
+ roughness: clamp(1 - def.gloss, 0.02, 0.7),
370
+ envMapIntensity: 0.9 + def.gloss * 1.4,
371
+ depthWrite: false,
372
+ polygonOffset: true,
373
+ polygonOffsetFactor: -3,
374
+ polygonOffsetUnits: -3
375
+ });
376
+ this.wet.set(def.id, m);
377
+ }
378
+ return m;
379
+ }
380
+ dispose() {
381
+ for (const map of [this.body, this.stream, this.film, this.wet]) {
382
+ for (const m of map.values()) m.dispose();
383
+ map.clear();
384
+ }
385
+ }
386
+ };
387
+
388
+ // src/particles.ts
389
+ var VERTEX = (
390
+ /* glsl */
391
+ `
392
+ attribute float aSize;
393
+ attribute vec4 aColor;
394
+ attribute float aSoft;
395
+ uniform float uScale;
396
+ varying vec4 vColor;
397
+ varying float vSoft;
398
+ void main() {
399
+ vColor = aColor;
400
+ vSoft = aSoft;
401
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
402
+ gl_PointSize = aSize * uScale / max(0.1, -mv.z);
403
+ gl_Position = projectionMatrix * mv;
404
+ }
405
+ `
406
+ );
407
+ var FRAGMENT = (
408
+ /* glsl */
409
+ `
410
+ varying vec4 vColor;
411
+ varying float vSoft;
412
+ void main() {
413
+ float d = length(gl_PointCoord - vec2(0.5)) * 2.0;
414
+ if (d > 1.0 || vColor.a <= 0.001) discard;
415
+ float edge = mix(smoothstep(1.0, 0.8, d), 1.0 - d, vSoft);
416
+ gl_FragColor = vec4(vColor.rgb, vColor.a * edge);
417
+ }
418
+ `
419
+ );
420
+ var ParticlePool = class {
421
+ constructor(THREE, capacity, pointScale) {
422
+ __publicField(this, "THREE", THREE);
423
+ __publicField(this, "points");
424
+ __publicField(this, "geometry");
425
+ __publicField(this, "material");
426
+ __publicField(this, "capacity");
427
+ __publicField(this, "next", 0);
428
+ __publicField(this, "pos");
429
+ __publicField(this, "size");
430
+ __publicField(this, "color");
431
+ __publicField(this, "soft");
432
+ __publicField(this, "vel");
433
+ __publicField(this, "life");
434
+ __publicField(this, "maxLife");
435
+ __publicField(this, "size0");
436
+ __publicField(this, "size1");
437
+ __publicField(this, "alpha");
438
+ __publicField(this, "floorY");
439
+ __publicField(this, "kind");
440
+ __publicField(this, "rgb");
441
+ __publicField(this, "rng", new Rng(1234567));
442
+ __publicField(this, "alive", 0);
443
+ this.capacity = capacity;
444
+ this.pos = new Float32Array(capacity * 3);
445
+ this.size = new Float32Array(capacity);
446
+ this.color = new Float32Array(capacity * 4);
447
+ this.soft = new Float32Array(capacity);
448
+ this.vel = new Float32Array(capacity * 3);
449
+ this.life = new Float32Array(capacity);
450
+ this.maxLife = new Float32Array(capacity);
451
+ this.size0 = new Float32Array(capacity);
452
+ this.size1 = new Float32Array(capacity);
453
+ this.alpha = new Float32Array(capacity);
454
+ this.floorY = new Float32Array(capacity);
455
+ this.kind = new Uint8Array(capacity);
456
+ this.rgb = new Float32Array(capacity * 3);
457
+ this.life.fill(0);
458
+ const T = THREE;
459
+ this.geometry = new T.BufferGeometry();
460
+ const attr = (array, n) => {
461
+ const a = new T.BufferAttribute(array, n);
462
+ a.setUsage(T.DynamicDrawUsage);
463
+ return a;
464
+ };
465
+ this.geometry.setAttribute("position", attr(this.pos, 3));
466
+ this.geometry.setAttribute("aSize", attr(this.size, 1));
467
+ this.geometry.setAttribute("aColor", attr(this.color, 4));
468
+ this.geometry.setAttribute("aSoft", attr(this.soft, 1));
469
+ this.material = new T.ShaderMaterial({
470
+ vertexShader: VERTEX,
471
+ fragmentShader: FRAGMENT,
472
+ uniforms: { uScale: { value: pointScale } },
473
+ transparent: true,
474
+ depthWrite: false
475
+ });
476
+ this.points = new T.Points(this.geometry, this.material);
477
+ this.points.frustumCulled = false;
478
+ this.points.renderOrder = 4;
479
+ this.points.name = "fluid_particles";
480
+ }
481
+ get count() {
482
+ return this.alive;
483
+ }
484
+ slot() {
485
+ const i = this.next;
486
+ this.next = (this.next + 1) % this.capacity;
487
+ return i;
488
+ }
489
+ setColor(i, hex) {
490
+ this.rgb[i * 3] = (hex >> 16 & 255) / 255;
491
+ this.rgb[i * 3 + 1] = (hex >> 8 & 255) / 255;
492
+ this.rgb[i * 3 + 2] = (hex & 255) / 255;
493
+ }
494
+ /** Droplets thrown up by an impact; `floorY` is the height they stop at. */
495
+ splash(point, intensity, hex, opacity, count) {
496
+ const rng = new Rng(hashPoint(point[0], point[1], point[2]) ^ this.next);
497
+ const n = Math.max(2, Math.round(count * (0.35 + 0.65 * clamp(intensity, 0, 1))));
498
+ for (let k = 0; k < n; k++) {
499
+ const i = this.slot();
500
+ const a = rng.next() * Math.PI * 2;
501
+ const h = (0.25 + rng.next() * 0.9) * (0.4 + intensity);
502
+ const up = (0.7 + rng.next() * 1.5) * (0.5 + intensity);
503
+ this.pos.set([point[0], point[1] + 0.01, point[2]], i * 3);
504
+ this.vel.set([Math.cos(a) * h, up, Math.sin(a) * h], i * 3);
505
+ this.life[i] = this.maxLife[i] = 0.45 + rng.next() * 0.45;
506
+ this.size0[i] = 0.012 + rng.next() * 0.014;
507
+ this.size1[i] = this.size0[i] * 0.7;
508
+ this.alpha[i] = clamp(opacity + 0.2, 0.35, 1);
509
+ this.soft[i] = 0;
510
+ this.kind[i] = 0;
511
+ this.floorY[i] = point[1];
512
+ this.setColor(i, hex);
513
+ }
514
+ }
515
+ /** One puff of steam rising from `point`. */
516
+ steam(point) {
517
+ const i = this.slot();
518
+ const r = this.rng;
519
+ this.pos.set([point[0] + (r.next() - 0.5) * 0.04, point[1], point[2] + (r.next() - 0.5) * 0.04], i * 3);
520
+ this.vel.set([(r.next() - 0.5) * 0.06, 0.22 + r.next() * 0.12, (r.next() - 0.5) * 0.06], i * 3);
521
+ this.life[i] = this.maxLife[i] = 1.2 + r.next() * 0.6;
522
+ this.size0[i] = 0.03;
523
+ this.size1[i] = 0.1;
524
+ this.alpha[i] = 0.24;
525
+ this.soft[i] = 1;
526
+ this.kind[i] = 1;
527
+ this.floorY[i] = -1e9;
528
+ this.setColor(i, 16054527);
529
+ }
530
+ update(dt, gravity) {
531
+ let alive = 0;
532
+ for (let i = 0; i < this.capacity; i++) {
533
+ if (this.life[i] <= 0) {
534
+ this.size[i] = 0;
535
+ this.color[i * 4 + 3] = 0;
536
+ continue;
537
+ }
538
+ this.life[i] -= dt;
539
+ const k = 1 - this.life[i] / this.maxLife[i];
540
+ if (this.life[i] <= 0) {
541
+ this.size[i] = 0;
542
+ this.color[i * 4 + 3] = 0;
543
+ continue;
544
+ }
545
+ alive++;
546
+ if (this.kind[i] === 0) {
547
+ this.vel[i * 3 + 1] -= gravity * dt;
548
+ } else {
549
+ this.vel[i * 3] += Math.sin((k + i) * 7) * 0.02 * dt;
550
+ }
551
+ this.pos[i * 3] += this.vel[i * 3] * dt;
552
+ this.pos[i * 3 + 1] += this.vel[i * 3 + 1] * dt;
553
+ this.pos[i * 3 + 2] += this.vel[i * 3 + 2] * dt;
554
+ if (this.kind[i] === 0 && this.pos[i * 3 + 1] < this.floorY[i]) {
555
+ this.life[i] = 0;
556
+ alive--;
557
+ this.size[i] = 0;
558
+ this.color[i * 4 + 3] = 0;
559
+ continue;
560
+ }
561
+ this.size[i] = this.size0[i] + (this.size1[i] - this.size0[i]) * k;
562
+ const fade = this.kind[i] === 1 ? Math.min(1, k * 4) * (1 - k) : 1 - k * k;
563
+ this.color[i * 4] = this.rgb[i * 3];
564
+ this.color[i * 4 + 1] = this.rgb[i * 3 + 1];
565
+ this.color[i * 4 + 2] = this.rgb[i * 3 + 2];
566
+ this.color[i * 4 + 3] = this.alpha[i] * fade;
567
+ }
568
+ this.alive = alive;
569
+ for (const name of ["position", "aSize", "aColor", "aSoft"]) this.geometry.getAttribute(name).needsUpdate = true;
570
+ }
571
+ clear() {
572
+ this.life.fill(0);
573
+ this.alive = 0;
574
+ this.update(0, 0);
575
+ }
576
+ dispose() {
577
+ this.geometry.dispose();
578
+ this.material.dispose();
579
+ }
580
+ };
581
+
582
+ // src/quality.ts
583
+ var PROFILES = {
584
+ low: { bodySegments: 10, streamSamples: 8, streamSides: 4, thinDrops: 4, stainSubdiv: 1, maxStains: 24, maxStreams: 12, maxParticles: 96, steamRate: 4, splashDroplets: 5 },
585
+ medium: { bodySegments: 14, streamSamples: 12, streamSides: 5, thinDrops: 6, stainSubdiv: 2, maxStains: 48, maxStreams: 24, maxParticles: 256, steamRate: 8, splashDroplets: 9 },
586
+ high: { bodySegments: 20, streamSamples: 20, streamSides: 8, thinDrops: 8, stainSubdiv: 4, maxStains: 96, maxStreams: 48, maxParticles: 512, steamRate: 14, splashDroplets: 14 }
587
+ };
588
+ var profileOf = (q) => PROFILES[q ?? "medium"] ?? PROFILES.medium;
589
+
590
+ // ../game-fluids/dist/index.js
591
+ var ballistic = (origin, velocity, gravity, t) => [
592
+ origin[0] + velocity[0] * t,
593
+ origin[1] + velocity[1] * t - 0.5 * gravity * t * t,
594
+ origin[2] + velocity[2] * t
595
+ ];
596
+ var sampleStream = (stream, gravity, t) => ballistic(stream.origin, stream.velocity, gravity, t);
597
+ var SAMPLES = 16;
598
+ var COS = [];
599
+ var SIN = [];
600
+ for (let i = 0; i < SAMPLES; i++) {
601
+ COS.push(Math.cos(i / SAMPLES * Math.PI * 2));
602
+ SIN.push(Math.sin(i / SAMPLES * Math.PI * 2));
603
+ }
604
+
605
+ // src/stains.ts
606
+ var smoothOutline = (front, subdiv, out) => {
607
+ const n = front.length;
608
+ const res = out ?? [];
609
+ res.length = n * subdiv;
610
+ for (let i = 0; i < n; i++) {
611
+ const p0 = front[(i + n - 1) % n];
612
+ const p1 = front[i];
613
+ const p2 = front[(i + 1) % n];
614
+ const p3 = front[(i + 2) % n];
615
+ for (let j = 0; j < subdiv; j++) {
616
+ const t = j / subdiv;
617
+ const r = 0.5 * (2 * p1 + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + (-p0 + 3 * p1 - 3 * p2 + p3) * t * t * t);
618
+ res[i * subdiv + j] = Math.max(4e-3, r);
619
+ }
620
+ }
621
+ return res;
622
+ };
623
+ var StainVisual = class {
624
+ constructor(THREE, profile, slot) {
625
+ __publicField(this, "THREE", THREE);
626
+ __publicField(this, "slot", slot);
627
+ __publicField(this, "group");
628
+ __publicField(this, "outer");
629
+ __publicField(this, "inner");
630
+ __publicField(this, "ring");
631
+ __publicField(this, "radii", []);
632
+ __publicField(this, "tint");
633
+ __publicField(this, "stainId", "");
634
+ __publicField(this, "signature", Number.NaN);
635
+ __publicField(this, "triangles", 0);
636
+ this.ring = SAMPLES * profile.stainSubdiv;
637
+ this.group = new THREE.Group();
638
+ this.group.name = "fluid_stain";
639
+ this.tint = new THREE.Color();
640
+ this.outer = this.makeFan("fluid_stain_film", 1);
641
+ this.inner = this.makeFan("fluid_stain_wet", 2);
642
+ this.group.add(this.outer.mesh, this.inner.mesh);
643
+ this.group.visible = false;
644
+ }
645
+ makeFan(name, order) {
646
+ const T = this.THREE;
647
+ const count = 1 + this.ring;
648
+ const position = new Float32Array(count * 3);
649
+ const color = new Float32Array(count * 4);
650
+ const normal = new Float32Array(count * 3);
651
+ const geometry = new T.BufferGeometry();
652
+ const p = new T.BufferAttribute(position, 3);
653
+ const c = new T.BufferAttribute(color, 4);
654
+ p.setUsage(T.DynamicDrawUsage);
655
+ c.setUsage(T.DynamicDrawUsage);
656
+ geometry.setAttribute("position", p);
657
+ geometry.setAttribute("normal", new T.BufferAttribute(normal, 3));
658
+ geometry.setAttribute("color", c);
659
+ const index = [];
660
+ for (let i = 0; i < this.ring; i++) index.push(0, 1 + i, 1 + (i + 1) % this.ring);
661
+ geometry.setIndex(index);
662
+ const mesh = new T.Mesh(geometry);
663
+ mesh.name = name;
664
+ mesh.frustumCulled = false;
665
+ mesh.renderOrder = order;
666
+ return { mesh, position, color };
667
+ }
668
+ assign(def, materials) {
669
+ this.outer.mesh.material = materials.stainFilm(def);
670
+ this.inner.mesh.material = materials.stainWet(def);
671
+ }
672
+ hide() {
673
+ this.group.visible = false;
674
+ this.stainId = "";
675
+ this.signature = Number.NaN;
676
+ this.triangles = 0;
677
+ }
678
+ /** Rebuild the vertices from the stain state (skipped when nothing changed). */
679
+ update(stain, def, profile) {
680
+ const sig = stain.center[0] * 3 + stain.center[1] * 5 + stain.center[2] * 7 + stain.front.reduce((s, v, i) => s + v * (i + 1), 0) * 11 + stain.wetness * 13 + stain.wetRadius * 17 + stain.soaked / (stain.soaked + stain.liters + 1e-6) * 19;
681
+ if (sig === this.signature && this.group.visible) return;
682
+ this.signature = sig;
683
+ const n = stain.normal ?? [0, 1, 0];
684
+ const t1 = stain.tangent ?? [1, 0, 0];
685
+ const t2 = [n[1] * t1[2] - n[2] * t1[1], n[2] * t1[0] - n[0] * t1[2], n[0] * t1[1] - n[1] * t1[0]];
686
+ smoothOutline(stain.front, profile.stainSubdiv, this.radii);
687
+ const ring = this.ring;
688
+ const lift = 15e-4 + 4e-4 * (this.slot % 6);
689
+ const mean = stain.front.reduce((s, v) => s + v, 0) / stain.front.length;
690
+ const wet = clamp(stain.wetness, 0, 1);
691
+ const soakRatio = stain.soaked / (stain.soaked + stain.liters + 1e-6);
692
+ this.tint.setHex(stainColorOf(def));
693
+ const dark = 1 - 0.3 * wet - 0.2 * soakRatio;
694
+ const markAlpha = def.leavesMark ? 0.34 : 0.1;
695
+ const aMid = (markAlpha + (0.62 - markAlpha) * wet) * (0.75 + 0.25 * Math.min(1, stain.liters * 20 + soakRatio));
696
+ this.writeFan(this.outer, stain.center, n, t1, t2, lift, 1, this.tint.r * dark, this.tint.g * dark, this.tint.b * dark, aMid, aMid * 0.45);
697
+ this.outer.mesh.visible = true;
698
+ const wr = clamp(stain.wetRadius, 0, 1);
699
+ const showInner = wr > 0.05 && wet > 0.03;
700
+ this.inner.mesh.visible = showInner;
701
+ if (showInner) {
702
+ this.tint.setHex(def.color);
703
+ const shift = stain.flow ? -mean * 0.1 * (1 - wr) : 0;
704
+ const centre = stain.flow ? [stain.center[0] + stain.flow[0] * shift, stain.center[1] + stain.flow[1] * shift, stain.center[2] + stain.flow[2] * shift] : stain.center;
705
+ const a = clamp(def.opacity, 0.2, 0.85) * wet * 0.8;
706
+ this.writeFan(this.inner, centre, n, t1, t2, lift + 7e-4, 0.9 * wr, this.tint.r, this.tint.g, this.tint.b, a, a * 0.5);
707
+ }
708
+ this.triangles = ring * (showInner ? 2 : 1);
709
+ this.group.visible = true;
710
+ }
711
+ writeFan(fan, center, n, t1, t2, lift, scale, r, g, b, aCentre, aRim) {
712
+ const ring = this.ring;
713
+ const px = center[0] + n[0] * lift;
714
+ const py = center[1] + n[1] * lift;
715
+ const pz = center[2] + n[2] * lift;
716
+ fan.position[0] = px;
717
+ fan.position[1] = py;
718
+ fan.position[2] = pz;
719
+ fan.color[0] = r * 0.85;
720
+ fan.color[1] = g * 0.85;
721
+ fan.color[2] = b * 0.85;
722
+ fan.color[3] = aCentre;
723
+ const normalAttr = fan.mesh.geometry.getAttribute("normal");
724
+ for (let i = 0; i <= ring; i++) {
725
+ normalAttr.setXYZ(i, n[0], n[1], n[2]);
726
+ }
727
+ normalAttr.needsUpdate = true;
728
+ for (let i = 0; i < ring; i++) {
729
+ const a = i / ring * Math.PI * 2;
730
+ const c = Math.cos(a) * this.radii[i] * scale;
731
+ const s = Math.sin(a) * this.radii[i] * scale;
732
+ const o = (1 + i) * 3;
733
+ fan.position[o] = px + t1[0] * c + t2[0] * s;
734
+ fan.position[o + 1] = py + t1[1] * c + t2[1] * s;
735
+ fan.position[o + 2] = pz + t1[2] * c + t2[2] * s;
736
+ const k = (1 + i) * 4;
737
+ fan.color[k] = r;
738
+ fan.color[k + 1] = g;
739
+ fan.color[k + 2] = b;
740
+ fan.color[k + 3] = aRim;
741
+ }
742
+ const geometry = fan.mesh.geometry;
743
+ geometry.getAttribute("position").needsUpdate = true;
744
+ geometry.getAttribute("color").needsUpdate = true;
745
+ }
746
+ dispose() {
747
+ for (const fan of [this.outer, this.inner]) fan.mesh.geometry.dispose();
748
+ }
749
+ };
750
+
751
+ // src/streams.ts
752
+ var GHOST_TIME = 0.4;
753
+ var StreamVisual = class {
754
+ constructor(THREE, profile, dropGeometry) {
755
+ __publicField(this, "THREE", THREE);
756
+ __publicField(this, "tube");
757
+ __publicField(this, "drops");
758
+ __publicField(this, "positions");
759
+ __publicField(this, "normals");
760
+ __publicField(this, "samples");
761
+ __publicField(this, "sides");
762
+ __publicField(this, "matrix");
763
+ __publicField(this, "maxDrops");
764
+ __publicField(this, "id", "");
765
+ __publicField(this, "age", 0);
766
+ __publicField(this, "ghostAge", -1);
767
+ /** Last known state, kept while the stream breaks into drops. */
768
+ __publicField(this, "state", null);
769
+ __publicField(this, "triangles", 0);
770
+ const T = THREE;
771
+ this.samples = profile.streamSamples;
772
+ this.sides = profile.streamSides;
773
+ this.maxDrops = Math.max(profile.thinDrops, 2) + 2;
774
+ this.positions = new Float32Array(this.samples * this.sides * 3);
775
+ this.normals = new Float32Array(this.samples * this.sides * 3);
776
+ const geometry = new T.BufferGeometry();
777
+ const p = new T.BufferAttribute(this.positions, 3);
778
+ const n = new T.BufferAttribute(this.normals, 3);
779
+ p.setUsage(T.DynamicDrawUsage);
780
+ n.setUsage(T.DynamicDrawUsage);
781
+ geometry.setAttribute("position", p);
782
+ geometry.setAttribute("normal", n);
783
+ const index = [];
784
+ for (let i = 0; i < this.samples - 1; i++) {
785
+ for (let j = 0; j < this.sides; j++) {
786
+ const a = i * this.sides + j;
787
+ const b = i * this.sides + (j + 1) % this.sides;
788
+ const c = (i + 1) * this.sides + j;
789
+ const d = (i + 1) * this.sides + (j + 1) % this.sides;
790
+ index.push(a, c, b, b, c, d);
791
+ }
792
+ }
793
+ geometry.setIndex(index);
794
+ this.tube = new T.Mesh(geometry);
795
+ this.tube.name = "fluid_stream";
796
+ this.tube.frustumCulled = false;
797
+ this.tube.renderOrder = 2;
798
+ this.drops = new T.InstancedMesh(dropGeometry, void 0, this.maxDrops);
799
+ this.drops.name = "fluid_stream_drops";
800
+ this.drops.frustumCulled = false;
801
+ this.drops.renderOrder = 2;
802
+ this.drops.instanceMatrix.setUsage(T.DynamicDrawUsage);
803
+ this.drops.count = 0;
804
+ this.matrix = new T.Matrix4();
805
+ }
806
+ setMaterial(material) {
807
+ this.tube.material = material;
808
+ this.drops.material = material;
809
+ }
810
+ hide() {
811
+ this.tube.visible = false;
812
+ this.drops.visible = false;
813
+ this.drops.count = 0;
814
+ this.triangles = 0;
815
+ }
816
+ dropAt(slot, x, y, z, radius) {
817
+ this.matrix.makeScale(radius, radius, radius).setPosition(x, y, z);
818
+ this.drops.setMatrixAt(slot, this.matrix);
819
+ }
820
+ /**
821
+ * Draw the stream. `state` is the world's stream while it lives, or the last known one while ghost (`ghostAge >= 0`).
822
+ */
823
+ update(state, gravity, dt, profile, time) {
824
+ this.state = state;
825
+ this.age += dt;
826
+ if (this.ghostAge >= 0) this.ghostAge += dt;
827
+ const flight = Math.max(0.02, state.flightTime ?? 1);
828
+ const [vx, vy, vz] = state.velocity;
829
+ const speed0 = Math.max(0.3, Math.hypot(vx, vy, vz));
830
+ const q = Math.max(state.rate, 0) / 1e3;
831
+ const r0 = clamp(Math.sqrt(q / (Math.PI * speed0)), 15e-4, 0.03);
832
+ const tB = Math.min(flight, this.age);
833
+ const tA = this.ghostAge >= 0 ? Math.min(flight, this.ghostAge) : 0;
834
+ let drops = 0;
835
+ const thin = state.thin && this.ghostAge < 0;
836
+ if (thin) {
837
+ this.tube.visible = false;
838
+ this.triangles = 0;
839
+ const n = profile.thinDrops;
840
+ const phase = time * 1.3;
841
+ for (let k = 0; k < n && drops < this.maxDrops; k++) {
842
+ const tau = (phase + k / n) % 1 * flight;
843
+ if (tau > tB) continue;
844
+ const p = sampleStream(state, gravity, tau);
845
+ const grow = Math.min(1, tau / (0.12 * flight + 0.02));
846
+ this.dropAt(drops++, p[0], p[1], p[2], Math.max(2e-3, r0 * 1.8) * (0.4 + 0.6 * grow));
847
+ }
848
+ } else if (tB - tA > 0.015) {
849
+ this.writeTube(state, gravity, tA, tB, r0, speed0);
850
+ this.tube.visible = true;
851
+ this.triangles = (this.samples - 1) * this.sides * 2;
852
+ if (tB < flight) {
853
+ const p = sampleStream(state, gravity, tB);
854
+ const w = this.radiusAt(state, gravity, tB, r0, speed0);
855
+ this.dropAt(drops++, p[0], p[1], p[2], w * 1.35);
856
+ }
857
+ if (this.ghostAge >= 0) {
858
+ const p = sampleStream(state, gravity, tA);
859
+ const w = this.radiusAt(state, gravity, tA, r0, speed0);
860
+ this.dropAt(drops++, p[0], p[1], p[2], w * 1.4 * (1 - this.ghostAge / GHOST_TIME * 0.4));
861
+ }
862
+ } else {
863
+ this.tube.visible = false;
864
+ this.triangles = 0;
865
+ if (tA < flight) {
866
+ const p = sampleStream(state, gravity, this.ghostAge >= 0 ? tA : tB);
867
+ this.dropAt(drops++, p[0], p[1], p[2], Math.max(3e-3, r0 * 1.3));
868
+ }
869
+ }
870
+ this.drops.count = drops;
871
+ this.drops.visible = drops > 0;
872
+ this.drops.instanceMatrix.needsUpdate = true;
873
+ this.triangles += drops * 24;
874
+ }
875
+ /** The ghost is over once its tail reached the landing. */
876
+ get finished() {
877
+ return this.ghostAge >= 0 && (this.ghostAge >= GHOST_TIME || this.ghostAge >= (this.state?.flightTime ?? 0));
878
+ }
879
+ radiusAt(state, gravity, t, r0, speed0) {
880
+ const vy = state.velocity[1] - gravity * t;
881
+ const speed = Math.max(0.3, Math.hypot(state.velocity[0], vy, state.velocity[2]));
882
+ const fade = this.ghostAge >= 0 ? clamp(1 - this.ghostAge / GHOST_TIME, 0.15, 1) : 1;
883
+ return r0 * Math.sqrt(speed0 / speed) * fade;
884
+ }
885
+ writeTube(state, gravity, tA, tB, r0, speed0) {
886
+ const n = this.samples;
887
+ const sides = this.sides;
888
+ for (let i = 0; i < n; i++) {
889
+ const k = i / (n - 1);
890
+ const t = tA + (tB - tA) * k;
891
+ const p = sampleStream(state, gravity, t);
892
+ const vx = state.velocity[0];
893
+ const vy = state.velocity[1] - gravity * t;
894
+ const vz = state.velocity[2];
895
+ const l = Math.hypot(vx, vy, vz) || 1;
896
+ const tx = vx / l;
897
+ const ty = vy / l;
898
+ const tz = vz / l;
899
+ const ax = Math.abs(tz) < 0.9 ? 0 : 1;
900
+ const az = Math.abs(tz) < 0.9 ? 1 : 0;
901
+ let b1x = ty * az - tz * 0;
902
+ let b1y = tz * ax - tx * az;
903
+ let b1z = tx * 0 - ty * ax;
904
+ const bl = Math.hypot(b1x, b1y, b1z) || 1;
905
+ b1x /= bl;
906
+ b1y /= bl;
907
+ b1z /= bl;
908
+ const b2x = ty * b1z - tz * b1y;
909
+ const b2y = tz * b1x - tx * b1z;
910
+ const b2z = tx * b1y - ty * b1x;
911
+ const end = Math.min(1, 0.35 + Math.min(k, 1 - k) * 8);
912
+ const r = this.radiusAt(state, gravity, t, r0, speed0) * end;
913
+ for (let j = 0; j < sides; j++) {
914
+ const a = j / sides * Math.PI * 2;
915
+ const c = Math.cos(a);
916
+ const s = Math.sin(a);
917
+ const nx = b1x * c + b2x * s;
918
+ const ny = b1y * c + b2y * s;
919
+ const nz = b1z * c + b2z * s;
920
+ const o = (i * sides + j) * 3;
921
+ this.positions[o] = p[0] + nx * r;
922
+ this.positions[o + 1] = p[1] + ny * r;
923
+ this.positions[o + 2] = p[2] + nz * r;
924
+ this.normals[o] = nx;
925
+ this.normals[o + 1] = ny;
926
+ this.normals[o + 2] = nz;
927
+ }
928
+ }
929
+ const g = this.tube.geometry;
930
+ g.getAttribute("position").needsUpdate = true;
931
+ g.getAttribute("normal").needsUpdate = true;
932
+ }
933
+ start(id) {
934
+ this.id = id;
935
+ this.age = 0;
936
+ this.ghostAge = -1;
937
+ this.state = null;
938
+ }
939
+ dispose() {
940
+ this.tube.geometry.dispose();
941
+ this.drops.dispose();
942
+ }
943
+ };
944
+
945
+ // src/renderer.ts
946
+ function createFluidRenderer(THREE, options) {
947
+ const { scene, fluids } = options;
948
+ const quality = options.quality ?? "medium";
949
+ const profile = profileOf(quality);
950
+ const materials = new MaterialCache(THREE, options.envMap ?? null);
951
+ const root = new THREE.Group();
952
+ root.name = "fluid_renderer";
953
+ scene.add(root);
954
+ const particles = new ParticlePool(THREE, profile.maxParticles, options.pointScale ?? 600);
955
+ root.add(particles.points);
956
+ const dropGeometry = new THREE.SphereGeometry(1, 8, 6);
957
+ const bodies = /* @__PURE__ */ new Map();
958
+ const streamsActive = /* @__PURE__ */ new Map();
959
+ const streamsFree = [];
960
+ let streamsCreated = 0;
961
+ const stainsActive = /* @__PURE__ */ new Map();
962
+ const stainsFree = [];
963
+ let stainsCreated = 0;
964
+ let time = 0;
965
+ let disposed = false;
966
+ const offSplash = fluids.on("splash", (e) => {
967
+ if (e.type !== "splash" || disposed) return;
968
+ const def = fluids.liquids.get(e.liquid);
969
+ if (!def) return;
970
+ particles.splash(e.point, e.intensity, def.color, def.opacity, profile.splashDroplets);
971
+ });
972
+ const acquireStream = (id) => {
973
+ let v = streamsFree.pop() ?? null;
974
+ if (!v) {
975
+ if (streamsCreated >= profile.maxStreams) return null;
976
+ streamsCreated++;
977
+ v = new StreamVisual(THREE, profile, dropGeometry);
978
+ root.add(v.tube, v.drops);
979
+ }
980
+ v.start(id);
981
+ return v;
982
+ };
983
+ const releaseStream = (v) => {
984
+ v.hide();
985
+ streamsActive.delete(v.id);
986
+ streamsFree.push(v);
987
+ };
988
+ const syncStreams = (dt) => {
989
+ const live = fluids.streams();
990
+ const seen = /* @__PURE__ */ new Set();
991
+ live.sort((a, b) => b.rate - a.rate);
992
+ for (const s of live) {
993
+ seen.add(s.id);
994
+ let v = streamsActive.get(s.id);
995
+ if (!v) {
996
+ const fresh = acquireStream(s.id);
997
+ if (!fresh) continue;
998
+ v = fresh;
999
+ streamsActive.set(s.id, v);
1000
+ }
1001
+ const def = fluids.liquids.get(s.liquid);
1002
+ if (!def) continue;
1003
+ v.ghostAge = -1;
1004
+ v.setMaterial(materials.liquidStream(def));
1005
+ v.update(s, fluids.gravity, dt, profile, time);
1006
+ }
1007
+ for (const v of [...streamsActive.values()]) {
1008
+ if (seen.has(v.id)) continue;
1009
+ if (v.ghostAge < 0) v.ghostAge = 0;
1010
+ if (!v.state || v.finished) {
1011
+ releaseStream(v);
1012
+ continue;
1013
+ }
1014
+ v.update(v.state, fluids.gravity, dt, profile, time);
1015
+ if (v.finished) releaseStream(v);
1016
+ }
1017
+ };
1018
+ const syncStains = () => {
1019
+ const all = fluids.stains();
1020
+ const wanted = all.length > profile.maxStains ? [...all].sort((a, b) => (a.age ?? 0) - (b.age ?? 0)).slice(0, profile.maxStains) : all;
1021
+ const ids = new Set(wanted.map((s) => s.id));
1022
+ for (const [id, v] of [...stainsActive]) {
1023
+ if (ids.has(id)) continue;
1024
+ v.hide();
1025
+ stainsActive.delete(id);
1026
+ stainsFree.push(v);
1027
+ }
1028
+ for (const s of wanted) {
1029
+ const def = fluids.liquids.get(s.liquid);
1030
+ if (!def) continue;
1031
+ let v = stainsActive.get(s.id);
1032
+ if (!v) {
1033
+ v = stainsFree.pop();
1034
+ if (!v) {
1035
+ if (stainsCreated >= profile.maxStains) continue;
1036
+ v = new StainVisual(THREE, profile, stainsCreated++);
1037
+ root.add(v.group);
1038
+ }
1039
+ v.stainId = s.id;
1040
+ v.signature = Number.NaN;
1041
+ v.assign(def, materials);
1042
+ stainsActive.set(s.id, v);
1043
+ }
1044
+ v.update(s, def, profile);
1045
+ }
1046
+ };
1047
+ const renderer = {
1048
+ bindContainer(containerId, object, opts = {}) {
1049
+ renderer.unbindContainer(containerId);
1050
+ bodies.set(containerId, new LiquidBody(THREE, containerId, object, opts.innerOffset ?? [0, 0, 0]));
1051
+ },
1052
+ unbindContainer(containerId) {
1053
+ const body = bodies.get(containerId);
1054
+ if (!body) return;
1055
+ body.dispose();
1056
+ bodies.delete(containerId);
1057
+ },
1058
+ update(dt) {
1059
+ if (disposed) return;
1060
+ time += dt;
1061
+ for (const body of bodies.values()) body.update(dt, fluids, materials, profile, particles);
1062
+ syncStreams(dt);
1063
+ syncStains();
1064
+ particles.update(dt, fluids.gravity);
1065
+ },
1066
+ clear() {
1067
+ for (const v of [...streamsActive.values()]) releaseStream(v);
1068
+ for (const [id, v] of [...stainsActive]) {
1069
+ v.hide();
1070
+ stainsActive.delete(id);
1071
+ stainsFree.push(v);
1072
+ }
1073
+ particles.clear();
1074
+ },
1075
+ dispose() {
1076
+ if (disposed) return;
1077
+ disposed = true;
1078
+ offSplash();
1079
+ for (const body of bodies.values()) body.dispose();
1080
+ bodies.clear();
1081
+ for (const v of [...streamsActive.values(), ...streamsFree]) v.dispose();
1082
+ for (const v of [...stainsActive.values(), ...stainsFree]) v.dispose();
1083
+ streamsActive.clear();
1084
+ stainsActive.clear();
1085
+ streamsFree.length = 0;
1086
+ stainsFree.length = 0;
1087
+ particles.dispose();
1088
+ dropGeometry.dispose();
1089
+ materials.dispose();
1090
+ scene.remove(root);
1091
+ root.clear();
1092
+ },
1093
+ stats() {
1094
+ let triangles = 0;
1095
+ let containers = 0;
1096
+ for (const body of bodies.values()) {
1097
+ triangles += body.triangles;
1098
+ if (body.visible) containers++;
1099
+ }
1100
+ for (const v of streamsActive.values()) triangles += v.triangles;
1101
+ for (const v of stainsActive.values()) triangles += v.triangles;
1102
+ return {
1103
+ quality,
1104
+ containers,
1105
+ streams: streamsActive.size,
1106
+ stains: stainsActive.size,
1107
+ particles: particles.count,
1108
+ triangles,
1109
+ budget: { stains: profile.maxStains, streams: profile.maxStreams, particles: profile.maxParticles }
1110
+ };
1111
+ }
1112
+ };
1113
+ return renderer;
1114
+ }
1115
+ return __toCommonJS(index_exports);
1116
+ })();
1117
+ //# sourceMappingURL=index.global.js.map