@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.
- package/LICENSE +21 -0
- package/README.md +105 -2
- package/dist/index.d.ts +68 -0
- package/dist/index.global.js +1117 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1081 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/clip.ts","../src/liquid-body.ts","../src/util.ts","../src/materials.ts","../src/particles.ts","../src/quality.ts","../../game-fluids/src/data/liquids.json","../../game-fluids/src/liquids.ts","../../game-fluids/src/math.ts","../../game-fluids/src/landing.ts","../../game-fluids/src/locale.ts","../../game-fluids/src/shapes.ts","../../game-fluids/src/stains.ts","../../game-fluids/src/world.ts","../../game-fluids/src/data/item-uses.json","../../game-fluids/src/item-use.ts","../src/stains.ts","../src/streams.ts","../src/renderer.ts"],"sourcesContent":["export { createFluidRenderer } from './renderer'\nexport { smoothOutline } from './stains'\nexport type { BindOptions, FluidQuality, FluidRenderer, FluidRendererOptions, FluidRendererStats, ThreeLib } from './types'\n","/**\n * Convex solids (a cylinder or a box, in container space) clipped by the liquid surface plane `u · p <= d`.\n * This is robust for any tilt (upright, lying on its side, upside down): the result is exactly the part of the container the liquid fills,\n * with the surface cap lying on the plane. Written into preallocated buffers, no per-frame geometry is created.\n */\nexport type V3 = [number, number, number]\n\nexport interface SolidFace {\n p: V3[]\n n: V3[]\n}\n\nexport interface Solid {\n faces: SolidFace[]\n /** Upper bound of the vertices a clip can write (non-indexed triangles). */\n maxVertices: number\n}\n\nconst sub = (a: V3, b: V3): V3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]\nconst cross = (a: V3, b: V3): V3 => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]\nconst dot = (a: V3, b: V3): number => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]\nconst norm = (a: V3): V3 => {\n const l = Math.hypot(a[0], a[1], a[2]) || 1\n return [a[0] / l, a[1] / l, a[2] / l]\n}\n\n/** Reverse the winding when the face does not look outward. */\nconst orient = (p: V3[], n: V3[], outward: V3): SolidFace => {\n if (dot(cross(sub(p[1], p[0]), sub(p[2], p[0])), outward) < 0) return { p: [...p].reverse(), n: [...n].reverse() }\n return { p, n }\n}\n\nconst finish = (faces: SolidFace[]): Solid => {\n let maxVertices = 0\n for (const f of faces) maxVertices += (f.p.length * 2 + 2) * 3\n // The surface cap: at most one point per face edge cut.\n maxVertices += faces.length * 3 * 2 + 64\n return { faces, maxVertices }\n}\n\nexport const cylinderSolid = (radius: number, height: number, segments: number): Solid => {\n const faces: SolidFace[] = []\n const ring = (y: number, i: number): V3 => {\n const a = (i / segments) * Math.PI * 2\n return [Math.cos(a) * radius, y, Math.sin(a) * radius]\n }\n const radial = (i: number): V3 => {\n const a = (i / segments) * Math.PI * 2\n return [Math.cos(a), 0, Math.sin(a)]\n }\n for (let i = 0; i < segments; i++) {\n const j = (i + 1) % segments\n const mid = norm([radial(i)[0] + radial(j)[0], 0, radial(i)[2] + radial(j)[2]])\n faces.push(orient([ring(0, i), ring(0, j), ring(height, j), ring(height, i)], [radial(i), radial(j), radial(j), radial(i)], mid))\n }\n const bottom: V3[] = []\n const top: V3[] = []\n for (let i = 0; i < segments; i++) {\n bottom.push(ring(0, i))\n top.push(ring(height, i))\n }\n faces.push(orient(bottom, bottom.map((): V3 => [0, -1, 0]), [0, -1, 0]))\n faces.push(orient(top, top.map((): V3 => [0, 1, 0]), [0, 1, 0]))\n return finish(faces)\n}\n\nexport const boxSolid = (size: V3): Solid => {\n const hx = size[0] / 2\n const hz = size[2] / 2\n const hy = size[1]\n const c = (x: number, y: number, z: number): V3 => [x * hx, y * hy, z * hz]\n const quad = (a: V3, b: V3, cc: V3, d: V3, n: V3): SolidFace => orient([a, b, cc, d], [n, n, n, n], n)\n return finish([\n quad(c(1, 0, -1), c(1, 1, -1), c(1, 1, 1), c(1, 0, 1), [1, 0, 0]),\n quad(c(-1, 0, -1), c(-1, 1, -1), c(-1, 1, 1), c(-1, 0, 1), [-1, 0, 0]),\n quad(c(-1, 1, -1), c(1, 1, -1), c(1, 1, 1), c(-1, 1, 1), [0, 1, 0]),\n quad(c(-1, 0, -1), c(1, 0, -1), c(1, 0, 1), c(-1, 0, 1), [0, -1, 0]),\n quad(c(-1, 0, 1), c(1, 0, 1), c(1, 1, 1), c(-1, 1, 1), [0, 0, 1]),\n quad(c(-1, 0, -1), c(1, 0, -1), c(1, 1, -1), c(-1, 1, -1), [0, 0, -1]),\n ])\n}\n\nconst polyP: number[] = []\nconst polyN: number[] = []\nconst cut: number[] = []\n\n/**\n * Writes the triangles of `solid ∩ { u · p <= d }` into `pos` / `nor` (non-indexed, 3 floats per vertex) and returns the vertex count.\n * The cap on the plane has normal `u`.\n */\nexport const clipSolid = (solid: Solid, u: V3, d: number, pos: Float32Array, nor: Float32Array): number => {\n let count = 0\n const capacity = Math.floor(pos.length / 3)\n cut.length = 0\n const put = (px: number, py: number, pz: number, nx: number, ny: number, nz: number): void => {\n if (count >= capacity) return\n pos[count * 3] = px\n pos[count * 3 + 1] = py\n pos[count * 3 + 2] = pz\n nor[count * 3] = nx\n nor[count * 3 + 1] = ny\n nor[count * 3 + 2] = nz\n count++\n }\n\n for (const face of solid.faces) {\n const m = face.p.length\n polyP.length = 0\n polyN.length = 0\n for (let i = 0; i < m; i++) {\n const a = face.p[i]\n const b = face.p[(i + 1) % m]\n const na = face.n[i]\n const nb = face.n[(i + 1) % m]\n const da = u[0] * a[0] + u[1] * a[1] + u[2] * a[2] - d\n const db = u[0] * b[0] + u[1] * b[1] + u[2] * b[2] - d\n const ina = da <= 0\n const inb = db <= 0\n if (ina) {\n polyP.push(a[0], a[1], a[2])\n polyN.push(na[0], na[1], na[2])\n if (da > -1e-7) cut.push(a[0], a[1], a[2])\n }\n if (ina !== inb) {\n const t = da / (da - db)\n const x = a[0] + (b[0] - a[0]) * t\n const y = a[1] + (b[1] - a[1]) * t\n const z = a[2] + (b[2] - a[2]) * t\n polyP.push(x, y, z)\n polyN.push(na[0] + (nb[0] - na[0]) * t, na[1] + (nb[1] - na[1]) * t, na[2] + (nb[2] - na[2]) * t)\n cut.push(x, y, z)\n }\n }\n const n = polyP.length / 3\n for (let i = 1; i < n - 1; i++) {\n 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])\n }\n }\n\n // Cap on the plane: unique cut points sorted around their centre.\n const pts: V3[] = []\n for (let i = 0; i < cut.length; i += 3) {\n let dup = false\n for (const q of pts) {\n if (Math.abs(q[0] - cut[i]) + Math.abs(q[1] - cut[i + 1]) + Math.abs(q[2] - cut[i + 2]) < 1e-6) {\n dup = true\n break\n }\n }\n if (!dup) pts.push([cut[i], cut[i + 1], cut[i + 2]])\n }\n if (pts.length >= 3) {\n const c: V3 = [0, 0, 0]\n for (const q of pts) {\n c[0] += q[0] / pts.length\n c[1] += q[1] / pts.length\n c[2] += q[2] / pts.length\n }\n const e1 = norm(cross(u, Math.abs(u[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]))\n const e2 = cross(u, e1)\n 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)\n for (let i = 0; i < keyed.length; i++) {\n 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])\n }\n }\n return count\n}\n","import type * as THREE_NS from 'three'\nimport type { ContainerState, FluidWorld, Vec3 } from '@adatechnology/game-fluids'\nimport { boxSolid, clipSolid, cylinderSolid } from './clip'\nimport type { Solid, V3 } from './clip'\nimport type { MaterialCache } from './materials'\nimport type { ParticlePool } from './particles'\nimport type { QualityProfile } from './quality'\nimport type { ThreeLib } from './types'\n\n/** How far the liquid is kept inside the container walls so it does not fight with them. */\nconst INSET = 0.985\n\n/**\n * The liquid inside one bound container: the container's inner shape clipped by the world surface plane, expressed in the bound object's space,\n * so the cap always lies on the plane whatever the tilt. Plus steam puffs above a hot surface.\n */\nexport class LiquidBody {\n readonly group: THREE_NS.Group\n private mesh: THREE_NS.Mesh | null = null\n private geometry: THREE_NS.BufferGeometry | null = null\n private pos: Float32Array = new Float32Array(0)\n private nor: Float32Array = new Float32Array(0)\n private solid: Solid | null = null\n private shapeKey = ''\n private steamAcc = 0\n private readonly inverse: THREE_NS.Matrix4\n private readonly tmpN: THREE_NS.Vector3\n private readonly tmpP: THREE_NS.Vector3\n vertices = 0\n\n constructor(\n private readonly THREE: ThreeLib,\n readonly containerId: string,\n readonly object: THREE_NS.Object3D,\n offset: Vec3,\n ) {\n this.group = new THREE.Group()\n this.group.name = 'fluid_liquid_body'\n this.group.position.set(offset[0], offset[1], offset[2])\n this.inverse = new THREE.Matrix4()\n this.tmpN = new THREE.Vector3()\n this.tmpP = new THREE.Vector3()\n object.add(this.group)\n }\n\n get visible(): boolean {\n return !!this.mesh && this.mesh.visible\n }\n\n private build(state: ContainerState, quality: QualityProfile): void {\n const shape = state.shape\n if (!shape) return\n const key = JSON.stringify(shape) + quality.bodySegments\n if (key === this.shapeKey) return\n this.shapeKey = key\n this.solid =\n shape.kind === 'cylinder'\n ? cylinderSolid(shape.radius * INSET, shape.height * INSET, quality.bodySegments)\n : boxSolid([shape.size[0] * INSET, shape.size[1] * INSET, shape.size[2] * INSET])\n const T = this.THREE\n this.geometry?.dispose()\n this.pos = new Float32Array(this.solid.maxVertices * 3)\n this.nor = new Float32Array(this.solid.maxVertices * 3)\n this.geometry = new T.BufferGeometry()\n const p = new T.BufferAttribute(this.pos, 3)\n const n = new T.BufferAttribute(this.nor, 3)\n p.setUsage(T.DynamicDrawUsage)\n n.setUsage(T.DynamicDrawUsage)\n this.geometry.setAttribute('position', p)\n this.geometry.setAttribute('normal', n)\n if (!this.mesh) {\n this.mesh = new T.Mesh(this.geometry)\n this.mesh.name = 'fluid_liquid'\n this.mesh.frustumCulled = false\n this.mesh.renderOrder = 3\n this.group.add(this.mesh)\n } else this.mesh.geometry = this.geometry\n }\n\n update(dt: number, world: FluidWorld, materials: MaterialCache, quality: QualityProfile, particles: ParticlePool): void {\n const state = world.container(this.containerId)\n const def = state?.liquid ? world.liquids.get(state.liquid) : undefined\n if (!state || !state.shape || !def || state.liters <= 1e-6) {\n if (this.mesh) this.mesh.visible = false\n this.vertices = 0\n return\n }\n this.build(state, quality)\n if (!this.mesh || !this.solid) return\n this.mesh.material = materials.liquidBody(def)\n\n // The surface plane in the group's space.\n this.object.updateWorldMatrix(true, false)\n this.group.updateWorldMatrix(true, false)\n this.inverse.copy(this.group.matrixWorld).invert()\n this.tmpN.set(state.surfaceNormal[0], state.surfaceNormal[1], state.surfaceNormal[2]).transformDirection(this.inverse)\n this.tmpP.set(state.surfacePoint[0], state.surfacePoint[1], state.surfacePoint[2]).applyMatrix4(this.inverse)\n const u: V3 = [this.tmpN.x, this.tmpN.y, this.tmpN.z]\n const d = u[0] * this.tmpP.x + u[1] * this.tmpP.y + u[2] * this.tmpP.z\n\n this.vertices = clipSolid(this.solid, u, d, this.pos, this.nor)\n const geometry = this.geometry as THREE_NS.BufferGeometry\n geometry.setDrawRange(0, this.vertices)\n ;(geometry.getAttribute('position') as THREE_NS.BufferAttribute).needsUpdate = true\n ;(geometry.getAttribute('normal') as THREE_NS.BufferAttribute).needsUpdate = true\n this.mesh.visible = this.vertices > 0\n\n // Steam above a hot open surface.\n if (state.steaming && this.vertices > 0) {\n this.steamAcc += dt * quality.steamRate\n while (this.steamAcc >= 1) {\n this.steamAcc -= 1\n const o = this.tmpP.set(state.surfacePoint[0], state.surfacePoint[1], state.surfacePoint[2])\n particles.steam([o.x, o.y + 0.01, o.z])\n }\n } else this.steamAcc = 0\n }\n\n get triangles(): number {\n return this.visible ? Math.floor(this.vertices / 3) : 0\n }\n\n dispose(): void {\n this.object.remove(this.group)\n this.geometry?.dispose()\n this.geometry = null\n this.mesh = null\n }\n}\n","export const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)\nexport const lerp = (a: number, b: number, t: number): number => a + (b - a) * t\n\n/** Small seeded generator for cosmetic randomness (droplet directions): the same input gives the same droplets. */\nexport class Rng {\n private state: number\n constructor(seed: number) {\n this.state = (seed >>> 0) || 1\n }\n next(): number {\n this.state = (this.state + 0x6d2b79f5) >>> 0\n let t = this.state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\n/** Integer hash of a point, to seed cosmetic randomness from world state. */\nexport const hashPoint = (x: number, y: number, z: number): number => {\n let h = 2166136261\n for (const v of [x, y, z]) h = Math.imul(h ^ Math.round(v * 1000), 16777619)\n return h >>> 0\n}\n","import type * as THREE_NS from 'three'\nimport type { LiquidDef } from '@adatechnology/game-fluids'\nimport { clamp } from './util'\nimport type { ThreeLib } from './types'\n\n/** Colour of a stain: the liquid's `stainColor`, else a darker `color`. */\nexport const stainColorOf = (def: LiquidDef): number => {\n if (def.stainColor !== undefined) return def.stainColor\n const r = ((def.color >> 16) & 255) * 0.65\n const g = ((def.color >> 8) & 255) * 0.65\n const b = (def.color & 255) * 0.65\n return (Math.round(r) << 16) | (Math.round(g) << 8) | Math.round(b)\n}\n\n/**\n * One material per liquid and role, created on demand and shared by every mesh: each liquid is its own material (colour, opacity, gloss).\n * Wet surfaces use a low roughness from `gloss` so they reflect the scene's environment.\n */\nexport class MaterialCache {\n private body = new Map<string, THREE_NS.MeshStandardMaterial>()\n private stream = new Map<string, THREE_NS.MeshStandardMaterial>()\n private film = new Map<string, THREE_NS.MeshStandardMaterial>()\n private wet = new Map<string, THREE_NS.MeshStandardMaterial>()\n\n constructor(\n private readonly THREE: ThreeLib,\n private readonly envMap: THREE_NS.Texture | null,\n ) {}\n\n private make(params: THREE_NS.MeshStandardMaterialParameters): THREE_NS.MeshStandardMaterial {\n const m = new this.THREE.MeshStandardMaterial({ metalness: 0, ...params })\n if (this.envMap) m.envMap = this.envMap\n return m\n }\n\n /** The volume of liquid inside a container. */\n liquidBody(def: LiquidDef): THREE_NS.MeshStandardMaterial {\n let m = this.body.get(def.id)\n if (!m) {\n m = this.make({\n color: def.color,\n opacity: def.opacity,\n transparent: def.opacity < 0.999,\n roughness: clamp(1 - def.gloss * 0.9, 0.04, 1),\n envMapIntensity: 0.5 + def.gloss * 0.9,\n depthWrite: def.opacity >= 0.999,\n })\n this.body.set(def.id, m)\n }\n return m\n }\n\n /** A jet or a drop in the air. */\n liquidStream(def: LiquidDef): THREE_NS.MeshStandardMaterial {\n let m = this.stream.get(def.id)\n if (!m) {\n m = this.make({\n color: def.color,\n opacity: clamp(def.opacity + 0.1, 0.35, 1),\n transparent: true,\n roughness: clamp(1 - def.gloss * 0.95, 0.03, 1),\n envMapIntensity: 0.8 + def.gloss,\n side: this.THREE.DoubleSide,\n depthWrite: false,\n })\n this.stream.set(def.id, m)\n }\n return m\n }\n\n /** The thin outer film of a stain (and the permanent mark): tinted by vertex colour, soft rim by vertex alpha, mostly matte. */\n stainFilm(def: LiquidDef): THREE_NS.MeshStandardMaterial {\n let m = this.film.get(def.id)\n if (!m) {\n m = this.make({\n color: 0xffffff,\n vertexColors: true,\n transparent: true,\n roughness: clamp(0.95 - def.gloss * 0.45, 0.3, 1),\n envMapIntensity: 0.4 + def.gloss * 0.4,\n depthWrite: false,\n polygonOffset: true,\n polygonOffsetFactor: -2,\n polygonOffsetUnits: -2,\n })\n this.film.set(def.id, m)\n }\n return m\n }\n\n /** The glossy wet part of a stain, which shrinks while it dries. */\n stainWet(def: LiquidDef): THREE_NS.MeshStandardMaterial {\n let m = this.wet.get(def.id)\n if (!m) {\n m = this.make({\n color: 0xffffff,\n vertexColors: true,\n transparent: true,\n roughness: clamp(1 - def.gloss, 0.02, 0.7),\n envMapIntensity: 0.9 + def.gloss * 1.4,\n depthWrite: false,\n polygonOffset: true,\n polygonOffsetFactor: -3,\n polygonOffsetUnits: -3,\n })\n this.wet.set(def.id, m)\n }\n return m\n }\n\n dispose(): void {\n for (const map of [this.body, this.stream, this.film, this.wet]) {\n for (const m of map.values()) m.dispose()\n map.clear()\n }\n }\n}\n","import type * as THREE_NS from 'three'\nimport { Rng, clamp, hashPoint } from './util'\nimport type { ThreeLib } from './types'\n\nconst VERTEX = /* glsl */ `\nattribute float aSize;\nattribute vec4 aColor;\nattribute float aSoft;\nuniform float uScale;\nvarying vec4 vColor;\nvarying float vSoft;\nvoid main() {\n vColor = aColor;\n vSoft = aSoft;\n vec4 mv = modelViewMatrix * vec4(position, 1.0);\n gl_PointSize = aSize * uScale / max(0.1, -mv.z);\n gl_Position = projectionMatrix * mv;\n}\n`\n\n// Round sprite drawn in the fragment shader: no textures.\nconst FRAGMENT = /* glsl */ `\nvarying vec4 vColor;\nvarying float vSoft;\nvoid main() {\n float d = length(gl_PointCoord - vec2(0.5)) * 2.0;\n if (d > 1.0 || vColor.a <= 0.001) discard;\n float edge = mix(smoothstep(1.0, 0.8, d), 1.0 - d, vSoft);\n gl_FragColor = vec4(vColor.rgb, vColor.a * edge);\n}\n`\n\n/**\n * A fixed pool of round point sprites: splash droplets (ballistic, they die on the floor they hit) and steam (soft, rising puffs).\n * One draw call. When the pool is full the oldest slot is reused.\n */\nexport class ParticlePool {\n readonly points: THREE_NS.Points\n private readonly geometry: THREE_NS.BufferGeometry\n private readonly material: THREE_NS.ShaderMaterial\n private readonly capacity: number\n private next = 0\n private readonly pos: Float32Array\n private readonly size: Float32Array\n private readonly color: Float32Array\n private readonly soft: Float32Array\n private readonly vel: Float32Array\n private readonly life: Float32Array\n private readonly maxLife: Float32Array\n private readonly size0: Float32Array\n private readonly size1: Float32Array\n private readonly alpha: Float32Array\n private readonly floorY: Float32Array\n private readonly kind: Uint8Array\n private readonly rgb: Float32Array\n private readonly rng = new Rng(1234567)\n private alive = 0\n\n constructor(\n private readonly THREE: ThreeLib,\n capacity: number,\n pointScale: number,\n ) {\n this.capacity = capacity\n this.pos = new Float32Array(capacity * 3)\n this.size = new Float32Array(capacity)\n this.color = new Float32Array(capacity * 4)\n this.soft = new Float32Array(capacity)\n this.vel = new Float32Array(capacity * 3)\n this.life = new Float32Array(capacity)\n this.maxLife = new Float32Array(capacity)\n this.size0 = new Float32Array(capacity)\n this.size1 = new Float32Array(capacity)\n this.alpha = new Float32Array(capacity)\n this.floorY = new Float32Array(capacity)\n this.kind = new Uint8Array(capacity)\n this.rgb = new Float32Array(capacity * 3)\n this.life.fill(0)\n const T = THREE\n this.geometry = new T.BufferGeometry()\n const attr = (array: Float32Array, n: number) => {\n const a = new T.BufferAttribute(array, n)\n a.setUsage(T.DynamicDrawUsage)\n return a\n }\n this.geometry.setAttribute('position', attr(this.pos, 3))\n this.geometry.setAttribute('aSize', attr(this.size, 1))\n this.geometry.setAttribute('aColor', attr(this.color, 4))\n this.geometry.setAttribute('aSoft', attr(this.soft, 1))\n this.material = new T.ShaderMaterial({\n vertexShader: VERTEX,\n fragmentShader: FRAGMENT,\n uniforms: { uScale: { value: pointScale } },\n transparent: true,\n depthWrite: false,\n })\n this.points = new T.Points(this.geometry, this.material)\n this.points.frustumCulled = false\n this.points.renderOrder = 4\n this.points.name = 'fluid_particles'\n }\n\n get count(): number {\n return this.alive\n }\n\n private slot(): number {\n const i = this.next\n this.next = (this.next + 1) % this.capacity\n return i\n }\n\n private setColor(i: number, hex: number): void {\n this.rgb[i * 3] = ((hex >> 16) & 255) / 255\n this.rgb[i * 3 + 1] = ((hex >> 8) & 255) / 255\n this.rgb[i * 3 + 2] = (hex & 255) / 255\n }\n\n /** Droplets thrown up by an impact; `floorY` is the height they stop at. */\n splash(point: [number, number, number], intensity: number, hex: number, opacity: number, count: number): void {\n const rng = new Rng(hashPoint(point[0], point[1], point[2]) ^ this.next)\n const n = Math.max(2, Math.round(count * (0.35 + 0.65 * clamp(intensity, 0, 1))))\n for (let k = 0; k < n; k++) {\n const i = this.slot()\n const a = rng.next() * Math.PI * 2\n const h = (0.25 + rng.next() * 0.9) * (0.4 + intensity)\n const up = (0.7 + rng.next() * 1.5) * (0.5 + intensity)\n this.pos.set([point[0], point[1] + 0.01, point[2]], i * 3)\n this.vel.set([Math.cos(a) * h, up, Math.sin(a) * h], i * 3)\n this.life[i] = this.maxLife[i] = 0.45 + rng.next() * 0.45\n this.size0[i] = 0.012 + rng.next() * 0.014\n this.size1[i] = this.size0[i] * 0.7\n this.alpha[i] = clamp(opacity + 0.2, 0.35, 1)\n this.soft[i] = 0\n this.kind[i] = 0\n this.floorY[i] = point[1]\n this.setColor(i, hex)\n }\n }\n\n /** One puff of steam rising from `point`. */\n steam(point: [number, number, number]): void {\n const i = this.slot()\n const r = this.rng\n this.pos.set([point[0] + (r.next() - 0.5) * 0.04, point[1], point[2] + (r.next() - 0.5) * 0.04], i * 3)\n this.vel.set([(r.next() - 0.5) * 0.06, 0.22 + r.next() * 0.12, (r.next() - 0.5) * 0.06], i * 3)\n this.life[i] = this.maxLife[i] = 1.2 + r.next() * 0.6\n this.size0[i] = 0.03\n this.size1[i] = 0.1\n this.alpha[i] = 0.24\n this.soft[i] = 1\n this.kind[i] = 1\n this.floorY[i] = -1e9\n this.setColor(i, 0xf4f8ff)\n }\n\n update(dt: number, gravity: number): void {\n let alive = 0\n for (let i = 0; i < this.capacity; i++) {\n if (this.life[i] <= 0) {\n this.size[i] = 0\n this.color[i * 4 + 3] = 0\n continue\n }\n this.life[i] -= dt\n const k = 1 - this.life[i] / this.maxLife[i]\n if (this.life[i] <= 0) {\n this.size[i] = 0\n this.color[i * 4 + 3] = 0\n continue\n }\n alive++\n if (this.kind[i] === 0) {\n this.vel[i * 3 + 1] -= gravity * dt\n } else {\n // Steam drifts sideways a little while rising.\n this.vel[i * 3] += Math.sin((k + i) * 7) * 0.02 * dt\n }\n this.pos[i * 3] += this.vel[i * 3] * dt\n this.pos[i * 3 + 1] += this.vel[i * 3 + 1] * dt\n this.pos[i * 3 + 2] += this.vel[i * 3 + 2] * dt\n if (this.kind[i] === 0 && this.pos[i * 3 + 1] < this.floorY[i]) {\n this.life[i] = 0\n alive--\n this.size[i] = 0\n this.color[i * 4 + 3] = 0\n continue\n }\n this.size[i] = this.size0[i] + (this.size1[i] - this.size0[i]) * k\n const fade = this.kind[i] === 1 ? Math.min(1, k * 4) * (1 - k) : 1 - k * k\n this.color[i * 4] = this.rgb[i * 3]\n this.color[i * 4 + 1] = this.rgb[i * 3 + 1]\n this.color[i * 4 + 2] = this.rgb[i * 3 + 2]\n this.color[i * 4 + 3] = this.alpha[i] * fade\n }\n this.alive = alive\n for (const name of ['position', 'aSize', 'aColor', 'aSoft']) (this.geometry.getAttribute(name) as THREE_NS.BufferAttribute).needsUpdate = true\n }\n\n clear(): void {\n this.life.fill(0)\n this.alive = 0\n this.update(0, 0)\n }\n\n dispose(): void {\n this.geometry.dispose()\n this.material.dispose()\n }\n}\n","import type { FluidQuality } from './types'\n\nexport interface QualityProfile {\n /** Radial segments of the container liquid volume. */\n bodySegments: number\n /** Samples along a stream and sides of its tube. */\n streamSamples: number\n streamSides: number\n /** Drops of a thin stream. */\n thinDrops: number\n /** Subdivisions per front sample of a stain outline (16 samples). */\n stainSubdiv: number\n maxStains: number\n maxStreams: number\n maxParticles: number\n /** Steam puffs per second of a steaming container. */\n steamRate: number\n /** Droplets of a full-intensity splash. */\n splashDroplets: number\n}\n\nexport const PROFILES: Record<FluidQuality, QualityProfile> = {\n low: { bodySegments: 10, streamSamples: 8, streamSides: 4, thinDrops: 4, stainSubdiv: 1, maxStains: 24, maxStreams: 12, maxParticles: 96, steamRate: 4, splashDroplets: 5 },\n medium: { bodySegments: 14, streamSamples: 12, streamSides: 5, thinDrops: 6, stainSubdiv: 2, maxStains: 48, maxStreams: 24, maxParticles: 256, steamRate: 8, splashDroplets: 9 },\n high: { bodySegments: 20, streamSamples: 20, streamSides: 8, thinDrops: 8, stainSubdiv: 4, maxStains: 96, maxStreams: 48, maxParticles: 512, steamRate: 14, splashDroplets: 14 },\n}\n\nexport const profileOf = (q: FluidQuality | undefined): QualityProfile => PROFILES[q ?? 'medium'] ?? PROFILES.medium\n","[\n {\n \"id\": \"water\",\n \"color\": 11064558,\n \"opacity\": 0.35,\n \"stainColor\": 7309206,\n \"viscosity\": 1,\n \"density\": 1000,\n \"dryTime\": 600,\n \"gloss\": 0.9,\n \"drink\": {\n \"thirst\": 100,\n \"effects\": []\n },\n \"tags\": [\n \"drink\",\n \"clear\"\n ]\n },\n {\n \"id\": \"coffee\",\n \"color\": 3875604,\n \"opacity\": 0.95,\n \"stainColor\": 2824202,\n \"viscosity\": 1.5,\n \"density\": 1005,\n \"dryTime\": 900,\n \"gloss\": 0.8,\n \"leavesMark\": true,\n \"steamC\": 60,\n \"drink\": {\n \"thirst\": 80,\n \"effects\": [\n \"caffeine\"\n ]\n },\n \"tags\": [\n \"drink\",\n \"hot\"\n ]\n },\n {\n \"id\": \"tea\",\n \"color\": 9063188,\n \"opacity\": 0.8,\n \"stainColor\": 6961682,\n \"viscosity\": 1.4,\n \"density\": 1005,\n \"dryTime\": 800,\n \"gloss\": 0.8,\n \"leavesMark\": true,\n \"steamC\": 60,\n \"drink\": {\n \"thirst\": 90,\n \"effects\": []\n },\n \"tags\": [\n \"drink\",\n \"hot\"\n ]\n },\n {\n \"id\": \"milk\",\n \"color\": 16052712,\n \"opacity\": 0.98,\n \"stainColor\": 14473160,\n \"viscosity\": 2,\n \"density\": 1030,\n \"dryTime\": 900,\n \"gloss\": 0.5,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 70,\n \"effects\": []\n },\n \"tags\": [\n \"drink\"\n ]\n },\n {\n \"id\": \"soda\",\n \"color\": 3808530,\n \"opacity\": 0.85,\n \"stainColor\": 2757901,\n \"viscosity\": 1.3,\n \"density\": 1040,\n \"dryTime\": 1200,\n \"gloss\": 0.85,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 60,\n \"effects\": []\n },\n \"tags\": [\n \"drink\",\n \"sticky\"\n ]\n },\n {\n \"id\": \"juice\",\n \"color\": 15239467,\n \"opacity\": 0.9,\n \"stainColor\": 12741916,\n \"viscosity\": 2,\n \"density\": 1045,\n \"dryTime\": 1100,\n \"gloss\": 0.8,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 75,\n \"effects\": []\n },\n \"tags\": [\n \"drink\",\n \"sticky\"\n ]\n },\n {\n \"id\": \"beer\",\n \"color\": 14259740,\n \"opacity\": 0.8,\n \"stainColor\": 11039764,\n \"viscosity\": 1.4,\n \"density\": 1010,\n \"dryTime\": 900,\n \"gloss\": 0.8,\n \"drink\": {\n \"thirst\": 50,\n \"effects\": [\n \"alcohol\"\n ]\n },\n \"tags\": [\n \"drink\",\n \"alcohol\"\n ]\n },\n {\n \"id\": \"wine\",\n \"color\": 7016223,\n \"opacity\": 0.9,\n \"stainColor\": 5048854,\n \"viscosity\": 1.6,\n \"density\": 990,\n \"dryTime\": 800,\n \"gloss\": 0.85,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 40,\n \"effects\": [\n \"alcohol\"\n ]\n },\n \"tags\": [\n \"drink\",\n \"alcohol\"\n ]\n },\n {\n \"id\": \"spirits\",\n \"color\": 13214042,\n \"opacity\": 0.6,\n \"stainColor\": 10320959,\n \"viscosity\": 1.8,\n \"density\": 940,\n \"dryTime\": 200,\n \"gloss\": 0.85,\n \"flammable\": true,\n \"drink\": {\n \"thirst\": 5,\n \"effects\": [\n \"alcohol\"\n ]\n },\n \"tags\": [\n \"drink\",\n \"alcohol\"\n ]\n },\n {\n \"id\": \"fuel\",\n \"color\": 14926682,\n \"opacity\": 0.6,\n \"stainColor\": 7232298,\n \"viscosity\": 0.6,\n \"density\": 740,\n \"dryTime\": 120,\n \"gloss\": 0.95,\n \"leavesMark\": true,\n \"flammable\": true,\n \"drink\": {\n \"thirst\": 0,\n \"effects\": [\n \"poison\"\n ]\n },\n \"tags\": [\n \"fuel\",\n \"gasoline\"\n ]\n },\n {\n \"id\": \"diesel\",\n \"color\": 12098094,\n \"opacity\": 0.8,\n \"stainColor\": 5917208,\n \"viscosity\": 3,\n \"density\": 830,\n \"dryTime\": 3600,\n \"gloss\": 0.95,\n \"leavesMark\": true,\n \"flammable\": true,\n \"drink\": {\n \"thirst\": 0,\n \"effects\": [\n \"poison\"\n ]\n },\n \"tags\": [\n \"fuel\"\n ]\n },\n {\n \"id\": \"motor_oil\",\n \"color\": 3877388,\n \"opacity\": 0.95,\n \"stainColor\": 1840134,\n \"viscosity\": 100,\n \"density\": 880,\n \"dryTime\": 0,\n \"gloss\": 1,\n \"leavesMark\": true,\n \"flammable\": true,\n \"drink\": {\n \"thirst\": 0,\n \"effects\": [\n \"poison\"\n ]\n },\n \"tags\": [\n \"oil\"\n ]\n },\n {\n \"id\": \"cooking_oil\",\n \"color\": 14267708,\n \"opacity\": 0.85,\n \"stainColor\": 11110698,\n \"viscosity\": 60,\n \"density\": 920,\n \"dryTime\": 0,\n \"gloss\": 1,\n \"leavesMark\": true,\n \"flammable\": true,\n \"drink\": {\n \"thirst\": 0,\n \"effects\": [\n \"poison\"\n ]\n },\n \"tags\": [\n \"oil\",\n \"food\"\n ]\n },\n {\n \"id\": \"blood\",\n \"color\": 7997964,\n \"opacity\": 0.92,\n \"stainColor\": 4850950,\n \"viscosity\": 4,\n \"density\": 1060,\n \"dryTime\": 1200,\n \"gloss\": 0.75,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 10,\n \"effects\": [\n \"disgust\"\n ]\n },\n \"tags\": [\n \"organic\"\n ]\n },\n {\n \"id\": \"sap\",\n \"color\": 13146940,\n \"opacity\": 0.8,\n \"stainColor\": 10122282,\n \"viscosity\": 200,\n \"density\": 1100,\n \"dryTime\": 3600,\n \"gloss\": 0.8,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 20,\n \"effects\": []\n },\n \"tags\": [\n \"organic\",\n \"sticky\"\n ]\n },\n {\n \"id\": \"acid\",\n \"color\": 10146378,\n \"opacity\": 0.6,\n \"stainColor\": 7313970,\n \"viscosity\": 1.5,\n \"density\": 1200,\n \"dryTime\": 900,\n \"gloss\": 0.9,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 0,\n \"effects\": [\n \"poison\"\n ]\n },\n \"tags\": [\n \"corrosive\"\n ]\n },\n {\n \"id\": \"paint\",\n \"color\": 11740715,\n \"opacity\": 0.98,\n \"stainColor\": 9050655,\n \"viscosity\": 300,\n \"density\": 1300,\n \"dryTime\": 1800,\n \"gloss\": 0.6,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 0,\n \"effects\": [\n \"poison\"\n ]\n },\n \"tags\": [\n \"paint\"\n ]\n },\n {\n \"id\": \"bleach\",\n \"color\": 15134688,\n \"opacity\": 0.7,\n \"stainColor\": 13621446,\n \"viscosity\": 1.2,\n \"density\": 1100,\n \"dryTime\": 900,\n \"gloss\": 0.85,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 0,\n \"effects\": [\n \"poison\"\n ]\n },\n \"tags\": [\n \"corrosive\"\n ]\n },\n {\n \"id\": \"honey\",\n \"color\": 14193162,\n \"opacity\": 0.9,\n \"stainColor\": 12089350,\n \"viscosity\": 3000,\n \"density\": 1420,\n \"dryTime\": 14400,\n \"gloss\": 0.95,\n \"leavesMark\": true,\n \"drink\": {\n \"thirst\": 10,\n \"effects\": []\n },\n \"tags\": [\n \"drink\",\n \"sticky\"\n ]\n }\n]","import liquidsData from './data/liquids.json'\nimport type { LiquidDef } from './types'\n\n/**\n * Built-in liquids (data in `data/liquids.json`; colours are `0xRRGGBB` stored as integers).\n * Viscosity is relative to water, density in kg/m³, `dryTime` in seconds for a thin film at 20 °C (0 = never dries).\n */\nexport const LIQUIDS: LiquidDef[] = liquidsData as unknown as LiquidDef[]\n","import type { Quat, Vec3 } from './types'\n\nexport const GRAVITY = 9.81\nexport const EPS = 1e-7\n\nexport const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)\nexport const lerp = (a: number, b: number, t: number): number => a + (b - a) * t\n\nexport const add = (a: Vec3, b: Vec3): Vec3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]]\nexport const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]\nexport const scale = (a: Vec3, s: number): Vec3 => [a[0] * s, a[1] * s, a[2] * s]\nexport const dot = (a: Vec3, b: Vec3): number => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]\nexport const cross = (a: Vec3, b: Vec3): Vec3 => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]\nexport const length = (a: Vec3): number => Math.hypot(a[0], a[1], a[2])\nexport const distance = (a: Vec3, b: Vec3): number => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2])\nexport const normalize = (a: Vec3, fallback: Vec3 = [0, 1, 0]): Vec3 => {\n const l = length(a)\n return l < 1e-9 ? [fallback[0], fallback[1], fallback[2]] : [a[0] / l, a[1] / l, a[2] / l]\n}\nexport const copy3 = (a: Vec3): Vec3 => [a[0], a[1], a[2]]\nexport const lerp3 = (a: Vec3, b: Vec3, t: number): Vec3 => [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)]\n\n/** Rotate `v` by the unit quaternion `q` (x, y, z, w). */\nexport const quatRotate = (q: Quat, v: Vec3): Vec3 => {\n const u: Vec3 = [q[0], q[1], q[2]]\n const c1 = cross(u, v)\n const c2 = cross(u, c1)\n return [v[0] + 2 * (q[3] * c1[0] + c2[0]), v[1] + 2 * (q[3] * c1[1] + c2[1]), v[2] + 2 * (q[3] * c1[2] + c2[2])]\n}\nexport const quatConjugate = (q: Quat): Quat => [-q[0], -q[1], -q[2], q[3]]\nexport const quatMultiply = (a: Quat, b: Quat): Quat => [\n a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],\n a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],\n a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],\n a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],\n]\nexport const quatNormalize = (q: Quat): Quat => {\n const l = Math.hypot(q[0], q[1], q[2], q[3])\n return l < 1e-9 ? [0, 0, 0, 1] : [q[0] / l, q[1] / l, q[2] / l, q[3] / l]\n}\n/** Quaternion from Euler angles in XYZ order (the same convention as three.js). */\nexport const quatFromEuler = (x: number, y: number, z: number): Quat => {\n const c1 = Math.cos(x / 2)\n const c2 = Math.cos(y / 2)\n const c3 = Math.cos(z / 2)\n const s1 = Math.sin(x / 2)\n const s2 = Math.sin(y / 2)\n const s3 = Math.sin(z / 2)\n return [s1 * c2 * c3 + c1 * s2 * s3, c1 * s2 * c3 - s1 * c2 * s3, c1 * c2 * s3 + s1 * s2 * c3, c1 * c2 * c3 - s1 * s2 * s3]\n}\n\n/** Small seeded generator (mulberry32). Its whole state is one integer, so it serialises trivially. */\nexport class Rng {\n state: number\n constructor(seed: number) {\n this.state = (Math.imul((seed | 0) ^ 0x9e3779b9, 0x85ebca6b) >>> 0) || 1\n }\n next(): number {\n this.state = (this.state + 0x6d2b79f5) >>> 0\n let t = this.state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n","import type { StreamState, Vec3 } from './types'\nimport { dot, sub } from './math'\nimport type { Basis } from './stains'\n\nexport const GROUND_ID = 'ground'\n/** Longest flight simulated for a stream before it counts as landing nowhere. */\nexport const MAX_FLIGHT = 3\n\n/** Position of a ballistic jet `t` seconds after leaving its origin. */\nexport const ballistic = (origin: Vec3, velocity: Vec3, gravity: number, t: number): Vec3 => [\n origin[0] + velocity[0] * t,\n origin[1] + velocity[1] * t - 0.5 * gravity * t * t,\n origin[2] + velocity[2] * t,\n]\n\n/** Point of a stream's path `t` seconds after its origin (use with `StreamState.flightTime` and the world's `gravity`). */\nexport const sampleStream = (stream: Pick<StreamState, 'origin' | 'velocity'>, gravity: number, t: number): Vec3 =>\n ballistic(stream.origin, stream.velocity, gravity, t)\n\nexport interface LandingSurface extends Basis {\n id: string\n point: Vec3\n half: [number, number]\n}\n\nexport interface Hit {\n t: number\n point: Vec3\n normal: Vec3\n surface: string\n /** Velocity of the jet at impact. */\n velocity: Vec3\n}\n\nconst velocityAt = (velocity: Vec3, gravity: number, t: number): Vec3 => [velocity[0], velocity[1] - gravity * t, velocity[2]]\n\n/** First thing a jet hits: an authored surface (plane with half extents) or else the ground. */\nexport const trace = (\n surfaces: Iterable<LandingSurface>,\n groundAt: (x: number, z: number) => number,\n origin: Vec3,\n velocity: Vec3,\n gravity: number,\n): Hit | null => {\n let best: Hit | null = null\n for (const s of surfaces) {\n const n = s.normal\n const c = dot(n, sub(origin, s.point))\n if (c < -0.005) continue\n const a = -0.5 * gravity * n[1]\n const b = dot(n, velocity)\n let t = Infinity\n if (Math.abs(a) < 1e-9) {\n if (b < -1e-9) t = -c / b\n } else {\n const disc = b * b - 4 * a * c\n if (disc >= 0) {\n const sq = Math.sqrt(disc)\n for (const root of [(-b - sq) / (2 * a), (-b + sq) / (2 * a)]) {\n if (root >= -1e-6 && b + 2 * a * root < 0 && root < t) t = root\n }\n }\n }\n if (!Number.isFinite(t) || t > MAX_FLIGHT) continue\n t = Math.max(0, t)\n if (best && t >= best.t) continue\n const p = ballistic(origin, velocity, gravity, t)\n const rel = sub(p, s.point)\n if (Math.abs(dot(rel, s.t1)) > s.half[0] || Math.abs(dot(rel, s.t2)) > s.half[1]) continue\n best = { t, point: p, normal: n, surface: s.id, velocity: velocityAt(velocity, gravity, t) }\n }\n\n // Ground: march the path and refine the crossing.\n const step = 0.02\n let prevT = 0\n const startOver = origin[1] - groundAt(origin[0], origin[2])\n const limit = best ? best.t : MAX_FLIGHT\n if (startOver <= 0) {\n return best && best.t <= 0 ? best : { t: 0, point: [origin[0], groundAt(origin[0], origin[2]), origin[2]], normal: [0, 1, 0], surface: GROUND_ID, velocity }\n }\n for (let t = step; t <= limit + step; t += step) {\n const tt = Math.min(t, limit)\n const p = ballistic(origin, velocity, gravity, tt)\n const over = p[1] - groundAt(p[0], p[2])\n if (over <= 0) {\n let lo = prevT\n let hi = tt\n for (let i = 0; i < 18; i++) {\n const mid = (lo + hi) / 2\n const q = ballistic(origin, velocity, gravity, mid)\n if (q[1] - groundAt(q[0], q[2]) > 0) lo = mid\n else hi = mid\n }\n if (best && hi >= best.t - 1e-3) return best\n const q = ballistic(origin, velocity, gravity, hi)\n return { t: hi, point: [q[0], groundAt(q[0], q[2]), q[2]], normal: [0, 1, 0], surface: GROUND_ID, velocity: velocityAt(velocity, gravity, hi) }\n }\n prevT = tt\n if (tt >= limit) break\n }\n return best\n}\n","import type { FluidEvent, LiquidId } from './types'\n\n/** Locales shipped with the package. Unknown locales fall back to `en`. */\nexport type FluidLocale = 'en' | 'pt-BR'\n\nexport interface FluidStrings {\n /** Display name per liquid id (unknown ids fall back to the id itself). */\n liquids: Record<LiquidId, string>\n /** Short label per event type. */\n events: Record<FluidEvent['type'], string>\n on: string\n off: string\n}\n\nexport const locales: Record<FluidLocale, FluidStrings> = {\n en: {\n liquids: {\n water: 'water',\n coffee: 'coffee',\n tea: 'tea',\n milk: 'milk',\n soda: 'soda',\n juice: 'juice',\n beer: 'beer',\n wine: 'wine',\n spirits: 'spirits',\n fuel: 'gasoline',\n diesel: 'diesel',\n motor_oil: 'motor oil',\n cooking_oil: 'cooking oil',\n blood: 'blood',\n sap: 'sap',\n acid: 'acid',\n paint: 'paint',\n bleach: 'bleach',\n honey: 'honey',\n },\n events: {\n fill: 'filled',\n pour: 'poured',\n spill: 'spilled',\n leak: 'leaking',\n drink: 'drank',\n empty: 'emptied',\n stream_end: 'stream ended',\n splash: 'splash',\n stain: 'stain',\n dried: 'dried',\n steam: 'steam',\n mix: 'mixed',\n tap: 'tap',\n },\n on: 'on',\n off: 'off',\n },\n 'pt-BR': {\n liquids: {\n water: 'água',\n coffee: 'café',\n tea: 'chá',\n milk: 'leite',\n soda: 'refrigerante',\n juice: 'suco',\n beer: 'cerveja',\n wine: 'vinho',\n spirits: 'destilado',\n fuel: 'gasolina',\n diesel: 'diesel',\n motor_oil: 'óleo de motor',\n cooking_oil: 'óleo de cozinha',\n blood: 'sangue',\n sap: 'seiva',\n acid: 'ácido',\n paint: 'tinta',\n bleach: 'água sanitária',\n honey: 'mel',\n },\n events: {\n fill: 'encheu',\n pour: 'despejou',\n spill: 'derramou',\n leak: 'vazando',\n drink: 'bebeu',\n empty: 'esvaziou',\n stream_end: 'parou de escorrer',\n splash: 'respingo',\n stain: 'mancha',\n dried: 'secou',\n steam: 'vapor',\n mix: 'misturou',\n tap: 'torneira',\n },\n on: 'ligado',\n off: 'desligado',\n },\n}\n\nexport const resolveStrings = (locale: string | undefined): FluidStrings => locales[(locale as FluidLocale) ?? 'en'] ?? locales.en\n\nconst fmt = (n: number): string => `${(Math.round(n * 1000) / 1000).toString()} l`\nconst pt = (p: [number, number, number]): string => `(${p.map((v) => Math.round(v * 100) / 100).join(', ')})`\n\n/** One-line description of an event for logs and debug panels, e.g. `drank: coffee 0.03 l`. */\nexport const describeEvent = (strings: FluidStrings, event: FluidEvent): string => {\n const label = strings.events[event.type]\n const name = (id: LiquidId) => strings.liquids[id] ?? id\n switch (event.type) {\n case 'fill':\n return `${label}: ${event.container} ${name(event.liquid)} ${fmt(event.liters)}`\n case 'pour':\n case 'spill':\n return `${label}: ${event.container} ${name(event.liquid)} ${fmt(event.rate)}/s`\n case 'leak':\n return `${label}: ${event.container} ${name(event.liquid)} ${fmt(event.rate)}/s ${pt(event.point)}`\n case 'drink':\n return `${label}: ${event.container} ${name(event.liquid)} ${fmt(event.liters)}${event.effects.length ? ` [${event.effects.join(', ')}]` : ''}${event.burn ? ' !' : ''}`\n case 'empty':\n return `${label}: ${event.container}`\n case 'stream_end':\n return `${label}: ${event.stream}`\n case 'splash':\n return `${label}: ${name(event.liquid)} ${pt(event.point)}`\n case 'stain':\n return `${label}: ${event.stain} ${name(event.liquid)} ${event.surface} ${pt(event.point)}`\n case 'dried':\n return `${label}: ${event.stain}${event.mark ? ' (mark)' : ''}`\n case 'steam':\n return `${label}: ${event.container} ${event.on ? strings.on : strings.off}`\n case 'mix':\n return `${label}: ${event.container} ${name(event.from)} -> ${name(event.into)}`\n case 'tap':\n return `${label}: ${event.tap} ${event.on ? strings.on : strings.off}`\n }\n}\n","import type { ContainerSpec, Vec3 } from './types'\nimport { clamp, normalize } from './math'\n\nexport type Shape = ContainerSpec['shape']\n\n/** Sample columns of a container's inner volume: equal-weight (x, z) positions with the full height `h`. */\nexport interface Columns {\n x: Float64Array\n z: Float64Array\n h: number\n /** Radius (cylinder) or half extents (box) used for the extent of a plane. */\n kind: 'cylinder' | 'box'\n r: number\n hx: number\n hz: number\n}\n\nconst RINGS = 8\nconst SEGMENTS = 16\nconst GRID = 12\n\nexport const makeColumns = (shape: Shape): Columns => {\n if (shape.kind === 'cylinder') {\n const x = new Float64Array(RINGS * SEGMENTS)\n const z = new Float64Array(RINGS * SEGMENTS)\n let n = 0\n for (let k = 0; k < RINGS; k++) {\n const rr = shape.radius * Math.sqrt((k + 0.5) / RINGS)\n for (let m = 0; m < SEGMENTS; m++) {\n // Offset every other ring so the samples do not line up radially.\n const a = ((m + (k % 2) * 0.5) / SEGMENTS) * Math.PI * 2\n x[n] = Math.cos(a) * rr\n z[n] = Math.sin(a) * rr\n n++\n }\n }\n return { x, z, h: shape.height, kind: 'cylinder', r: shape.radius, hx: shape.radius, hz: shape.radius }\n }\n const [sx, sy, sz] = shape.size\n const x = new Float64Array(GRID * GRID)\n const z = new Float64Array(GRID * GRID)\n let n = 0\n for (let i = 0; i < GRID; i++) {\n for (let j = 0; j < GRID; j++) {\n x[n] = ((i + 0.5) / GRID - 0.5) * sx\n z[n] = ((j + 0.5) / GRID - 0.5) * sz\n n++\n }\n }\n return { x, z, h: sy, kind: 'box', r: Math.hypot(sx, sz) / 2, hx: sx / 2, hz: sz / 2 }\n}\n\n/** Range of `u · p` over the container: below `lo` it is empty, above `hi` it is full. */\nexport const planeRange = (c: Columns, u: Vec3): [number, number] => {\n const horizontal = c.kind === 'cylinder' ? c.r * Math.hypot(u[0], u[2]) : Math.abs(u[0]) * c.hx + Math.abs(u[2]) * c.hz\n const top = u[1] * c.h\n return [-horizontal + Math.min(0, top), horizontal + Math.max(0, top)]\n}\n\n/** Fraction (0..1) of the inner volume with `u · p <= d` (the liquid for a surface plane with normal `u` at offset `d`). */\nexport const planeFraction = (c: Columns, u: Vec3, d: number): number => {\n const uy = u[1]\n let total = 0\n const n = c.x.length\n for (let i = 0; i < n; i++) {\n const s0 = u[0] * c.x[i] + u[2] * c.z[i]\n let len: number\n if (uy > 1e-6) len = clamp((d - s0) / uy, 0, c.h)\n else if (uy < -1e-6) len = c.h - clamp((d - s0) / uy, 0, c.h)\n else len = s0 <= d ? c.h : 0\n total += len\n }\n return total / (n * c.h)\n}\n\n/** Plane offset `d` along unit normal `u` for which the liquid takes the volume fraction `f`. */\nexport const levelFor = (c: Columns, u: Vec3, f: number): number => {\n const [lo0, hi0] = planeRange(c, u)\n if (f <= 0) return lo0\n if (f >= 1) return hi0\n if (Math.abs(u[0]) + Math.abs(u[2]) < 1e-9) {\n return u[1] > 0 ? f * c.h * u[1] : u[1] * c.h - f * c.h * u[1]\n }\n let lo = lo0\n let hi = hi0\n for (let i = 0; i < 28; i++) {\n const mid = (lo + hi) / 2\n if (planeFraction(c, u, mid) < f) lo = mid\n else hi = mid\n }\n return (lo + hi) / 2\n}\n\n/** Radius of the lip circle (the opening); a closed spec (`opening` 0) uses half the container width once opened. */\nexport const lipRadius = (shape: Shape, opening: number): number => {\n const limit = shape.kind === 'cylinder' ? shape.radius : Math.min(shape.size[0], shape.size[2]) / 2\n return opening > 0 ? Math.min(opening, limit) : limit * 0.5\n}\n\nexport const shapeHeight = (shape: Shape): number => (shape.kind === 'cylinder' ? shape.height : shape.size[1])\n\nexport interface Lip {\n /** Lowest lip point along the surface normal, container space. */\n point: Vec3\n /** Centre of the lip circle, container space. */\n center: Vec3\n /** Outward direction (container space, horizontal in the container frame) the liquid leaves along, or null when upright. */\n outward: Vec3 | null\n /** `u · point`: the surface offset above which the liquid spills. */\n level: number\n}\n\nexport const lipOf = (shape: Shape, opening: number, u: Vec3): Lip => {\n const h = shapeHeight(shape)\n const ro = lipRadius(shape, opening)\n const center: Vec3 = [0, h, 0]\n const uh = Math.hypot(u[0], u[2])\n if (uh < 1e-6) return { point: center, center, outward: null, level: u[1] * h }\n const out: Vec3 = normalize([-u[0], 0, -u[2]])\n const point: Vec3 = [out[0] * ro, h, out[2] * ro]\n return { point, center, outward: out, level: u[0] * point[0] + u[1] * point[1] + u[2] * point[2] }\n}\n","import type { LiquidDef, StainState, Vec3 } from './types'\nimport { Rng, add, clamp, cross, dot, length, normalize, scale, sub } from './math'\n\n/** Number of radial samples of the flow front. */\nexport const SAMPLES = 16\n\nconst COS: number[] = []\nconst SIN: number[] = []\nfor (let i = 0; i < SAMPLES; i++) {\n COS.push(Math.cos((i / SAMPLES) * Math.PI * 2))\n SIN.push(Math.sin((i / SAMPLES) * Math.PI * 2))\n}\n\n/** Orthonormal frame of a surface: `normal`, `t1` (tangent) and `t2 = normal × t1`. */\nexport interface Basis {\n normal: Vec3\n t1: Vec3\n t2: Vec3\n}\n\nexport const makeBasis = (normalIn: Vec3, tangentIn?: Vec3): Basis => {\n const normal = normalize(normalIn)\n const ref: Vec3 = tangentIn && length(tangentIn) > 1e-6 ? tangentIn : Math.abs(normal[1]) > 0.9 ? [1, 0, 0] : [0, 1, 0]\n const t1 = normalize(sub(ref, scale(normal, dot(ref, normal))), [1, 0, 0])\n return { normal, t1, t2: cross(normal, t1) }\n}\n\n/** What a stain needs to know about the surface it lies on. */\nexport interface StainSurface extends Basis {\n point: Vec3\n half: [number, number]\n porosity: number\n}\n\nexport interface StainInt extends StainState {\n normal: Vec3\n tangent: Vec3\n noise: number[]\n /** Seconds since the last inflow. */\n sinceInflow: number\n /** Drying progress 0..1. */\n dry: number\n mark: boolean\n age: number\n}\n\n/** Direction (world space) of radial sample `i` on a surface frame. */\nexport const sampleDir = (b: Basis, i: number): Vec3 => [\n b.t1[0] * COS[i] + b.t2[0] * SIN[i],\n b.t1[1] * COS[i] + b.t2[1] * SIN[i],\n b.t1[2] * COS[i] + b.t2[2] * SIN[i],\n]\n\nexport const stainArea = (front: number[]): number => {\n let a = 0\n for (let i = 0; i < SAMPLES; i++) a += front[i] * front[(i + 1) % SAMPLES]\n return 0.5 * Math.sin((Math.PI * 2) / SAMPLES) * a\n}\n\nexport const meanRadius = (front: number[]): number => front.reduce((s, v) => s + v, 0) / front.length\n\n/** Thickness (m) a puddle of this liquid cannot get thinner than: 1 mm for water, thicker for viscous liquids. */\nexport const minFilm = (def: LiquidDef): number => 0.001 * (1 + 0.8 * Math.log10(Math.max(1, def.viscosity)))\n\n/** Per-direction growth multipliers: low harmonics plus smoothed seeded noise, so the outline is never a disc. */\nexport const makeNoise = (rng: Rng): number[] => {\n const amp = [0.12 + rng.next() * 0.18, 0.08 + rng.next() * 0.14, 0.05 + rng.next() * 0.08]\n const phase = [rng.next() * Math.PI * 2, rng.next() * Math.PI * 2, rng.next() * Math.PI * 2]\n const raw: number[] = []\n for (let i = 0; i < SAMPLES; i++) {\n const a = (i / SAMPLES) * Math.PI * 2\n let v = 1 + (rng.next() - 0.5) * 0.3\n for (let k = 0; k < 3; k++) v += amp[k] * Math.sin((k + 2) * a + phase[k])\n raw.push(v)\n }\n const out = raw.map((v, i) => 0.25 * raw[(i + SAMPLES - 1) % SAMPLES] + 0.5 * v + 0.25 * raw[(i + 1) % SAMPLES])\n return out.map((v) => clamp(v, 0.45, 1.7))\n}\n\n/** Horizontal run direction on a surface (downhill, unit, in the plane) and the slope strength 0..1 (sin of the angle). */\nexport const downhill = (normal: Vec3): { dir: Vec3; slope: number } => {\n const d = add([0, -1, 0], scale(normal, normal[1]))\n const slope = length(d)\n return slope < 1e-6 ? { dir: [0, 0, 0], slope: 0 } : { dir: scale(d, 1 / slope), slope }\n}\n\n/** Creates a stain at `center` (on the surface) with a small starting front. */\nexport const createStain = (\n id: string,\n surfaceId: string,\n def: LiquidDef,\n basis: Basis,\n center: Vec3,\n liters: number,\n rng: Rng,\n): StainInt => {\n const r0 = clamp(Math.sqrt(liters / 1000 / minFilm(def) / Math.PI) * 0.4, 0.012, 0.12)\n return {\n id,\n surface: surfaceId,\n liquid: def.id,\n center: [center[0], center[1], center[2]],\n front: new Array(SAMPLES).fill(r0),\n liters,\n soaked: 0,\n wetness: 1,\n wetRadius: 1,\n normal: basis.normal,\n tangent: basis.t1,\n noise: makeNoise(rng),\n sinceInflow: 0,\n dry: 0,\n mark: false,\n age: 0,\n }\n}\n\n/** Adds liquid to a stain (rewets a dried mark). `velocity` makes the front stretch the way the liquid came in. */\nexport const addInflow = (s: StainInt, liters: number, velocity: Vec3 | null): void => {\n s.liters += liters\n s.sinceInflow = 0\n if (s.dry > 0 || s.mark) {\n s.dry = 0\n s.mark = false\n s.wetness = 1\n s.wetRadius = 1\n }\n if (velocity) {\n const n = s.normal\n const tangential = sub(velocity, scale(n, dot(velocity, n)))\n if (length(tangential) > 0.2) s.flow = normalize(tangential)\n }\n}\n\n/** Moves the centre towards an inflow point without moving the outline. */\nexport const nudgeCenter = (s: StainInt, point: Vec3, weight: number, basis: Basis): void => {\n const delta = scale(sub(point, s.center), weight)\n if (length(delta) < 1e-6) return\n s.center = add(s.center, delta)\n for (let i = 0; i < SAMPLES; i++) s.front[i] = Math.max(0.008, s.front[i] - dot(delta, sampleDir(basis, i)))\n}\n\nexport type StainStatus = 'alive' | 'dried_mark' | 'dried_gone'\n\n/** Largest radius along each sample direction before the centre's surface rectangle ends. */\nconst edgeLimit = (s: StainInt, surface: StainSurface, i: number): number => {\n if (!Number.isFinite(surface.half[0]) && !Number.isFinite(surface.half[1])) return Infinity\n const rel = sub(s.center, surface.point)\n const cx = dot(rel, surface.t1)\n const cy = dot(rel, surface.t2)\n let limit = Infinity\n if (COS[i] > 1e-6) limit = Math.min(limit, (surface.half[0] - cx) / COS[i])\n else if (COS[i] < -1e-6) limit = Math.min(limit, (-surface.half[0] - cx) / COS[i])\n if (SIN[i] > 1e-6) limit = Math.min(limit, (surface.half[1] - cy) / SIN[i])\n else if (SIN[i] < -1e-6) limit = Math.min(limit, (-surface.half[1] - cy) / SIN[i])\n return Math.max(0.01, limit)\n}\n\n/**\n * Advances a stain: the front grows (faster downhill, with seeded noise, tapering as the area approaches what the liquid can cover at its\n * minimum film thickness), porous surfaces soak the pool, and once the inflow stopped the stain dries from the edge in.\n */\nexport const stepStain = (s: StainInt, def: LiquidDef, surface: StainSurface, ambientC: number, dt: number): StainStatus => {\n s.age += dt\n s.sinceInflow += dt\n const basis: Basis = surface\n const { dir: down, slope } = downhill(s.normal)\n\n // Soak into porous surfaces.\n if (s.liters > 0 && surface.porosity > 0) {\n const soak = Math.min(s.liters, surface.porosity * 0.2 * stainArea(s.front) * dt)\n s.liters -= soak\n s.soaked += soak\n }\n\n // Grow the flow front.\n if (s.liters > 1e-7 && s.wetness > 0) {\n const area = stainArea(s.front)\n const cover = s.liters / 1000 / (minFilm(def) * (1 - 0.7 * slope))\n if (area < cover) {\n const taper = clamp(1 - area / cover, 0, 1)\n const v0 = (0.02 + 0.2 * Math.sqrt(s.liters)) / Math.sqrt(Math.max(1, def.viscosity))\n const delta: number[] = []\n for (let i = 0; i < SAMPLES; i++) {\n let m = s.noise[i]\n if (s.flow) m *= 1 + 0.5 * Math.max(0, dot(sampleDir(basis, i), s.flow))\n if (slope > 0.02) {\n const a = dot(sampleDir(basis, i), down)\n m *= a > 0 ? 1 + 14 * slope * a : Math.max(0.1, 1 + 2.5 * slope * a)\n }\n delta.push(Math.max(0, Math.min(s.front[i] + v0 * m * taper * dt, edgeLimit(s, surface, i)) - s.front[i]))\n }\n // Never overshoot the area the liquid can cover: scale the growth (not the outline) down to fit.\n let lambda = 1\n const apply = (k: number) => s.front.map((r, i) => r + delta[i] * k)\n if (stainArea(apply(1)) > cover) {\n let lo = 0\n let hi = 1\n for (let i = 0; i < 14; i++) {\n const mid = (lo + hi) / 2\n if (stainArea(apply(mid)) > cover) hi = mid\n else lo = mid\n }\n lambda = lo\n }\n s.front = apply(lambda)\n }\n }\n\n // Dry from the edge in once the inflow stopped.\n if (s.sinceInflow > 1 && def.dryTime > 0 && s.dry < 1) {\n const warm = Math.pow(2, (ambientC - 20) / 12)\n const ds = (dt * warm * (1 + 0.5 * surface.porosity)) / def.dryTime\n const before = s.dry\n s.dry = Math.min(1, s.dry + ds)\n if (before < 1) s.liters *= 1 - (s.dry - before) / (1 - before)\n s.wetness = 1 - s.dry\n s.wetRadius = Math.sqrt(Math.max(0, 1 - s.dry))\n if (s.dry >= 1) {\n s.liters = 0\n s.wetness = 0\n s.wetRadius = 0\n if (def.leavesMark) {\n s.mark = true\n return 'dried_mark'\n }\n return 'dried_gone'\n }\n }\n return 'alive'\n}\n\n/** Unit direction (world space) of front sample `i` of a stain, from its `normal` and `tangent`. */\nexport const stainDirection = (s: Pick<StainState, 'normal' | 'tangent'>, i: number): Vec3 => sampleDir(makeBasis(s.normal ?? [0, 1, 0], s.tangent), i)\n","import { LIQUIDS } from './liquids'\nimport { GROUND_ID, MAX_FLIGHT, ballistic, trace } from './landing'\nimport type { Hit, LandingSurface } from './landing'\nimport { describeEvent, resolveStrings } from './locale'\nimport { GRAVITY, Rng, add, clamp, copy3, cross, distance, dot, length, normalize, quatConjugate, quatRotate, scale } from './math'\nimport { levelFor, lipOf, lipRadius, makeColumns, planeFraction, planeRange, shapeHeight } from './shapes'\nimport type { Columns } from './shapes'\nimport { addInflow, createStain, makeBasis, meanRadius, nudgeCenter, stepStain } from './stains'\nimport type { Basis, StainInt, StainSurface } from './stains'\nimport type {\n ContainerSpec,\n ContainerState,\n FluidEvent,\n FluidOptions,\n FluidWorld,\n LiquidDef,\n LiquidId,\n Quat,\n StainState,\n StreamCause,\n StreamState,\n SurfaceSpec,\n Vec3,\n} from './types'\n\ninterface Hole {\n point: Vec3\n normal: Vec3\n diameter: number\n}\n\ninterface ContainerInt {\n id: string\n spec: ContainerSpec\n cols: Columns\n liquid: LiquidId | null\n liters: number\n temperature: number\n closed: boolean\n position: Vec3\n rotation: Quat\n velocity: Vec3\n prevVelocity: Vec3\n hasPrev: boolean\n holes: Hole[]\n /** Slosh of the liquid surface: tilt offsets and their rates (x, z, vx, vz). */\n slosh: [number, number, number, number]\n /** Surface normal in world space (up plus slosh) and in container space, and the surface offset along it. */\n normal: Vec3\n u: Vec3\n d: number\n steaming: boolean\n outflow: number\n /** Stream key (`spill`, `pour`, `leak:<n>`) to stream id. */\n streams: Record<string, string>\n pourRate: number\n pourTTL: number\n pourVisual: boolean\n lastMix: number\n}\n\ninterface TapInt {\n id: string\n position: Vec3\n direction: Vec3\n liquid: LiquidId\n rate: number\n temperature: number\n on: boolean\n stream?: string\n}\n\ninterface StreamInt extends StreamState {\n key: string\n lastSplash: number\n visualOnly: boolean\n temperature: number\n}\n\ninterface SurfaceInt extends SurfaceSpec, StainSurface {}\n\ninterface Snapshot {\n version: 1\n time: number\n rng: number\n counters: { container: number; stream: number; stain: number; tap?: number }\n taps?: TapInt[]\n containers: Array<Omit<ContainerInt, 'cols'>>\n surfaces: SurfaceSpec[]\n streams: StreamInt[]\n stains: StainInt[]\n}\n\nconst GROUND_POROSITY = 0.2\nconst MAX_STEP = 0.1\nconst MAX_SUBSTEPS = 50\nconst MAX_HOLES = 32\n\nconst clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T\n\nexport function createFluids(options: FluidOptions = {}): FluidWorld {\n const strings = resolveStrings(options.locale)\n const liquids = new Map<LiquidId, LiquidDef>()\n for (const def of LIQUIDS) liquids.set(def.id, def)\n for (const def of options.liquids ?? []) liquids.set(def.id, def)\n const ambientC = options.ambientC ?? 20\n const gravity = options.gravity ?? GRAVITY\n const groundAt = options.groundAt ?? (() => 0)\n const mergeDistance = options.mergeDistance ?? 0.15\n const maxStains = options.maxStains ?? 64\n\n let rng = new Rng(options.seed ?? 1)\n let time = 0\n const counters = { container: 0, stream: 0, stain: 0, tap: 0 }\n let containers = new Map<string, ContainerInt>()\n let surfaces = new Map<string, SurfaceInt>()\n let streams = new Map<string, StreamInt>()\n let taps = new Map<string, TapInt>()\n let stains = new Map<string, StainInt>()\n const listeners = new Map<string, Set<(e: FluidEvent) => void>>()\n let queue: FluidEvent[] = []\n /** Only events produced by `update` are queued and returned by it; direct calls just notify the listeners. */\n let updating = false\n\n const groundSurface: StainSurface = {\n normal: [0, 1, 0],\n t1: [1, 0, 0],\n t2: [0, 0, -1],\n point: [0, 0, 0],\n half: [Infinity, Infinity],\n porosity: GROUND_POROSITY,\n }\n\n // ---- events ----\n\n const emit = (e: FluidEvent): void => {\n if (updating) queue.push(e)\n options.onEvent?.(e)\n listeners.get(e.type)?.forEach((fn) => fn(e))\n listeners.get('*')?.forEach((fn) => fn(e))\n }\n\n // ---- helpers ----\n\n const toWorld = (c: ContainerInt, p: Vec3): Vec3 => add(c.position, quatRotate(c.rotation, p))\n const defOf = (c: ContainerInt): LiquidDef | null => (c.liquid ? (liquids.get(c.liquid) ?? null) : null)\n const viscScale = (def: LiquidDef): number => 1 / Math.sqrt(Math.max(1, def.viscosity))\n\n const refreshLevel = (c: ContainerInt): void => {\n c.normal = normalize([c.slosh[0], 1, c.slosh[1]])\n c.u = normalize(quatRotate(quatConjugate(c.rotation), c.normal))\n c.d = levelFor(c.cols, c.u, c.spec.capacity > 0 ? c.liters / c.spec.capacity : 0)\n }\n\n const surfaceFor = (id: string): StainSurface => (id === GROUND_ID ? groundSurface : (surfaces.get(id) ?? groundSurface))\n\n const landingSurfaces = (): LandingSurface[] => {\n const list: LandingSurface[] = []\n for (const [id, s] of surfaces) list.push({ id, normal: s.normal, t1: s.t1, t2: s.t2, point: s.point, half: s.half })\n return list\n }\n\n const stopStream = (id: string): void => {\n const s = streams.get(id)\n if (!s) return\n streams.delete(id)\n const c = s.container ? containers.get(s.container) : undefined\n if (c) delete c.streams[s.key]\n const tap = s.tap ? taps.get(s.tap) : undefined\n if (tap && tap.stream === id) tap.stream = undefined\n emit({ type: 'stream_end', stream: id })\n }\n\n const endStreamKey = (c: ContainerInt, key: string): void => {\n const id = c.streams[key]\n if (id) stopStream(id)\n }\n\n /** Create or update the stream under `key` of a container; creating one emits the matching cause event. */\n const syncStream = (\n c: ContainerInt,\n key: string,\n cause: StreamCause,\n rate: number,\n origin: Vec3,\n velocity: Vec3,\n visualOnly = false,\n ): void => {\n if (rate < 1e-5 || !c.liquid) {\n endStreamKey(c, key)\n return\n }\n const existing = c.streams[key] ? streams.get(c.streams[key]) : undefined\n if (existing) {\n existing.rate = rate\n existing.origin = origin\n existing.velocity = velocity\n existing.thin = rate < 0.04\n existing.visualOnly = visualOnly\n existing.temperature = c.temperature\n return\n }\n const id = `stream_${++counters.stream}`\n const stream: StreamInt = {\n id,\n liquid: c.liquid,\n cause,\n container: c.id,\n origin,\n velocity,\n rate,\n landing: null,\n thin: rate < 0.04,\n flightTime: MAX_FLIGHT,\n key,\n lastSplash: -1,\n visualOnly,\n temperature: c.temperature,\n }\n streams.set(id, stream)\n c.streams[key] = id\n if (cause === 'leak') emit({ type: 'leak', container: c.id, liquid: c.liquid, rate, stream: id, point: copy3(origin) })\n else if (cause === 'spill') emit({ type: 'spill', container: c.id, liquid: c.liquid, rate, stream: id })\n else emit({ type: 'pour', container: c.id, liquid: c.liquid, rate, stream: id })\n }\n\n /** Speed (m/s) at which a tap's liquid leaves the nozzle. */\n const TAP_SPEED = 1.2\n\n /** Create or update the `tap` stream of a tap that is on; end it when it is off. */\n const syncTap = (t: TapInt): void => {\n if (!t.on || t.rate < 1e-5 || !liquids.has(t.liquid)) {\n if (t.stream) stopStream(t.stream)\n return\n }\n const velocity: Vec3 = [t.direction[0] * TAP_SPEED, t.direction[1] * TAP_SPEED, t.direction[2] * TAP_SPEED]\n const existing = t.stream ? streams.get(t.stream) : undefined\n if (existing) {\n existing.rate = t.rate\n existing.thin = t.rate < 0.04\n existing.temperature = t.temperature\n existing.velocity = velocity\n return\n }\n const id = `stream_${++counters.stream}`\n streams.set(id, {\n id,\n liquid: t.liquid,\n cause: 'tap',\n tap: t.id,\n origin: copy3(t.position),\n velocity,\n rate: t.rate,\n landing: null,\n thin: t.rate < 0.04,\n flightTime: MAX_FLIGHT,\n key: `tap:${t.id}`,\n lastSplash: -1,\n visualOnly: false,\n temperature: t.temperature,\n })\n t.stream = id\n }\n\n /** Where and how fast the liquid leaves over the lowest lip point. */\n const lipFlow = (c: ContainerInt, head: number): { origin: Vec3; velocity: Vec3 } => {\n const lip = lipOf(c.spec.shape, c.spec.opening, c.u)\n const origin = toWorld(c, lip.point)\n let dir: Vec3 = [1, 0, 0]\n const horizontal = (v: Vec3): Vec3 | null => (Math.hypot(v[0], v[2]) > 1e-3 ? normalize([v[0], 0, v[2]]) : null)\n if (lip.outward) dir = horizontal(quatRotate(c.rotation, lip.outward)) ?? horizontal(quatRotate(c.rotation, [1, 0, 0])) ?? dir\n else dir = horizontal(quatRotate(c.rotation, [1, 0, 0])) ?? dir\n const speed = clamp(0.3 + 0.5 * Math.sqrt(2 * gravity * Math.max(0, head)), 0.2, 2.5)\n return { origin, velocity: [dir[0] * speed, -0.1, dir[2] * speed] }\n }\n\n const removeLiters = (c: ContainerInt, amount: number): void => {\n c.liters = Math.max(0, c.liters - amount)\n }\n\n /** Clear a container that ran out and emit `empty` once. */\n const settle = (c: ContainerInt): void => {\n if (c.liquid && c.liters <= 1e-6) {\n c.liters = 0\n c.liquid = null\n emit({ type: 'empty', container: c.id })\n }\n }\n\n const fillInternal = (c: ContainerInt, liquid: LiquidId, liters: number, temperature: number | undefined, announce: boolean): number => {\n if (!(liters > 0) || !liquids.has(liquid)) return 0\n const room = c.spec.capacity - c.liters\n const accepted = Math.min(liters, Math.max(0, room))\n if (accepted <= 0) return 0\n const t = temperature ?? ambientC\n if (c.liquid && c.liquid !== liquid && c.liters > 1e-6) {\n const incomingWins = accepted > c.liters\n const into = incomingWins ? liquid : c.liquid\n const from = incomingWins ? c.liquid : liquid\n if (announce || time - c.lastMix > 0.5) {\n c.lastMix = time\n emit({ type: 'mix', container: c.id, from, into })\n }\n c.liquid = into\n } else if (!c.liquid || c.liters <= 1e-6) {\n c.liquid = liquid\n }\n c.temperature = c.liters > 1e-6 ? (c.temperature * c.liters + t * accepted) / (c.liters + accepted) : t\n c.liters += accepted\n if (announce) emit({ type: 'fill', container: c.id, liquid, liters: accepted })\n return accepted\n }\n\n // ---- stains ----\n\n const stainAt = (surfaceId: string, liquid: LiquidId, point: Vec3): StainInt | null => {\n let best: StainInt | null = null\n let bestD = Infinity\n for (const s of stains.values()) {\n if (s.surface !== surfaceId || s.liquid !== liquid) continue\n const d = distance(point, s.center)\n if (d < mergeDistance + 0.5 * meanRadius(s.front) && d < bestD) {\n best = s\n bestD = d\n }\n }\n return best\n }\n\n const evictStain = (): void => {\n let victim: StainInt | null = null\n for (const s of stains.values()) {\n const rank = (x: StainInt) => (x.wetness <= 0 ? 0 : 1) * 1e9 - x.age\n if (!victim || rank(s) < rank(victim)) victim = s\n }\n if (victim) stains.delete(victim.id)\n }\n\n const deposit = (liquid: LiquidId, hit: Pick<Hit, 'point' | 'surface' | 'normal'>, liters: number, velocity: Vec3 | null): void => {\n const def = liquids.get(liquid)\n if (!def || liters <= 0) return\n let s = stainAt(hit.surface, liquid, hit.point)\n if (s) {\n const basis: Basis = { normal: s.normal, t1: s.tangent, t2: cross(s.normal, s.tangent) }\n nudgeCenter(s, hit.point, clamp((liters / (s.liters + liters)) * 0.5, 0, 0.5), basis)\n addInflow(s, liters, velocity)\n return\n }\n while (stains.size >= maxStains) evictStain()\n const surface = surfaceFor(hit.surface)\n const basis: Basis = hit.surface === GROUND_ID ? makeBasis(hit.normal) : surface\n s = createStain(`stain_${++counters.stain}`, hit.surface, def, basis, hit.point, liters, rng)\n if (velocity) addInflow(s, 0, velocity)\n s.sinceInflow = 0\n stains.set(s.id, s)\n emit({ type: 'stain', stain: s.id, surface: hit.surface, liquid, point: copy3(hit.point) })\n }\n\n // ---- streams ----\n\n const lipCenterWorld = (c: ContainerInt): Vec3 => toWorld(c, [0, shapeHeight(c.spec.shape), 0])\n\n /** A container whose opening the jet passes through before it lands (pouring into a cup). */\n const catcher = (s: StreamInt, landingT: number): { container: ContainerInt; t: number; point: Vec3 } | null => {\n let best: { container: ContainerInt; t: number; point: Vec3 } | null = null\n for (const oc of containers.values()) {\n if (oc.id === s.container || oc.closed || oc.liters >= oc.spec.capacity) continue\n const lip = lipCenterWorld(oc)\n const disc = s.velocity[1] * s.velocity[1] - 2 * gravity * (lip[1] - s.origin[1])\n if (disc < 0) continue\n const t = (s.velocity[1] + Math.sqrt(disc)) / gravity\n if (t <= 0 || t >= landingT || (best && t >= best.t)) continue\n const p = ballistic(s.origin, s.velocity, gravity, t)\n if (Math.hypot(p[0] - lip[0], p[2] - lip[2]) <= lipRadius(oc.spec.shape, oc.spec.opening) + 0.01) best = { container: oc, t, point: p }\n }\n return best\n }\n\n const stepStream = (s: StreamInt, dt: number): void => {\n const hit = trace(landingSurfaces(), groundAt, s.origin, s.velocity, gravity)\n const caught = catcher(s, hit ? hit.t : MAX_FLIGHT)\n if (caught) {\n s.landing = caught.point\n s.flightTime = caught.t\n if (!s.visualOnly) fillInternal(caught.container, s.liquid, s.rate * dt, s.temperature, false)\n return\n }\n s.landing = hit ? copy3(hit.point) : null\n s.flightTime = hit ? hit.t : MAX_FLIGHT\n if (!hit || s.visualOnly) return\n const speed = length(hit.velocity)\n if (time - s.lastSplash >= 0.12) {\n s.lastSplash = time\n emit({ type: 'splash', liquid: s.liquid, point: copy3(hit.point), intensity: clamp(0.15 + s.rate * 1.2 + speed * 0.05, 0, 1) })\n }\n deposit(s.liquid, hit, s.rate * dt, hit.velocity)\n }\n\n // ---- containers ----\n\n const spillRate = (c: ContainerInt, def: LiquidDef, h: number): number => {\n const ro = lipRadius(c.spec.shape, c.spec.opening)\n const area = 2 * ro * Math.min(h, ro)\n return Math.max(0.6 * area * Math.sqrt(2 * gravity * h) * 1000 * viscScale(def), 0.004)\n }\n\n const steamThreshold = (def: LiquidDef): number => def.steamC ?? (def.drink && def.drink.thirst > 0 ? 60 : Infinity)\n\n const stepContainer = (c: ContainerInt, dt: number): void => {\n // Slosh: the surface tilts towards the horizontal acceleration and settles with a damped spring.\n if (c.hasPrev && dt > 0) {\n const ax = (c.velocity[0] - c.prevVelocity[0]) / dt\n const az = (c.velocity[2] - c.prevVelocity[2]) / dt\n const tx = clamp(ax / gravity, -0.35, 0.35)\n const tz = clamp(az / gravity, -0.35, 0.35)\n const w = 9\n const z = 0.12\n c.slosh[2] += (w * w * (tx - c.slosh[0]) - 2 * z * w * c.slosh[2]) * dt\n c.slosh[3] += (w * w * (tz - c.slosh[1]) - 2 * z * w * c.slosh[3]) * dt\n } else {\n c.slosh[2] *= 0.5\n c.slosh[3] *= 0.5\n }\n c.slosh[0] += c.slosh[2] * dt\n c.slosh[1] += c.slosh[3] * dt\n if (Math.abs(c.slosh[0]) + Math.abs(c.slosh[1]) + Math.abs(c.slosh[2]) + Math.abs(c.slosh[3]) < 1e-5) c.slosh = [0, 0, 0, 0]\n c.prevVelocity = copy3(c.velocity)\n c.hasPrev = true\n\n // Cooling towards ambient (insulated containers 10× slower).\n if (c.temperature !== ambientC) {\n const tau = (600 + 2400 * c.liters) * (c.spec.insulated ? 10 : 1)\n c.temperature = ambientC + (c.temperature - ambientC) * Math.exp(-dt / tau)\n if (Math.abs(c.temperature - ambientC) < 0.01) c.temperature = ambientC\n }\n\n refreshLevel(c)\n const def = defOf(c)\n\n const steaming = !!def && c.liters > 1e-6 && !c.closed && c.temperature > steamThreshold(def)\n if (steaming !== c.steaming) {\n c.steaming = steaming\n emit({ type: 'steam', container: c.id, on: steaming })\n }\n\n c.outflow = 0\n if (!def || c.liters <= 1e-6) {\n for (const key of Object.keys(c.streams)) endStreamKey(c, key)\n c.pourTTL = 0\n settle(c)\n return\n }\n\n // Pour (driven by `pour()`), else spill over the lip when the tilted surface rises above it.\n if (c.pourTTL > 0) {\n c.pourTTL -= dt\n if (c.pourTTL > 0) {\n const flow = lipFlow(c, 0.05)\n syncStream(c, 'pour', 'pour', c.pourRate, flow.origin, flow.velocity, c.pourVisual)\n c.outflow += c.pourRate\n } else endStreamKey(c, 'pour')\n endStreamKey(c, 'spill')\n } else {\n endStreamKey(c, 'pour')\n const lip = lipOf(c.spec.shape, c.spec.opening, c.u)\n const h = c.d - lip.level\n let handled = false\n if (!c.closed && h > 1e-5) {\n const excess = c.liters - c.spec.capacity * planeFraction(c.cols, c.u, lip.level)\n if (excess > 1e-6) {\n const rate = Math.min(spillRate(c, def, h), excess / Math.max(dt, 1e-6))\n removeLiters(c, Math.min(rate * dt, excess, c.liters))\n const flow = lipFlow(c, h)\n syncStream(c, 'spill', 'spill', rate, flow.origin, flow.velocity)\n c.outflow += rate\n handled = true\n }\n }\n if (!handled) endStreamKey(c, 'spill')\n }\n\n // Holes leak by Torricelli with the head above each hole.\n for (let i = 0; i < c.holes.length; i++) {\n const hole = c.holes[i]\n const key = `leak:${i}`\n const hh = dot(c.u, hole.point)\n const head = c.d - hh\n if (c.liters > 1e-6 && head > 1e-4) {\n const area = Math.PI * (hole.diameter / 2) ** 2\n const natural = 0.6 * area * Math.sqrt(2 * gravity * head) * 1000 * viscScale(def)\n const avail = c.liters - c.spec.capacity * planeFraction(c.cols, c.u, hh)\n if (avail > 1e-7) {\n const rate = Math.min(natural, avail / Math.max(dt, 1e-6))\n removeLiters(c, Math.min(rate * dt, avail))\n const nW = normalize(quatRotate(c.rotation, hole.normal))\n const v = Math.min(3, 0.6 * Math.sqrt(2 * gravity * head))\n syncStream(c, key, 'leak', rate, toWorld(c, hole.point), [nW[0] * v, nW[1] * v - 0.05, nW[2] * v])\n c.outflow += rate\n continue\n }\n }\n endStreamKey(c, key)\n }\n if (c.holes.length > 0 || c.outflow > 0) refreshLevel(c)\n settle(c)\n }\n\n const step = (dt: number): void => {\n time += dt\n for (const c of [...containers.values()]) stepContainer(c, dt)\n for (const s of [...streams.values()]) stepStream(s, dt)\n for (const st of [...stains.values()]) {\n const def = liquids.get(st.liquid)\n if (!def) continue\n const status = stepStain(st, def, surfaceFor(st.surface), ambientC, dt)\n if (status === 'dried_mark') emit({ type: 'dried', stain: st.id, mark: true })\n else if (status === 'dried_gone') {\n stains.delete(st.id)\n emit({ type: 'dried', stain: st.id, mark: false })\n }\n }\n }\n\n // ---- public state views ----\n\n const stateOf = (c: ContainerInt): ContainerState => ({\n id: c.id,\n liquid: c.liquid,\n liters: c.liters,\n capacity: c.spec.capacity,\n temperature: c.temperature,\n closed: c.closed,\n surfaceHeight: c.liters > 0 ? c.d : 0,\n surfacePoint: add(c.position, scale(c.normal, c.liters > 0 ? c.d : 0)),\n surfaceNormal: copy3(c.normal),\n holes: c.holes.map((h) => ({ point: copy3(h.point), normal: copy3(h.normal), diameter: h.diameter })),\n outflow: c.outflow,\n steaming: c.steaming,\n shape: c.spec.shape,\n lipRadius: lipRadius(c.spec.shape, c.spec.opening),\n position: copy3(c.position),\n rotation: [...c.rotation] as Quat,\n })\n\n const streamView = (s: StreamInt): StreamState => ({\n id: s.id,\n liquid: s.liquid,\n cause: s.cause,\n container: s.container,\n tap: s.tap,\n origin: copy3(s.origin),\n velocity: copy3(s.velocity),\n rate: s.rate,\n landing: s.landing ? copy3(s.landing) : null,\n thin: s.thin,\n flightTime: s.flightTime,\n })\n\n const stainView = (s: StainInt): StainState => ({\n id: s.id,\n surface: s.surface,\n liquid: s.liquid,\n center: copy3(s.center),\n front: [...s.front],\n liters: s.liters,\n soaked: s.soaked,\n wetness: s.wetness,\n wetRadius: s.wetRadius,\n normal: copy3(s.normal),\n tangent: copy3(s.tangent),\n flow: s.flow ? copy3(s.flow) : undefined,\n mark: s.mark,\n age: s.age,\n })\n\n // ---- world API ----\n\n const world: FluidWorld = {\n liquids,\n gravity,\n describe: (event) => describeEvent(strings, event),\n\n addContainer(spec, opts = {}) {\n const id = opts.id ?? `container_${++counters.container}`\n if (containers.has(id)) world.removeContainer(id)\n const liquid = spec.liquid && liquids.has(spec.liquid) ? spec.liquid : null\n const liters = liquid ? clamp(spec.liters ?? 0, 0, spec.capacity) : 0\n const c: ContainerInt = {\n id,\n spec: clone(spec),\n cols: makeColumns(spec.shape),\n liquid: liters > 0 ? liquid : null,\n liters,\n temperature: spec.temperature ?? ambientC,\n closed: spec.closed ?? spec.opening <= 0,\n position: copy3(opts.position ?? [0, 0, 0]),\n rotation: opts.rotation ? ([...opts.rotation] as Quat) : [0, 0, 0, 1],\n velocity: [0, 0, 0],\n prevVelocity: [0, 0, 0],\n hasPrev: false,\n holes: [],\n slosh: [0, 0, 0, 0],\n normal: [0, 1, 0],\n u: [0, 1, 0],\n d: 0,\n steaming: false,\n outflow: 0,\n streams: {},\n pourRate: 0,\n pourTTL: 0,\n pourVisual: false,\n lastMix: -10,\n }\n containers.set(id, c)\n refreshLevel(c)\n return id\n },\n\n addTap(spec) {\n const id = spec.id ?? `tap_${++counters.tap}`\n if (taps.has(id)) world.removeTap(id)\n const d = spec.direction ?? [0, -1, 0]\n const len = Math.hypot(d[0], d[1], d[2]) || 1\n const t: TapInt = {\n id,\n position: copy3(spec.position),\n direction: [d[0] / len, d[1] / len, d[2] / len],\n liquid: spec.liquid,\n rate: Math.max(0, spec.rate),\n temperature: spec.temperature ?? ambientC,\n on: false,\n }\n taps.set(id, t)\n if (spec.on ?? true) world.setTap(id, true)\n return id\n },\n\n setTap(id, on, rate) {\n const t = taps.get(id)\n if (!t) return\n if (rate !== undefined) t.rate = Math.max(0, rate)\n const changed = t.on !== on\n t.on = on\n syncTap(t)\n if (changed) emit({ type: 'tap', tap: id, on })\n },\n\n removeTap(id) {\n const t = taps.get(id)\n if (!t) return\n t.on = false\n syncTap(t)\n taps.delete(id)\n },\n\n taps: () => [...taps.values()].map((t) => ({ id: t.id, position: copy3(t.position), direction: copy3(t.direction), liquid: t.liquid, rate: t.rate, temperature: t.temperature, on: t.on, stream: t.stream })),\n\n removeContainer(id) {\n const c = containers.get(id)\n if (!c) return\n for (const key of Object.keys(c.streams)) endStreamKey(c, key)\n containers.delete(id)\n },\n\n setTransform(id, position, rotation, velocity) {\n const c = containers.get(id)\n if (!c) return\n c.position = copy3(position)\n c.rotation = [...rotation] as Quat\n if (velocity) c.velocity = copy3(velocity)\n refreshLevel(c)\n },\n\n open(id) {\n const c = containers.get(id)\n if (c) c.closed = false\n },\n\n close(id) {\n const c = containers.get(id)\n if (!c) return\n c.closed = true\n for (const key of ['pour', 'spill']) endStreamKey(c, key)\n c.pourTTL = 0\n },\n\n fill(id, liquid, liters, temperature) {\n const c = containers.get(id)\n if (!c || c.closed) return 0\n const accepted = fillInternal(c, liquid, liters, temperature, true)\n refreshLevel(c)\n return accepted\n },\n\n pour(id, dt, into) {\n const c = containers.get(id)\n const def = c ? defOf(c) : null\n if (!c || !def || c.closed || c.liters <= 1e-6 || !(dt > 0)) return 0\n refreshLevel(c)\n const lip = lipOf(c.spec.shape, c.spec.opening, c.u)\n const depth = Math.max(0, c.d - planeRange(c.cols, c.u)[0])\n const head = Math.max(c.d - lip.level, 0.02 + 0.5 * depth)\n const ro = lipRadius(c.spec.shape, c.spec.opening)\n const natural = 0.6 * Math.PI * ro * ro * Math.sqrt(2 * gravity * head) * 1000\n const rate = Math.min(natural, 0.15 + 12 * ro) * viscScale(def)\n const out = Math.min(rate * dt, c.liters)\n const target = into ? containers.get(into) : undefined\n let delivered = out\n if (target) {\n delivered = fillInternal(target, c.liquid as LiquidId, out, c.temperature, false)\n refreshLevel(target)\n const overflow = out - delivered\n if (overflow > 1e-6) {\n const flow = lipFlow(c, head)\n const hit = trace(landingSurfaces(), groundAt, flow.origin, flow.velocity, gravity)\n if (hit) deposit(c.liquid as LiquidId, hit, overflow, hit.velocity)\n }\n }\n removeLiters(c, out)\n c.pourRate = out / dt\n c.pourTTL = Math.max(0.12, dt * 1.5)\n c.pourVisual = !!target\n const flow = lipFlow(c, head)\n syncStream(c, 'pour', 'pour', c.pourRate, flow.origin, flow.velocity, c.pourVisual)\n refreshLevel(c)\n settle(c)\n return out\n },\n\n drink(id, liters) {\n const c = containers.get(id)\n const def = c ? defOf(c) : null\n if (!c || !def || c.closed || c.liters <= 1e-6 || !(liters > 0)) return null\n const amount = Math.min(liters, c.liters)\n removeLiters(c, amount)\n const event: Extract<FluidEvent, { type: 'drink' }> = {\n type: 'drink',\n container: c.id,\n liquid: def.id,\n liters: amount,\n temperature: c.temperature,\n thirst: (def.drink?.thirst ?? 0) * amount,\n effects: [...(def.drink?.effects ?? [])],\n burn: c.temperature > 65,\n }\n emit(event)\n refreshLevel(c)\n settle(c)\n return event\n },\n\n puncture(id, point, normal, diameter) {\n const c = containers.get(id)\n if (!c || !(diameter > 0) || c.holes.length >= MAX_HOLES) return\n c.holes.push({ point: copy3(point), normal: normalize(normal), diameter })\n },\n\n heat(id, deltaC) {\n const c = containers.get(id)\n if (c) c.temperature = clamp(c.temperature + deltaC, -50, 1000)\n },\n\n addSurface(spec) {\n const basis = makeBasis(spec.normal, spec.tangent)\n surfaces.set(spec.id, {\n ...clone(spec),\n ...basis,\n point: copy3(spec.point),\n half: [spec.halfSize[0], spec.halfSize[1]],\n })\n },\n\n removeSurface(id) {\n surfaces.delete(id)\n for (const s of [...stains.values()]) if (s.surface === id) stains.delete(s.id)\n },\n\n splash(liquid, point, liters, velocity) {\n if (!liquids.has(liquid) || !(liters > 0)) return\n const v: Vec3 = velocity ?? [0, -0.1, 0]\n const origin = add(point, [0, 0.02, 0])\n const hit = trace(landingSurfaces(), groundAt, origin, v, gravity)\n if (!hit) return\n emit({ type: 'splash', liquid, point: copy3(hit.point), intensity: clamp(0.3 + liters * 2, 0, 1) })\n deposit(liquid, hit, liters, length(v) > 0.3 ? hit.velocity : null)\n },\n\n clearStains(point, radius) {\n for (const s of [...stains.values()]) {\n if (distance(point, s.center) <= radius + 0.5 * meanRadius(s.front)) {\n stains.delete(s.id)\n emit({ type: 'dried', stain: s.id, mark: false })\n }\n }\n },\n\n update(dt) {\n if (dt > 0) {\n const n = clamp(Math.ceil(dt / MAX_STEP), 1, MAX_SUBSTEPS)\n updating = true\n try {\n for (let i = 0; i < n; i++) step(dt / n)\n } finally {\n updating = false\n }\n }\n const out = queue\n queue = []\n return out\n },\n\n container: (id) => {\n const c = containers.get(id)\n return c ? stateOf(c) : null\n },\n containers: () => [...containers.values()].map(stateOf),\n streams: () => [...streams.values()].map(streamView),\n stains: () => [...stains.values()].map(stainView),\n\n on(type, fn) {\n let set = listeners.get(type)\n if (!set) listeners.set(type, (set = new Set()))\n set.add(fn)\n return () => {\n set?.delete(fn)\n }\n },\n\n serialize() {\n const snapshot: Snapshot = {\n version: 1,\n time,\n rng: rng.state,\n counters: { ...counters },\n containers: [...containers.values()].map((c) => {\n const { cols: _cols, ...rest } = c\n return clone(rest)\n }),\n surfaces: [...surfaces.values()].map((s) => clone({ id: s.id, point: s.point, normal: s.normal, halfSize: s.half, tangent: s.t1, porosity: s.porosity })),\n streams: [...streams.values()].map((s) => clone(s)),\n taps: [...taps.values()].map((t) => clone(t)),\n stains: [...stains.values()].map((s) => clone(s)),\n }\n return snapshot\n },\n\n load(input) {\n const snap = clone(input) as Snapshot\n if (!snap || snap.version !== 1) throw new Error('game-fluids: unsupported snapshot')\n time = snap.time\n rng = new Rng(0)\n rng.state = snap.rng\n counters.container = snap.counters.container\n counters.stream = snap.counters.stream\n counters.stain = snap.counters.stain\n containers = new Map()\n for (const raw of snap.containers) containers.set(raw.id, { ...raw, cols: makeColumns(raw.spec.shape) })\n surfaces = new Map()\n for (const s of snap.surfaces) world.addSurface(s)\n counters.tap = snap.counters.tap ?? 0\n streams = new Map(snap.streams.map((s) => [s.id, s]))\n taps = new Map((snap.taps ?? []).map((t) => [t.id, t]))\n stains = new Map(snap.stains.map((s) => [s.id, s]))\n queue = []\n },\n }\n\n return world\n}\n","[\n {\n \"id\": \"sip\",\n \"duration\": 1.2,\n \"keys\": [\n {\n \"t\": 0,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.35,\n \"position\": [\n 0.0,\n 0.02,\n -0.2\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.45,\n \"position\": [\n 0.0,\n 0.02,\n -0.2\n ],\n \"rotation\": [\n -0.9,\n 0,\n 0\n ],\n \"action\": \"drink\",\n \"rate\": 0.06\n },\n {\n \"t\": 0.95,\n \"position\": [\n 0.0,\n 0.02,\n -0.2\n ],\n \"rotation\": [\n -0.9,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 1.2,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n }\n ]\n },\n {\n \"id\": \"drink\",\n \"duration\": 2.5,\n \"keys\": [\n {\n \"t\": 0,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.5,\n \"position\": [\n 0.0,\n 0.02,\n -0.2\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.8,\n \"position\": [\n 0.0,\n 0.02,\n -0.2\n ],\n \"rotation\": [\n -1.22,\n 0,\n 0\n ],\n \"action\": \"drink\",\n \"rate\": 0.1667\n },\n {\n \"t\": 2.0,\n \"position\": [\n 0.0,\n 0.02,\n -0.2\n ],\n \"rotation\": [\n -1.22,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 2.5,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n }\n ]\n },\n {\n \"id\": \"chug\",\n \"duration\": 4,\n \"keys\": [\n {\n \"t\": 0,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.5,\n \"position\": [\n 0.0,\n 0.02,\n -0.2\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.9,\n \"position\": [\n 0.0,\n 0.02,\n -0.2\n ],\n \"rotation\": [\n -1.45,\n 0,\n 0\n ],\n \"action\": \"drink\",\n \"rate\": 0.2\n },\n {\n \"t\": 3.4,\n \"position\": [\n 0.0,\n 0.02,\n -0.2\n ],\n \"rotation\": [\n -1.45,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 4,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n }\n ]\n },\n {\n \"id\": \"pour\",\n \"duration\": 3,\n \"keys\": [\n {\n \"t\": 0,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.4,\n \"position\": [\n 0.1,\n 0.0,\n -0.35\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.8,\n \"position\": [\n 0.1,\n 0.0,\n -0.35\n ],\n \"rotation\": [\n -1.745,\n 0,\n 0\n ],\n \"action\": \"pour\",\n \"rate\": 1\n },\n {\n \"t\": 2.4,\n \"position\": [\n 0.1,\n 0.0,\n -0.35\n ],\n \"rotation\": [\n -1.745,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 3,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n }\n ]\n },\n {\n \"id\": \"pour_out\",\n \"duration\": 4,\n \"keys\": [\n {\n \"t\": 0,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.5,\n \"position\": [\n 0.1,\n 0.1,\n -0.35\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 1.0,\n \"position\": [\n 0.1,\n 0.1,\n -0.35\n ],\n \"rotation\": [\n -2.2,\n 0,\n 0\n ],\n \"action\": \"pour\",\n \"rate\": 40\n },\n {\n \"t\": 3.2,\n \"position\": [\n 0.1,\n 0.1,\n -0.35\n ],\n \"rotation\": [\n -2.2,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 4,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n }\n ]\n },\n {\n \"id\": \"open\",\n \"duration\": 1,\n \"keys\": [\n {\n \"t\": 0,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.25,\n \"position\": [\n 0.1,\n 0,\n -0.3\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"open\"\n },\n {\n \"t\": 0.7,\n \"position\": [\n 0.1,\n 0,\n -0.3\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 1,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n }\n ]\n },\n {\n \"id\": \"close\",\n \"duration\": 1,\n \"keys\": [\n {\n \"t\": 0,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 0.25,\n \"position\": [\n 0.1,\n 0,\n -0.3\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"close\"\n },\n {\n \"t\": 0.7,\n \"position\": [\n 0.1,\n 0,\n -0.3\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n },\n {\n \"t\": 1,\n \"position\": [\n 0.22,\n -0.2,\n -0.4\n ],\n \"rotation\": [\n 0,\n 0,\n 0\n ],\n \"action\": \"none\"\n }\n ]\n }\n]","import itemUsesData from './data/item-uses.json'\nimport { lerp3, quatFromEuler, quatMultiply, quatRotate, add } from './math'\nimport type { FluidEvent, FluidWorld, ItemUseDef, ItemUsePlayer, Quat, Vec3 } from './types'\n\n/** Built-in scripted uses of a held item (data in `data/item-uses.json`): `sip`, `drink`, `chug`, `pour`, `pour_out`, `open`, `close`. */\nexport const ITEM_USES: ItemUseDef[] = itemUsesData as unknown as ItemUseDef[]\n\n/** World transform of the hand (or any holder) the item pose is relative to. */\nexport type HandTransform = () => { position: Vec3; rotation: Quat }\n\nconst DEFAULT_DRINK_RATE = 0.1\n\n/**\n * Plays an item use on a container: interpolates the keys, applies `drink` (rate × dt litres) and `pour` (`world.pour`, `rate` multiplies the\n * time) while the current key has that action, and fires `open` / `close` once when their key is reached. When `getHandTransform` is given, the\n * container's world transform follows hand ∘ pose every update; otherwise the host animates the viewmodel from `pose()`.\n */\nexport function createItemUse(world: FluidWorld, containerId: string, def: ItemUseDef, getHandTransform?: HandTransform): ItemUsePlayer {\n const keys = [...def.keys].sort((a, b) => a.t - b.t)\n let time = 0\n let done = keys.length === 0 || def.duration <= 0\n let checked = false\n let lastPos: Vec3 | null = null\n const fired = new Set<number>()\n\n const pose = (): { position: Vec3; rotation: Vec3 } => {\n if (keys.length === 0) return { position: [0, 0, 0], rotation: [0, 0, 0] }\n if (time <= keys[0].t) return { position: [...keys[0].position], rotation: [...keys[0].rotation] }\n for (let i = 0; i < keys.length - 1; i++) {\n const a = keys[i]\n const b = keys[i + 1]\n if (time <= b.t) {\n const k = b.t > a.t ? (time - a.t) / (b.t - a.t) : 1\n return { position: lerp3(a.position, b.position, k), rotation: lerp3(a.rotation, b.rotation, k) }\n }\n }\n const last = keys[keys.length - 1]\n return { position: [...last.position], rotation: [...last.rotation] }\n }\n\n const follow = (dt: number): void => {\n if (!getHandTransform) return\n const hand = getHandTransform()\n const p = pose()\n const position = add(hand.position, quatRotate(hand.rotation, p.position))\n const rotation = quatMultiply(hand.rotation, quatFromEuler(p.rotation[0], p.rotation[1], p.rotation[2]))\n const velocity: Vec3 | undefined =\n lastPos && dt > 0 ? [(position[0] - lastPos[0]) / dt, (position[1] - lastPos[1]) / dt, (position[2] - lastPos[2]) / dt] : undefined\n lastPos = position\n world.setTransform(containerId, position, rotation, velocity)\n }\n\n const player: ItemUsePlayer = {\n def,\n get time() {\n return time\n },\n get done() {\n return done\n },\n pose,\n update(dt) {\n if (done) return []\n const events: FluidEvent[] = []\n const off = world.on('*', (e) => events.push(e))\n try {\n if (!checked) {\n checked = true\n const state = world.container(containerId)\n if (!state || (def.liquids && def.liquids.length > 0 && (!state.liquid || !def.liquids.includes(state.liquid)))) {\n done = true\n return events\n }\n }\n const start = time\n const end = Math.min(def.duration, time + Math.max(0, dt))\n for (let i = 0; i < keys.length && !done; i++) {\n const key = keys[i]\n const spanStart = key.t\n const spanEnd = i + 1 < keys.length ? keys[i + 1].t : def.duration\n if ((key.action === 'open' || key.action === 'close') && !fired.has(i) && end >= spanStart) {\n fired.add(i)\n if (key.action === 'open') world.open(containerId)\n else world.close(containerId)\n }\n const overlap = Math.min(end, spanEnd) - Math.max(start, spanStart)\n if (overlap <= 0) continue\n if (key.action === 'drink') {\n const ev = world.drink(containerId, (key.rate ?? DEFAULT_DRINK_RATE) * overlap)\n const left = world.container(containerId)\n if (!ev || !left || left.liters <= 1e-6) {\n time = def.duration\n done = true\n }\n } else if (key.action === 'pour') {\n world.pour(containerId, overlap * (key.rate ?? 1))\n }\n }\n if (!done) {\n time = end\n done = time >= def.duration\n }\n follow(dt)\n } finally {\n off()\n }\n return events\n },\n cancel() {\n done = true\n },\n }\n return player\n}\n","import type * as THREE_NS from 'three'\nimport { STAIN_SAMPLES } from '@adatechnology/game-fluids'\nimport type { LiquidDef, StainState, Vec3 } from '@adatechnology/game-fluids'\nimport type { MaterialCache } from './materials'\nimport { stainColorOf } from './materials'\nimport type { QualityProfile } from './quality'\nimport { clamp } from './util'\nimport type { ThreeLib } from './types'\n\n/** Periodic Catmull-Rom of the 16 front radii: a smooth outline with `subdiv` points per sample. */\nexport const smoothOutline = (front: number[], subdiv: number, out?: number[]): number[] => {\n const n = front.length\n const res = out ?? []\n res.length = n * subdiv\n for (let i = 0; i < n; i++) {\n const p0 = front[(i + n - 1) % n]\n const p1 = front[i]\n const p2 = front[(i + 1) % n]\n const p3 = front[(i + 2) % n]\n for (let j = 0; j < subdiv; j++) {\n const t = j / subdiv\n 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)\n res[i * subdiv + j] = Math.max(0.004, r)\n }\n }\n return res\n}\n\ninterface Fan {\n mesh: THREE_NS.Mesh\n position: Float32Array\n color: Float32Array\n}\n\n/**\n * One stain: an outer film (the whole flow-front polygon, soft at the rim) and an inner glossy fan at `wetRadius` that recedes while it dries.\n * Both are triangle fans with preallocated buffers, lying on the surface plane a hair above it.\n */\nexport class StainVisual {\n readonly group: THREE_NS.Group\n private readonly outer: Fan\n private readonly inner: Fan\n private readonly ring: number\n private readonly radii: number[] = []\n private readonly tint: THREE_NS.Color\n stainId = ''\n signature = Number.NaN\n triangles = 0\n\n constructor(\n private readonly THREE: ThreeLib,\n profile: QualityProfile,\n readonly slot: number,\n ) {\n this.ring = STAIN_SAMPLES * profile.stainSubdiv\n this.group = new THREE.Group()\n this.group.name = 'fluid_stain'\n this.tint = new THREE.Color()\n this.outer = this.makeFan('fluid_stain_film', 1)\n this.inner = this.makeFan('fluid_stain_wet', 2)\n this.group.add(this.outer.mesh, this.inner.mesh)\n this.group.visible = false\n }\n\n private makeFan(name: string, order: number): Fan {\n const T = this.THREE\n const count = 1 + this.ring\n const position = new Float32Array(count * 3)\n const color = new Float32Array(count * 4)\n const normal = new Float32Array(count * 3)\n const geometry = new T.BufferGeometry()\n const p = new T.BufferAttribute(position, 3)\n const c = new T.BufferAttribute(color, 4)\n p.setUsage(T.DynamicDrawUsage)\n c.setUsage(T.DynamicDrawUsage)\n geometry.setAttribute('position', p)\n geometry.setAttribute('normal', new T.BufferAttribute(normal, 3))\n geometry.setAttribute('color', c)\n const index: number[] = []\n for (let i = 0; i < this.ring; i++) index.push(0, 1 + i, 1 + ((i + 1) % this.ring))\n geometry.setIndex(index)\n const mesh = new T.Mesh(geometry)\n mesh.name = name\n mesh.frustumCulled = false\n mesh.renderOrder = order\n return { mesh, position, color }\n }\n\n assign(def: LiquidDef, materials: MaterialCache): void {\n this.outer.mesh.material = materials.stainFilm(def)\n this.inner.mesh.material = materials.stainWet(def)\n }\n\n hide(): void {\n this.group.visible = false\n this.stainId = ''\n this.signature = Number.NaN\n this.triangles = 0\n }\n\n /** Rebuild the vertices from the stain state (skipped when nothing changed). */\n update(stain: StainState, def: LiquidDef, profile: QualityProfile): void {\n const sig =\n stain.center[0] * 3 +\n stain.center[1] * 5 +\n stain.center[2] * 7 +\n stain.front.reduce((s, v, i) => s + v * (i + 1), 0) * 11 +\n stain.wetness * 13 +\n stain.wetRadius * 17 +\n (stain.soaked / (stain.soaked + stain.liters + 1e-6)) * 19\n if (sig === this.signature && this.group.visible) return\n this.signature = sig\n\n const n: Vec3 = stain.normal ?? [0, 1, 0]\n const t1: Vec3 = stain.tangent ?? [1, 0, 0]\n const t2: Vec3 = [n[1] * t1[2] - n[2] * t1[1], n[2] * t1[0] - n[0] * t1[2], n[0] * t1[1] - n[1] * t1[0]]\n smoothOutline(stain.front, profile.stainSubdiv, this.radii)\n const ring = this.ring\n const lift = 0.0015 + 0.0004 * (this.slot % 6)\n const mean = stain.front.reduce((s, v) => s + v, 0) / stain.front.length\n const wet = clamp(stain.wetness, 0, 1)\n const soakRatio = stain.soaked / (stain.soaked + stain.liters + 1e-6)\n\n // Outer film: tinted by the liquid, darker while wet and where it soaked in, fainter once only the mark is left.\n this.tint.setHex(stainColorOf(def))\n const dark = 1 - 0.3 * wet - 0.2 * soakRatio\n const markAlpha = def.leavesMark ? 0.34 : 0.1\n const aMid = (markAlpha + (0.62 - markAlpha) * wet) * (0.75 + 0.25 * Math.min(1, stain.liters * 20 + soakRatio))\n 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)\n this.outer.mesh.visible = true\n\n // Inner glossy wet fan, centred where the liquid came in, shrinking as it dries.\n const wr = clamp(stain.wetRadius, 0, 1)\n const showInner = wr > 0.05 && wet > 0.03\n this.inner.mesh.visible = showInner\n if (showInner) {\n this.tint.setHex(def.color)\n const shift = stain.flow ? -mean * 0.1 * (1 - wr) : 0\n const centre: Vec3 = stain.flow\n ? [stain.center[0] + stain.flow[0] * shift, stain.center[1] + stain.flow[1] * shift, stain.center[2] + stain.flow[2] * shift]\n : stain.center\n const a = clamp(def.opacity, 0.2, 0.85) * wet * 0.8\n this.writeFan(this.inner, centre, n, t1, t2, lift + 0.0007, 0.9 * wr, this.tint.r, this.tint.g, this.tint.b, a, a * 0.5)\n }\n this.triangles = ring * (showInner ? 2 : 1)\n this.group.visible = true\n }\n\n private writeFan(\n fan: Fan,\n center: Vec3,\n n: Vec3,\n t1: Vec3,\n t2: Vec3,\n lift: number,\n scale: number,\n r: number,\n g: number,\n b: number,\n aCentre: number,\n aRim: number,\n ): void {\n const ring = this.ring\n const px = center[0] + n[0] * lift\n const py = center[1] + n[1] * lift\n const pz = center[2] + n[2] * lift\n fan.position[0] = px\n fan.position[1] = py\n fan.position[2] = pz\n fan.color[0] = r * 0.85\n fan.color[1] = g * 0.85\n fan.color[2] = b * 0.85\n fan.color[3] = aCentre\n const normalAttr = fan.mesh.geometry.getAttribute('normal') as THREE_NS.BufferAttribute\n for (let i = 0; i <= ring; i++) {\n normalAttr.setXYZ(i, n[0], n[1], n[2])\n }\n normalAttr.needsUpdate = true\n for (let i = 0; i < ring; i++) {\n const a = (i / ring) * Math.PI * 2\n const c = Math.cos(a) * this.radii[i] * scale\n const s = Math.sin(a) * this.radii[i] * scale\n const o = (1 + i) * 3\n fan.position[o] = px + t1[0] * c + t2[0] * s\n fan.position[o + 1] = py + t1[1] * c + t2[1] * s\n fan.position[o + 2] = pz + t1[2] * c + t2[2] * s\n const k = (1 + i) * 4\n fan.color[k] = r\n fan.color[k + 1] = g\n fan.color[k + 2] = b\n fan.color[k + 3] = aRim\n }\n const geometry = fan.mesh.geometry\n ;(geometry.getAttribute('position') as THREE_NS.BufferAttribute).needsUpdate = true\n ;(geometry.getAttribute('color') as THREE_NS.BufferAttribute).needsUpdate = true\n }\n\n dispose(): void {\n for (const fan of [this.outer, this.inner]) fan.mesh.geometry.dispose()\n }\n}\n","import type * as THREE_NS from 'three'\nimport { sampleStream } from '@adatechnology/game-fluids'\nimport type { StreamState } from '@adatechnology/game-fluids'\nimport type { QualityProfile } from './quality'\nimport { clamp } from './util'\nimport type { ThreeLib } from './types'\n\n/** Seconds a stream takes to break up into drops after the world ended it. */\nconst GHOST_TIME = 0.4\n\n/**\n * One stream: a thread along the ballistic path (a tube updated in place) with a growing drop at its leading tip, a trailing drop while it ends,\n * and for thin streams only a sequence of falling drops. The jet narrows as it accelerates.\n */\nexport class StreamVisual {\n readonly tube: THREE_NS.Mesh\n readonly drops: THREE_NS.InstancedMesh\n private readonly positions: Float32Array\n private readonly normals: Float32Array\n private readonly samples: number\n private readonly sides: number\n private readonly matrix: THREE_NS.Matrix4\n private readonly maxDrops: number\n id = ''\n age = 0\n ghostAge = -1\n /** Last known state, kept while the stream breaks into drops. */\n state: StreamState | null = null\n triangles = 0\n\n constructor(\n private readonly THREE: ThreeLib,\n profile: QualityProfile,\n dropGeometry: THREE_NS.SphereGeometry,\n ) {\n const T = THREE\n this.samples = profile.streamSamples\n this.sides = profile.streamSides\n this.maxDrops = Math.max(profile.thinDrops, 2) + 2\n this.positions = new Float32Array(this.samples * this.sides * 3)\n this.normals = new Float32Array(this.samples * this.sides * 3)\n const geometry = new T.BufferGeometry()\n const p = new T.BufferAttribute(this.positions, 3)\n const n = new T.BufferAttribute(this.normals, 3)\n p.setUsage(T.DynamicDrawUsage)\n n.setUsage(T.DynamicDrawUsage)\n geometry.setAttribute('position', p)\n geometry.setAttribute('normal', n)\n const index: number[] = []\n for (let i = 0; i < this.samples - 1; i++) {\n for (let j = 0; j < this.sides; j++) {\n const a = i * this.sides + j\n const b = i * this.sides + ((j + 1) % this.sides)\n const c = (i + 1) * this.sides + j\n const d = (i + 1) * this.sides + ((j + 1) % this.sides)\n index.push(a, c, b, b, c, d)\n }\n }\n geometry.setIndex(index)\n this.tube = new T.Mesh(geometry)\n this.tube.name = 'fluid_stream'\n this.tube.frustumCulled = false\n this.tube.renderOrder = 2\n this.drops = new T.InstancedMesh(dropGeometry, undefined as unknown as THREE_NS.Material, this.maxDrops)\n this.drops.name = 'fluid_stream_drops'\n this.drops.frustumCulled = false\n this.drops.renderOrder = 2\n this.drops.instanceMatrix.setUsage(T.DynamicDrawUsage)\n this.drops.count = 0\n this.matrix = new T.Matrix4()\n }\n\n setMaterial(material: THREE_NS.Material): void {\n this.tube.material = material\n this.drops.material = material\n }\n\n hide(): void {\n this.tube.visible = false\n this.drops.visible = false\n this.drops.count = 0\n this.triangles = 0\n }\n\n private dropAt(slot: number, x: number, y: number, z: number, radius: number): void {\n this.matrix.makeScale(radius, radius, radius).setPosition(x, y, z)\n this.drops.setMatrixAt(slot, this.matrix)\n }\n\n /**\n * Draw the stream. `state` is the world's stream while it lives, or the last known one while ghost (`ghostAge >= 0`).\n */\n update(state: StreamState, gravity: number, dt: number, profile: QualityProfile, time: number): void {\n this.state = state\n this.age += dt\n if (this.ghostAge >= 0) this.ghostAge += dt\n\n const flight = Math.max(0.02, state.flightTime ?? 1)\n const [vx, vy, vz] = state.velocity\n const speed0 = Math.max(0.3, Math.hypot(vx, vy, vz))\n const q = Math.max(state.rate, 0) / 1000\n const r0 = clamp(Math.sqrt(q / (Math.PI * speed0)), 0.0015, 0.03)\n\n const tB = Math.min(flight, this.age)\n // The tail leaves the origin at free fall once the world stopped the stream.\n const tA = this.ghostAge >= 0 ? Math.min(flight, this.ghostAge) : 0\n\n let drops = 0\n const thin = state.thin && this.ghostAge < 0\n\n if (thin) {\n this.tube.visible = false\n this.triangles = 0\n const n = profile.thinDrops\n const phase = time * 1.3\n for (let k = 0; k < n && drops < this.maxDrops; k++) {\n const tau = ((phase + k / n) % 1) * flight\n if (tau > tB) continue\n const p = sampleStream(state, gravity, tau)\n const grow = Math.min(1, tau / (0.12 * flight + 0.02))\n this.dropAt(drops++, p[0], p[1], p[2], Math.max(0.002, r0 * 1.8) * (0.4 + 0.6 * grow))\n }\n } else if (tB - tA > 0.015) {\n this.writeTube(state, gravity, tA, tB, r0, speed0)\n this.tube.visible = true\n this.triangles = (this.samples - 1) * this.sides * 2\n // Leading drop that grows and detaches while the jet is still advancing; trailing drop while it ends.\n if (tB < flight) {\n const p = sampleStream(state, gravity, tB)\n const w = this.radiusAt(state, gravity, tB, r0, speed0)\n this.dropAt(drops++, p[0], p[1], p[2], w * 1.35)\n }\n if (this.ghostAge >= 0) {\n const p = sampleStream(state, gravity, tA)\n const w = this.radiusAt(state, gravity, tA, r0, speed0)\n this.dropAt(drops++, p[0], p[1], p[2], w * 1.4 * (1 - this.ghostAge / GHOST_TIME * 0.4))\n }\n } else {\n this.tube.visible = false\n this.triangles = 0\n if (tA < flight) {\n // Only the drop is left.\n const p = sampleStream(state, gravity, this.ghostAge >= 0 ? tA : tB)\n this.dropAt(drops++, p[0], p[1], p[2], Math.max(0.003, r0 * 1.3))\n }\n }\n\n this.drops.count = drops\n this.drops.visible = drops > 0\n this.drops.instanceMatrix.needsUpdate = true\n this.triangles += drops * 24\n }\n\n /** The ghost is over once its tail reached the landing. */\n get finished(): boolean {\n return this.ghostAge >= 0 && (this.ghostAge >= GHOST_TIME || this.ghostAge >= (this.state?.flightTime ?? 0))\n }\n\n private radiusAt(state: StreamState, gravity: number, t: number, r0: number, speed0: number): number {\n const vy = state.velocity[1] - gravity * t\n const speed = Math.max(0.3, Math.hypot(state.velocity[0], vy, state.velocity[2]))\n const fade = this.ghostAge >= 0 ? clamp(1 - this.ghostAge / GHOST_TIME, 0.15, 1) : 1\n return r0 * Math.sqrt(speed0 / speed) * fade\n }\n\n private writeTube(state: StreamState, gravity: number, tA: number, tB: number, r0: number, speed0: number): void {\n const n = this.samples\n const sides = this.sides\n for (let i = 0; i < n; i++) {\n const k = i / (n - 1)\n const t = tA + (tB - tA) * k\n const p = sampleStream(state, gravity, t)\n const vx = state.velocity[0]\n const vy = state.velocity[1] - gravity * t\n const vz = state.velocity[2]\n const l = Math.hypot(vx, vy, vz) || 1\n const tx = vx / l\n const ty = vy / l\n const tz = vz / l\n // Any vector not parallel to the tangent gives the first side axis.\n const ax = Math.abs(tz) < 0.9 ? 0 : 1\n const az = Math.abs(tz) < 0.9 ? 1 : 0\n let b1x = ty * az - tz * 0\n let b1y = tz * ax - tx * az\n let b1z = tx * 0 - ty * ax\n const bl = Math.hypot(b1x, b1y, b1z) || 1\n b1x /= bl\n b1y /= bl\n b1z /= bl\n const b2x = ty * b1z - tz * b1y\n const b2y = tz * b1x - tx * b1z\n const b2z = tx * b1y - ty * b1x\n // Rounded ends: the radius tapers to a bead at both ends.\n const end = Math.min(1, 0.35 + Math.min(k, 1 - k) * 8)\n const r = this.radiusAt(state, gravity, t, r0, speed0) * end\n for (let j = 0; j < sides; j++) {\n const a = (j / sides) * Math.PI * 2\n const c = Math.cos(a)\n const s = Math.sin(a)\n const nx = b1x * c + b2x * s\n const ny = b1y * c + b2y * s\n const nz = b1z * c + b2z * s\n const o = (i * sides + j) * 3\n this.positions[o] = p[0] + nx * r\n this.positions[o + 1] = p[1] + ny * r\n this.positions[o + 2] = p[2] + nz * r\n this.normals[o] = nx\n this.normals[o + 1] = ny\n this.normals[o + 2] = nz\n }\n }\n const g = this.tube.geometry\n ;(g.getAttribute('position') as THREE_NS.BufferAttribute).needsUpdate = true\n ;(g.getAttribute('normal') as THREE_NS.BufferAttribute).needsUpdate = true\n }\n\n start(id: string): void {\n this.id = id\n this.age = 0\n this.ghostAge = -1\n this.state = null\n }\n\n dispose(): void {\n this.tube.geometry.dispose()\n this.drops.dispose()\n }\n}\n","import type * as THREE_NS from 'three'\nimport type { FluidEvent, StainState, StreamState } from '@adatechnology/game-fluids'\nimport { LiquidBody } from './liquid-body'\nimport { MaterialCache } from './materials'\nimport { ParticlePool } from './particles'\nimport { profileOf } from './quality'\nimport { StainVisual } from './stains'\nimport { StreamVisual } from './streams'\nimport type { BindOptions, FluidQuality, FluidRenderer, FluidRendererOptions, FluidRendererStats, ThreeLib } from './types'\n\n/**\n * Draws a `FluidWorld` with three.js. It only reads the world state (plus `splash` events for droplets), so a LAN client that receives the\n * same world state draws the same liquids.\n */\nexport function createFluidRenderer(THREE: ThreeLib, options: FluidRendererOptions): FluidRenderer {\n const { scene, fluids } = options\n const quality: FluidQuality = options.quality ?? 'medium'\n const profile = profileOf(quality)\n const materials = new MaterialCache(THREE, options.envMap ?? null)\n const root = new THREE.Group()\n root.name = 'fluid_renderer'\n scene.add(root)\n const particles = new ParticlePool(THREE, profile.maxParticles, options.pointScale ?? 600)\n root.add(particles.points)\n const dropGeometry = new THREE.SphereGeometry(1, 8, 6)\n\n const bodies = new Map<string, LiquidBody>()\n const streamsActive = new Map<string, StreamVisual>()\n const streamsFree: StreamVisual[] = []\n let streamsCreated = 0\n const stainsActive = new Map<string, StainVisual>()\n const stainsFree: StainVisual[] = []\n let stainsCreated = 0\n let time = 0\n let disposed = false\n\n const offSplash = fluids.on('splash', (e: FluidEvent) => {\n if (e.type !== 'splash' || disposed) return\n const def = fluids.liquids.get(e.liquid)\n if (!def) return\n particles.splash(e.point, e.intensity, def.color, def.opacity, profile.splashDroplets)\n })\n\n // ---- streams ----\n\n const acquireStream = (id: string): StreamVisual | null => {\n let v = streamsFree.pop() ?? null\n if (!v) {\n if (streamsCreated >= profile.maxStreams) return null\n streamsCreated++\n v = new StreamVisual(THREE, profile, dropGeometry)\n root.add(v.tube, v.drops)\n }\n v.start(id)\n return v\n }\n\n const releaseStream = (v: StreamVisual): void => {\n v.hide()\n streamsActive.delete(v.id)\n streamsFree.push(v)\n }\n\n const syncStreams = (dt: number): void => {\n const live = fluids.streams()\n const seen = new Set<string>()\n // Biggest first, so the budget keeps the visible ones.\n live.sort((a: StreamState, b: StreamState) => b.rate - a.rate)\n for (const s of live) {\n seen.add(s.id)\n let v = streamsActive.get(s.id)\n if (!v) {\n const fresh = acquireStream(s.id)\n if (!fresh) continue\n v = fresh\n streamsActive.set(s.id, v)\n }\n const def = fluids.liquids.get(s.liquid)\n if (!def) continue\n v.ghostAge = -1\n v.setMaterial(materials.liquidStream(def))\n v.update(s, fluids.gravity, dt, profile, time)\n }\n for (const v of [...streamsActive.values()]) {\n if (seen.has(v.id)) continue\n // The world ended it: let it break into drops.\n if (v.ghostAge < 0) v.ghostAge = 0\n if (!v.state || v.finished) {\n releaseStream(v)\n continue\n }\n v.update(v.state, fluids.gravity, dt, profile, time)\n if (v.finished) releaseStream(v)\n }\n }\n\n // ---- stains ----\n\n const syncStains = (): void => {\n const all = fluids.stains()\n // Keep the newest stains when the budget is smaller than the world.\n const wanted = all.length > profile.maxStains ? [...all].sort((a: StainState, b: StainState) => (a.age ?? 0) - (b.age ?? 0)).slice(0, profile.maxStains) : all\n const ids = new Set(wanted.map((s: StainState) => s.id))\n for (const [id, v] of [...stainsActive]) {\n if (ids.has(id)) continue\n v.hide()\n stainsActive.delete(id)\n stainsFree.push(v)\n }\n for (const s of wanted) {\n const def = fluids.liquids.get(s.liquid)\n if (!def) continue\n let v = stainsActive.get(s.id)\n if (!v) {\n v = stainsFree.pop()\n if (!v) {\n if (stainsCreated >= profile.maxStains) continue\n v = new StainVisual(THREE, profile, stainsCreated++)\n root.add(v.group)\n }\n v.stainId = s.id\n v.signature = Number.NaN\n v.assign(def, materials)\n stainsActive.set(s.id, v)\n }\n v.update(s, def, profile)\n }\n }\n\n // ---- public ----\n\n const renderer: FluidRenderer = {\n bindContainer(containerId: string, object: THREE_NS.Object3D, opts: BindOptions = {}) {\n renderer.unbindContainer(containerId)\n bodies.set(containerId, new LiquidBody(THREE, containerId, object, opts.innerOffset ?? [0, 0, 0]))\n },\n\n unbindContainer(containerId: string) {\n const body = bodies.get(containerId)\n if (!body) return\n body.dispose()\n bodies.delete(containerId)\n },\n\n update(dt: number) {\n if (disposed) return\n time += dt\n for (const body of bodies.values()) body.update(dt, fluids, materials, profile, particles)\n syncStreams(dt)\n syncStains()\n particles.update(dt, fluids.gravity)\n },\n\n clear() {\n for (const v of [...streamsActive.values()]) releaseStream(v)\n for (const [id, v] of [...stainsActive]) {\n v.hide()\n stainsActive.delete(id)\n stainsFree.push(v)\n }\n particles.clear()\n },\n\n dispose() {\n if (disposed) return\n disposed = true\n offSplash()\n for (const body of bodies.values()) body.dispose()\n bodies.clear()\n for (const v of [...streamsActive.values(), ...streamsFree]) v.dispose()\n for (const v of [...stainsActive.values(), ...stainsFree]) v.dispose()\n streamsActive.clear()\n stainsActive.clear()\n streamsFree.length = 0\n stainsFree.length = 0\n particles.dispose()\n dropGeometry.dispose()\n materials.dispose()\n scene.remove(root)\n root.clear()\n },\n\n stats(): FluidRendererStats {\n let triangles = 0\n let containers = 0\n for (const body of bodies.values()) {\n triangles += body.triangles\n if (body.visible) containers++\n }\n for (const v of streamsActive.values()) triangles += v.triangles\n for (const v of stainsActive.values()) triangles += v.triangles\n return {\n quality,\n containers,\n streams: streamsActive.size,\n stains: stainsActive.size,\n particles: particles.count,\n triangles,\n budget: { stains: profile.maxStains, streams: profile.maxStreams, particles: profile.maxParticles },\n }\n },\n }\n\n return renderer\n}\n\n"],"mappings":"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","names":[]}
|