1// The mode browser: a table of the modes of the linear model (frequency and
2// decay time), with a filter by frequency; clicking a row selects the mode,
3// whose shape the host then draws on the plate.
5import { t60Text } from "../loss.ts";
7export interface ModeRow {
8 freq: number; // [Hz]
9 /** Decay time of the mode [s], and the decay time each mechanism alone would give (Infinity when it contributes nothing). */
10 t60: number;
11 radiation: number;
12 material: number;
13 thermoelastic: number;
14 felt: number;
15}
17const fmtT60 = t60Text;
19export class ModeBrowser {
20 private info: HTMLDivElement;
21 private tableWrap: HTMLDivElement;
22 private tbody: HTMLTableSectionElement;
23 private minInput: HTMLInputElement;
24 private maxInput: HTMLInputElement;
25 private clearBtn: HTMLButtonElement;
26 private progress: HTMLDivElement;
27 private progressBar: HTMLDivElement;
28 private rows: ModeRow[] = [];
29 private sel: number | null = null;
31 constructor(container: HTMLElement, private onSelect: (m: number | null) => void) {
32 container.classList.add("modes");
33 const head = document.createElement("div");
34 head.className = "modes-head";
35 const lab = (t: string) => { const l = document.createElement("label"); l.textContent = t; return l; };
36 this.minInput = document.createElement("input");
37 this.minInput.type = "number"; this.minInput.placeholder = "min"; this.minInput.min = "0"; this.minInput.step = "10";
38 this.maxInput = document.createElement("input");
39 this.maxInput.type = "number"; this.maxInput.placeholder = "max"; this.maxInput.min = "0"; this.maxInput.step = "10";
40 this.clearBtn = document.createElement("button");
41 this.clearBtn.textContent = "Deselect";
42 this.clearBtn.disabled = true;
43 head.append(lab("Show"), this.minInput, lab("to"), this.maxInput, lab("Hz"), this.clearBtn);
44 this.info = document.createElement("div");
45 this.info.className = "modes-info";
46 this.progress = document.createElement("div");
47 this.progress.className = "progress";
48 this.progress.hidden = true;
49 this.progressBar = document.createElement("div");
50 this.progress.append(this.progressBar);
51 this.tableWrap = document.createElement("div");
52 this.tableWrap.className = "modes-table";
53 const table = document.createElement("table");
54 const thead = document.createElement("thead");
55 thead.innerHTML = "<tr><th>#</th><th>f (Hz)</th><th title=\"Decay time of the mode\">T60 (s)</th><th title=\"Decay time from radiation into the air alone\">air</th><th title=\"Decay time from the metal's internal friction alone\">metal</th><th title=\"Decay time from thermoelastic damping alone\">thermal</th><th title=\"Decay time from the felt of the stand alone\">felt</th></tr>";
56 this.tbody = document.createElement("tbody");
57 table.append(thead, this.tbody);
58 this.tableWrap.append(table);
59 container.append(head, this.info, this.progress, this.tableWrap);
60 this.minInput.addEventListener("change", () => this.renderRows());
61 this.maxInput.addEventListener("change", () => this.renderRows());
62 this.clearBtn.addEventListener("click", () => this.select(null));
63 }
65 /** Shows a progress bar (fraction 0..1) with a message; null hides it. */
66 setProgress(fraction: number | null, message?: string): void {
67 this.progress.hidden = fraction === null;
68 if (fraction !== null) this.progressBar.style.width = `${Math.round(100 * fraction)}%`;
69 if (message !== undefined) this.info.textContent = message;
70 }
72 setModes(rows: ModeRow[], info: string): void {
73 this.rows = rows;
74 this.info.textContent = info;
75 this.sel = null;
76 this.clearBtn.disabled = true;
77 this.renderRows();
78 }
80 clear(message: string): void {
81 this.rows = [];
82 this.sel = null;
83 this.clearBtn.disabled = true;
84 this.info.textContent = message;
85 this.tbody.innerHTML = "";
86 }
88 selected(): number | null {
89 return this.sel;
90 }
92 select(m: number | null): void {
93 this.sel = m;
94 this.clearBtn.disabled = m === null;
95 for (const tr of Array.from(this.tbody.rows)) tr.classList.toggle("selected", Number(tr.dataset.m) === m);
96 this.onSelect(m);
97 }
99 private renderRows(): void {
100 const lo = Number(this.minInput.value) || 0;
101 const hi = Number(this.maxInput.value) || Infinity;
102 this.tbody.innerHTML = "";
103 const frag = document.createDocumentFragment();
104 this.rows.forEach((r, m) => {
105 if (r.freq < lo || r.freq > hi) return;
106 const tr = document.createElement("tr");
107 tr.dataset.m = String(m);
108 if (m === this.sel) tr.classList.add("selected");
109 const f = r.freq < 1000 ? r.freq.toFixed(1) : r.freq.toFixed(0);
110 tr.innerHTML = `<td>${m + 1}</td><td>${f}</td><td>${fmtT60(r.t60)}</td><td>${fmtT60(r.radiation)}</td><td>${fmtT60(r.material)}</td><td>${fmtT60(r.thermoelastic)}</td><td>${fmtT60(r.felt)}</td>`;
111 tr.addEventListener("click", () => this.select(this.sel === m ? null : m));
112 frag.append(tr);
113 });
114 this.tbody.append(frag);
115 }
116}