// Web Worker entry: the computations that are too slow for the main thread. "modes" is the full // eigensolve (seconds to minutes); "gains" is the listener sum, redone whenever the listener or // the material changes (a fraction of a second); "complement" applies the static condensation to // a transverse field, giving the in-plane displacements and rotations that make a mode shape a // complete initial condition for the five-DOF simulator. The condensation operator (a banded // Cholesky factor and the sparse coupling block) is built on first use and kept while the // structural configuration stays the same. import type { Listener, SimulationConfig } from "../config/schema.ts"; import { cloneConfig } from "../config/yaml.ts"; import { CymbalProfile } from "../model/geometry.ts"; import { diskMesh } from "../model/mesh.ts"; import { buildShell } from "../model/shell.ts"; import { parseModes, serializeModes, type ModesData } from "../modes.ts"; import { buildCondensationOperator, type CondensationOperator } from "./assemble.ts"; import { computeGains } from "./gains.ts"; import { computeModes, structuralKey, type PrecomputeOptions } from "./pipeline.ts"; export type PrecomputeRequest = | { type: "modes"; token: number; config: SimulationConfig; options: PrecomputeOptions } | { type: "gains"; token: number; buffer?: ArrayBuffer; listener: Listener; freq: Float64Array; phiFactor: number } | { type: "complement"; token: number; config: SimulationConfig; options: PrecomputeOptions; w: Float64Array }; export type PrecomputeReply = | { type: "progress"; token: number; phase: string; frac: number } | { type: "modes"; token: number; buffer: ArrayBuffer; seconds: number } | { type: "gains"; token: number; front: Float32Array; back: Float32Array; seconds: number } | { type: "complement"; token: number; o: Float64Array; seconds: number } | { type: "error"; token: number; message: string }; /** The worker keeps the modes it was last given so that repeated gain requests, which happen * while a listener slider is being dragged, do not re-send a 12 MB buffer every time. */ let held: { buffer: ArrayBuffer; data: ModesData } | null = null; /** The condensation operator of the last structural configuration. */ let condensation: { key: string; op: CondensationOperator } | null = null; self.onmessage = (ev: MessageEvent) => { const msg = ev.data; const post = (reply: PrecomputeReply, transfer?: Transferable[]) => (self as unknown as Worker).postMessage(reply, transfer ?? []); const t0 = performance.now(); let lastPost = 0; const progress = (token: number) => (phase: string, frac: number) => { const t = performance.now(); if (t - lastPost > 100 || frac >= 1) { lastPost = t; post({ type: "progress", token, phase, frac }); } }; try { if (msg.type === "modes") { const data = computeModes(msg.config, msg.options, progress(msg.token)); const buffer = serializeModes(data); held = { buffer, data }; condensation = null; // the shape changed; rebuild on the next complement request post({ type: "modes", token: msg.token, buffer: buffer.slice(0), seconds: (performance.now() - t0) / 1000 }); } else if (msg.type === "complement") { const key = JSON.stringify(structuralKey(msg.config, msg.options)); if (!condensation || condensation.key !== key) { const cfg = cloneConfig(msg.config); cfg.mesh.element_size = msg.options.elementSize; const profile = new CymbalProfile(cfg.profile, cfg.imperfections); const mesh = diskMesh(cfg.profile.radius, cfg.mesh.element_size, cfg.mesh.smoothing); const shell = buildShell(mesh, profile, cfg.material, cfg.numerics); condensation = { key, op: buildCondensationOperator(mesh, shell, progress(msg.token)) }; } const o = condensation.op.complement(msg.w); post({ type: "complement", token: msg.token, o, seconds: (performance.now() - t0) / 1000 }, [o.buffer]); } else { if (msg.buffer) held = { buffer: msg.buffer, data: parseModes(msg.buffer) }; if (!held) throw new Error("no modes in the worker: send the buffer with the first gains request"); const g = computeGains(held.data, msg.listener, msg.freq, msg.phiFactor); post({ type: "gains", token: msg.token, front: g.front, back: g.back, seconds: (performance.now() - t0) / 1000 }, [g.front.buffer, g.back.buffer]); } } catch (err) { post({ type: "error", token: msg.token, message: (err as Error).message ?? String(err) }); } };