/** * The solved field as the views need it, and how a complex value becomes a * color: the pressure at phase t, Re(u exp(-i omega t)) = Re u cos wt + Im u sin wt, * on a symmetric diverging scale, or the level 20 log10(|u| / ref) in dB. The * reference of the level is one number for the room (the median of |u|) and the * free field |G| at each point for the cymbal, so that 0 dB means no effect of * the cymbal. */ import type { Slice } from '../problem.ts'; export type Mode = 'pressure' | 'level'; export interface SliceField { slice: Slice; /** interleaved complex, NaN where the field is not computed */ u: Float64Array; /** reference of the level at each point; when absent, Field.ref */ ref?: Float64Array; } export interface Field { k: number; /** the source (the listener, for the cymbal) */ x0: [number, number, number]; ngrid: number; slices: SliceField[]; /** surface field at the nodes, interleaved complex */ uWall: Float64Array; /** the level's reference on the surface is the free field |G| = 1 / (4 pi |x - x0|) rather than ref */ freeFieldLevel: boolean; /** color limit for the pressure: 99th percentile of |u| more than half a wavelength from the source */ umax: number; /** reference for the level when there is no per-point reference: the median of |u| */ ref: number; /** the level scale in dB, and whether it is centered (diverging colormap) */ level: { min: number; max: number; diverging: boolean; label: string }; /** the 3D view: what to look at, how far, and whether surfaces facing the camera are cut away */ scene: { center: [number, number, number]; radius: number; cutaway: boolean }; /** the cymbal's cross section (r, z) and radius, drawn on the slices */ outline?: { r: Float64Array; z: Float64Array; radius: number }; } /** Color limits from the slices, as plot.py computes them. */ export function colorLimits(slices: SliceField[], x0: ArrayLike, k: number): { umax: number; ref: number } { const vals: number[] = []; const rmin = Math.PI / k; for (const { slice, u } of slices) { const n = slice.mask.length; for (let q = 0; q < n; q++) { if (!slice.mask[q]) continue; const r = Math.hypot(slice.pts[q] - x0[0], slice.pts[n + q] - x0[1], slice.pts[2 * n + q] - x0[2]); if (r <= rmin) continue; const v = Math.hypot(u[2 * q], u[2 * q + 1]); if (Number.isFinite(v)) vals.push(v); } } if (!vals.length) return { umax: 1, ref: 1 }; vals.sort((x, y) => x - y); return { umax: vals[Math.floor(0.99 * (vals.length - 1))], ref: vals[Math.floor(0.5 * (vals.length - 1))] }; } /** Position in [0, 1] on the colormap of a complex value, or NaN. */ export function colorParam(re: number, im: number, mode: Mode, c: number, s: number, umax: number, ref: number, level: Field['level']): number { if (mode === 'pressure') return 0.5 + (0.5 * (re * c + im * s)) / umax; const db = 20 * Math.log10(Math.hypot(re, im) / ref); return (db - level.min) / (level.max - level.min); }