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