import { GpuComputeNode, GpuFragmentParams } from '../../gpu/contract'; import { ComputeStep, KitComputePipeline } from '../../gpu/compute'; import { StableFluidsKernels } from '../../gpu/scaffolds/fluids'; import { PointerFrame, PointerLike, PointerVelocityTrackerOptions } from '../../gpu/kit/host/pointer'; /** The TypeGPU root, via the contract (direct `typegpu` imports are restricted to src/gpu/). */ export type FluidRoot = NonNullable['root']; /** A 2D-dispatched TGSL kernel over the N×N grid. */ export type FluidKernel = (cx: number, cy: number) => void; /** The renderer's per-frame params, as the fluid consumers read them. */ export interface FluidFrameParams { pointer?: PointerLike; deltaTime?: number; dimensions?: { width: number; height: number; }; } /** One frame of the simulation, handed to every part and to `values`. */ export interface FluidFrame { /** Frame delta, clamped to `maxDeltaTime` (0.033 s default). */ dt: number; /** `Date.now()` this frame (wall-clock bookkeeping; the idle gates run on simulated `dt`). */ now: number; /** Local clock: sum of clamped deltas. An init part may reset it (Fog's warm-up). */ readonly elapsed: number; setElapsed(t: number): void; /** The noun-owned tracker's sample (config `pointer`), or null. Ribbons own their own. */ ptr: PointerFrame | null; num(key: string, fallback: number): number; getCpuValue(key: string): unknown; frameParams: FluidFrameParams; /** Write the params uniform directly (per-stamp rewrites inside ribbon thunks). */ writeParams(v: unknown): void; } /** What parts get to build their pipelines against at composition time. */ export interface FluidSetupCtx { params: GpuFragmentParams; root: FluidRoot; /** Grid resolution per axis. */ n: number; buffers: { velA: unknown; velB: unknown; dyeA: unknown; dyeB: unknown; pressure: unknown; divergence: unknown; maskBuf?: unknown; }; /** The params uniform's buffer (for a part's own bind group). */ paramsBuffer: unknown; /** The solve bind group every solver pass runs on. */ fluidBindGroup: unknown; /** One kernel as a guarded pass — on the fluid bind group unless another is given. */ pass(kernel: FluidKernel, bindGroup?: unknown): KitComputePipeline; } /** An emission part: pre-gate bookkeeping, an optional idle gate, and this frame's dispatches. */ export interface FluidEmitter { setup?(ctx: FluidSetupCtx): void; /** Pre-gate bookkeeping (pointer sampling, colour-cycle drift, markActive). */ tick?(f: FluidFrame): void; /** Idle gate: true → the whole frame is skipped (the settled field persists on screen). */ skip?(f: FluidFrame): boolean; /** Push this frame's emission dispatches (they run before the solve chain). */ emit?(f: FluidFrame, nodes: ComputeStep[], values: TParams): void; } /** A per-SOLVE stage: runs inside every solve, including an init part's warm-up steps. */ export interface FluidSolveStage { stage: 'pre' | 'post'; kernel: FluidKernel; } /** Ambient body force (Fog's turbulence field) — dispatched before every solve chain. */ export declare const ambientForce: (kernel: FluidKernel) => FluidSolveStage; /** Restoration pass (Fog's colour re-seed against numerical diffusion) — after every solve chain. */ export declare const restoreToward: (kernel: FluidKernel) => FluidSolveStage; /** What an init part gets to append with. `writeParams` returns a QUEUED thunk (device.queue order). */ export interface FluidInitIo { nodes: ComputeStep[]; writeParams(v: TParams): ComputeStep; solve(nodes: ComputeStep[], jacobiIters: number): void; } export interface FluidInitPart { setup?(ctx: FluidSetupCtx): void; stale(f: FluidFrame): boolean; run(f: FluidFrame, io: FluidInitIo): void; } /** What a container part (SmokeFill's confining shape) contributes. */ export interface FluidContainerParts { /** The mask pass, dispatched after the params write and before emission. */ maskPass: KitComputePipeline; /** Extra compute outputs to expose (the volumetric field texture). */ outputs?: Record; /** * Per-frame pre-nodes (the volumetric field pre-march). They run FIRST, and they are what a * sub-millisecond frame still returns — a pending march is never dropped. */ preFrame?(frameParams: unknown): ComputeStep[] | null; } export interface FluidSimConfig { /** Grid resolution per axis (structural — the kernels are baked against it). */ resolution: number; /** The shader's fluid bind-group layout (the scaffolds/fluids entry-name contract). */ layout: unknown; /** The output pass's layout (`dyeA` read-only + `outTex`). */ outputLayout: unknown; /** The shader's per-frame params struct. */ paramsSchema: unknown; solver: { kernels: StableFluidsKernels; jacobiIters: number; }; /** The dye→texture publish pass and the `outputs` key the fragment samples it under. */ output: { kernel: FluidKernel; key: string; }; /** How the per-frame params reach the GPU: one direct write (default) or a queued thunk. */ write?: 'direct' | 'thunk'; /** Noun-owned pointer tracker for cursor-force consumers; ribbon emitters own their own. */ pointer?: PointerVelocityTrackerOptions; /** Confining shape container (allocates `maskBuf`, dispatches its mask pass every frame). */ container?: { setup(ctx: FluidSetupCtx): FluidContainerParts; }; init?: FluidInitPart; inject?: FluidEmitter[]; solve?: FluidSolveStage[]; /** The per-frame params derivation — the one runtime surface. */ values(f: FluidFrame): TParams; /** Upper bound on the frame delta. Default 0.033 s (every current consumer's clamp). */ maxDeltaTime?: number; } /** * The fluid-simulation noun. Spread it into a definition: `...fluidSim((params, root) => ({...}))`. * * The build callback runs at COMPUTE-NODE creation (per instance, after the no-device bail), so * parts and closures created inside it are instance state. */ export declare function fluidSim(build: (params: GpuFragmentParams, root: FluidRoot) => FluidSimConfig): { compute: GpuComputeNode; }; /** A fixed-source emission pass (Smoke's cone, SmokeFill's confined cone): one dispatch per frame. */ export declare function splat(opts: { kernel: FluidKernel; } | { setup: (ctx: FluidSetupCtx) => KitComputePipeline; }): FluidEmitter; /** One ribbon's stamp geometry + payload, resolved per frame. */ export interface CursorRibbonSpec { /** Spacing between stamps, in viewport UV. */ stepSize: number; /** Hard cap on stamps per frame. */ maxSteps: number; /** Per-stamp payload advanced at BUILD time in stamp order (InkFlow's colour cycle). */ prepare?: (t: number) => TPrepared; /** Write the stamp's uniform — runs in a thunk right before that stamp's dispatch. */ write: (posX: number, posY: number, t: number, prepared: TPrepared) => void; } /** * Stroke emission: a pointer-velocity tracker (teleport/drag policy as data), an idle gate that * freezes the sim once the field has faded after the last stroke, and a per-frame ribbon of stamps * interpolated along the drag path (`pathStampRibbon` — no dotted gaps on a fast flick). * * The part owns its tracker; read the frame's sample back via `.ptr()` / `.active()` (SmokeFlow's * `values` derives the cursor fields from them). */ export declare function cursorRibbon(opts: { tracker?: PointerVelocityTrackerOptions; /** When emission is live this frame — a live frame marks the idle gate active. */ activeWhen: (ptr: PointerFrame, f: FluidFrame) => boolean; /** Idle window: seconds after the last activity before the sim may freeze. */ fadeSeconds: (f: FluidFrame) => number; /** Extra pre-gate per-frame bookkeeping (InkFlow's colour-cycle time drift). */ onFrame?: (f: FluidFrame, ptr: PointerFrame) => void; /** The stamp pass — on the fluid bind group, or a part-owned pipeline via `setup`. */ pass: { kernel: FluidKernel; } | { setup: (ctx: FluidSetupCtx) => KitComputePipeline; }; /** This frame's ribbon geometry + payload writer. */ ribbon: (f: FluidFrame, ptr: PointerFrame, values: TParams) => CursorRibbonSpec; }): FluidEmitter & { ptr(): PointerFrame | null; active(): boolean; }; /** * Deterministic field seeding + silent warm-up (Fog): when the seed key changes (or on first * frame), dispatch the init kernel and run the whole solve `warm.steps` times with time-advancing * param thunks, then hand the warm clock to the frame clock so ambient forces continue seamlessly. */ export declare function seededFieldInit(opts: { kernel: FluidKernel; seed: (f: FluidFrame) => number; warm: { steps: number; jacobiIters: number; /** Simulated seconds per warm step. */ dt: number; /** The warm clock's starting value (Fog randomizes it so two instances differ). */ startTime: () => number; /** The full params object for a warm step at simulated time `t` (t = 0 is the init write). */ values: (t: number, f: FluidFrame) => TParams; }; }): FluidInitPart; //# sourceMappingURL=fluids.d.ts.map