/ proof-of-concept / steelsim31
Sign in
proof-of-concept / steelsim31
steelsim31 / src / ui / form.ts
612 lines · 25.0 KBCodeBlameHistory
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 1// Parameter form: numeric fields in convenient units, a few selects, the
2// editable list of initial-shape features, and the derived-quantity notes.
3// Everything is stored in SI in `params`.
5import {
6 type Excitation,
7 type Grid,
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 8 hitSchedule,
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 9 type PhysParams,
10 type PlateShape,
11 countInside,
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 13 dispersionFrequency,
14 makeGrid,
15 makeMaterial,
16 t60,
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 18} from "../params.ts";
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 19import { maxAngularFrequency } from "../sim/cpuRef.ts";
21 FEATURE_SPECS,
22 FEATURE_TYPES,
23 type FeatureType,
24 type ShapeFeature,
25 encodeFeatures,
26 initialShape,
27 legacyFeatures,
28 newFeature,
29 parseFeatures,
30 randomFeatures,
31 shapeStats,
32} from "../shape.ts";
34export type SizeUnit = "in" | "mm";
36interface FieldSpec {
37 key: keyof PhysParams;
38 label: string;
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 39 unit: string; // "size" means the shared width/height unit
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 40 scale: number; // form value = SI value / scale
41 step?: number;
42 live?: boolean; // may be changed while a simulation runs
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 43 show?: (p: PhysParams) => boolean; // shown only when this holds
46interface SelectSpec {
47 key: keyof PhysParams;
48 label: string;
49 options: [string, string][];
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 50 show?: (p: PhysParams) => boolean;
53interface Group {
54 title: string;
55 open: boolean;
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 56 selects?: SelectSpec[]; // shown at the top of the group
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 57 fields: FieldSpec[];
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 58 features?: boolean; // the group holds the initial-shape feature list
59 note?: string | "grid" | "damping" | "numerics" | "hits" | "shape";
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 62const push = (p: PhysParams) => p.excitation === "push";
63const strike = (p: PhysParams) => p.excitation === "strike";
64const series = (p: PhysParams) => strike(p) && p.hitSequence !== "single";
65const graded = (p: PhysParams) => strike(p) && (p.hitSequence === "crescendo" || p.hitSequence === "decrescendo");
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 67/** Slope above which the shallow-shell treatment of the initial shape is doubtful. */
68const SLOPE_LIMIT = 0.1;
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 70const groups: Group[] = [
71 {
72 title: "Sheet",
73 open: true,
74 selects: [{ key: "shape", label: "Outline", options: [["rectangle", "Rectangle"], ["ellipse", "Ellipse (inscribed)"]] }],
75 fields: [
76 { key: "Lx", label: "Width", unit: "size", scale: 1, step: 0.5 },
77 { key: "Ly", label: "Height", unit: "size", scale: 1, step: 0.5 },
78 { key: "h", label: "Thickness", unit: "mm", scale: 1e-3, step: 0.01 },
79 { key: "E", label: "Young's modulus", unit: "GPa", scale: 1e9, step: 1 },
80 { key: "nu", label: "Poisson ratio", unit: "", scale: 1, step: 0.01 },
81 { key: "rho", label: "Density", unit: "kg/m³", scale: 1, step: 10 },
82 { key: "holeInset", label: "Hole inset", unit: "mm", scale: 1e-3, step: 1 },
83 ],
84 note: "grid",
85 },
86 {
87 title: "Initial shape",
88 open: true,
90 features: true,
91 note: "shape",
93 {
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 95 open: true,
97 { key: "excitation", label: "Type", options: [["strike", "Drumstick strike"], ["push", "Push a corner and release"]] },
98 { key: "hitSequence", label: "Hits", options: [["single", "One hit"], ["steady", "Series, same speed"], ["crescendo", "Series, soft to loud"], ["decrescendo", "Series, loud to soft"]], show: strike },
99 ],
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 100 fields: [
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 101 { key: "pushAmount", label: "Push amount", unit: "mm", scale: 1e-3, step: 0.5, show: push },
102 { key: "fingerRadius", label: "Finger radius", unit: "mm", scale: 1e-3, step: 1, show: push },
103 { key: "rampTime", label: "Push ramp", unit: "s", scale: 1, step: 0.01, show: push },
104 { key: "holdMax", label: "Max settle time", unit: "s", scale: 1, step: 0.1, show: push },
105 { key: "releaseTime", label: "Release time", unit: "ms", scale: 1e-3, step: 1, show: push },
106 { key: "settleSigma0", label: "Settle damping σ₀", unit: "1/s", scale: 1, step: 10, show: push },
107 { key: "strikeFracX", label: "Strike x (fraction of width)", unit: "", scale: 1, step: 0.05, show: strike },
108 { key: "strikeFracY", label: "Strike y (fraction of height)", unit: "", scale: 1, step: 0.05, show: strike },
109 { key: "strikeVelocity", label: "Stick speed", unit: "m/s", scale: 1, step: 0.5, show: strike },
110 { key: "strikeVelocityMin", label: "Softest stick speed", unit: "m/s", scale: 1, step: 0.5, show: graded },
111 { key: "hitCount", label: "Number of hits", unit: "", scale: 1, step: 1, show: series },
112 { key: "hitInterval", label: "Time between hits", unit: "s", scale: 1, step: 0.1, show: series },
113 { key: "stickMass", label: "Stick mass", unit: "g", scale: 1e-3, step: 5, show: strike },
114 { key: "contactStiffness", label: "Contact stiffness K", unit: "N/mᵅ", scale: 1, step: 1e8, show: strike },
115 { key: "contactExponent", label: "Contact exponent α", unit: "", scale: 1, step: 0.1, show: strike },
116 { key: "contactRadius", label: "Tip radius", unit: "mm", scale: 1e-3, step: 1, show: strike },
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 118 note: "hits",
120 {
121 title: "Losses",
122 open: true,
123 fields: [
124 { key: "sigma0", label: "σ₀ (frequency independent)", unit: "1/s", scale: 1, step: 0.1, live: true },
125 { key: "sigma1", label: "σ₁ (∝ frequency)", unit: "m²/s", scale: 1, step: 1e-5, live: true },
126 { key: "sigmaIn", label: "In-plane loss", unit: "1/s", scale: 1, step: 5, live: true },
127 ],
128 note: "damping",
129 },
130 {
131 title: "Listener",
132 open: true,
133 fields: [
134 { key: "listenerDist", label: "Distance", unit: "m", scale: 1, step: 0.1, live: true },
135 { key: "listenerAzimuth", label: "Azimuth", unit: "deg", scale: 1, step: 5, live: true },
136 { key: "listenerElevation", label: "Elevation", unit: "deg", scale: 1, step: 5, live: true },
137 ],
138 },
139 {
140 title: "Numerics",
141 open: false,
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 142 selects: [{ key: "rigidProjection", label: "Rigid-body projection while ringing", options: [["false", "Off"], ["true", "On (every 32 steps)"]] }],
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 143 fields: [
144 { key: "spacing", label: "Grid spacing", unit: "mm", scale: 1e-3, step: 0.5 },
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 145 { key: "inPlaneMass", label: "In-plane mass factor", unit: "", scale: 1, step: 1 },
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 146 { key: "fs", label: "Audio sample rate", unit: "Hz", scale: 1, step: 100 },
147 { key: "fps", label: "Movie frame rate", unit: "fps", scale: 1, step: 10 },
148 { key: "settleKE", label: "Settled below KE", unit: "J", scale: 1, step: 1e-9 },
149 ],
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 150 note: "numerics",
152];
154const SIZE_SCALE: Record<SizeUnit, number> = { in: 0.0254, mm: 1e-3 };
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 155const selectKeys: (keyof PhysParams)[] = ["shape", "excitation", "hitSequence", "rigidProjection"];
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 156const selectValues: Record<string, string[]> = {
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 157 shape: ["rectangle", "ellipse"],
158 excitation: ["push", "strike"],
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 159 hitSequence: ["single", "steady", "crescendo", "decrescendo"],
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 160 rigidProjection: ["false", "true"],
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 163/** Parses a select value (URL or form) into the parameter's type. */
164function parseSelect(key: keyof PhysParams, v: string): string | boolean {
165 return key === "rigidProjection" ? v === "true" || v === "1" : v;
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 168export function formatNumber(v: number): string {
169 if (v === 0) return "0";
170 const a = Math.abs(v);
171 if (a >= 1e5 || a < 1e-3) return v.toExponential(3);
172 return String(+v.toPrecision(6));
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 175/** A number in SI for a link: shortest exact-enough decimal. */
176function formatSI(v: number): string {
177 return v === 0 ? "0" : String(+v.toPrecision(10));
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 180export class ParamForm {
181 readonly params: PhysParams;
182 sizeUnit: SizeUnit = "in";
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 183 /** Height scale of the random shapes drawn by Randomize [m]. */
184 randomScale = 3e-3;
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 185 /** Called after any change; `live` tells whether the field may change during a run. */
186 onChange: (key: keyof PhysParams, live: boolean) => void = () => {};
188 private inputs = new Map<keyof PhysParams, HTMLInputElement | HTMLSelectElement>();
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 189 private sizeUnitSelect = document.createElement("select");
190 private sizeUnitLabels: HTMLElement[] = [];
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 191 private conditional: { el: HTMLElement; show: (p: PhysParams) => boolean }[] = [];
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 192 private gridNote: HTMLElement | null = null;
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 193 private hitsNote: HTMLElement | null = null;
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 194 private dampingNote: HTMLElement | null = null;
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 195 private numericsNote: HTMLElement | null = null;
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 196 private shapeNote: HTMLElement | null = null;
197 private featureList: HTMLElement | null = null;
198 private featureControls: (HTMLInputElement | HTMLSelectElement | HTMLButtonElement)[] = [];
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 199 private omegaCache: { key: string; omega: number } | null = null;
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 200 private liveKeys = new Set<keyof PhysParams>();
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 201 private enabled = true;
203 constructor(root: HTMLElement, params: PhysParams) {
204 this.params = params;
205 for (const g of groups) {
206 const det = document.createElement("details");
207 det.open = g.open;
208 const sum = document.createElement("summary");
209 sum.textContent = g.title;
210 det.appendChild(sum);
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 211 for (const s of g.selects ?? []) {
212 const row = this.selectRow(s);
213 if (s.show) this.conditional.push({ el: row, show: s.show });
214 det.appendChild(row);
215 }
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 216 for (const f of g.fields) {
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 217 const row = this.fieldRow(f);
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 218 if (f.show) this.conditional.push({ el: row, show: f.show });
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 219 if (f.live) this.liveKeys.add(f.key);
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 222 if (g.features) det.appendChild(this.featureSection());
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 223 if (g.note) {
224 const note = document.createElement("p");
225 note.className = "sub";
226 if (g.note === "grid") this.gridNote = note;
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 227 else if (g.note === "hits") this.hitsNote = note;
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 228 else if (g.note === "damping") this.dampingNote = note;
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 229 else if (g.note === "numerics") this.numericsNote = note;
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 230 else if (g.note === "shape") this.shapeNote = note;
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 231 else note.textContent = g.note;
232 det.appendChild(note);
233 }
234 root.appendChild(det);
235 }
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 237 this.updateNotes();
238 }
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 240 private selectRow(s: SelectSpec): HTMLElement {
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 241 const row = document.createElement("div");
242 row.className = "field";
243 const l = document.createElement("label");
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 245 const sel = document.createElement("select");
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 246 for (const [v, t] of s.options) {
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 247 const o = document.createElement("option");
248 o.value = v; o.textContent = t;
249 sel.appendChild(o);
250 }
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 251 sel.value = String(this.params[s.key]);
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 252 sel.addEventListener("change", () => {
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 253 (this.params as unknown as Record<string, string | boolean>)[s.key] = parseSelect(s.key, sel.value);
254 this.updateVisibility();
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 255 this.updateNotes();
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 256 this.onChange(s.key, false);
258 row.append(l, sel, document.createElement("span"));
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 259 this.inputs.set(s.key, sel);
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 260 return row;
261 }
263 private fieldRow(f: FieldSpec): HTMLElement {
264 const row = document.createElement("div");
265 row.className = "field";
266 const label = document.createElement("label");
267 label.textContent = f.label;
268 const inp = document.createElement("input");
269 inp.type = "number";
270 inp.step = String(f.step ?? 1);
271 inp.value = formatNumber((this.params[f.key] as number) / this.scale(f));
272 inp.addEventListener("change", () => {
273 const v = parseFloat(inp.value);
274 if (!isFinite(v)) return;
275 (this.params as unknown as Record<string, number>)[f.key] = v * this.scale(f);
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 276 this.updateVisibility();
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 277 this.updateNotes();
278 this.onChange(f.key, !!f.live);
279 });
280 this.inputs.set(f.key, inp);
281 const unit = document.createElement("span");
282 unit.className = "unit";
283 if (f.unit === "size") {
284 // Width and height share one unit selector (inches by default), shown on the first of them.
285 const sel = this.sizeUnitSelect;
286 if (sel.options.length === 0) {
287 for (const u of ["in", "mm"] as SizeUnit[]) {
288 const o = document.createElement("option");
289 o.value = u; o.textContent = u;
290 sel.appendChild(o);
291 }
292 sel.value = this.sizeUnit;
659c2adForm: room for the width/height unit selectorJeremy Magland 293 sel.className = "unit-select";
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 294 sel.addEventListener("change", () => {
295 this.sizeUnit = sel.value as SizeUnit;
296 for (const el of this.sizeUnitLabels) el.textContent = this.sizeUnit;
297 this.refresh();
298 });
299 row.append(label, inp, sel);
300 return row;
301 }
302 unit.textContent = this.sizeUnit;
303 this.sizeUnitLabels.push(unit);
304 } else {
305 unit.textContent = f.unit;
306 }
307 row.append(label, inp, unit);
308 return row;
309 }
311 private scale(f: FieldSpec): number {
312 return f.unit === "size" ? SIZE_SCALE[this.sizeUnit] : f.scale;
313 }
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 315 // ---------- Initial-shape feature list ----------
317 /** The list of features, the add row, and the randomize row. */
318 private featureSection(): HTMLElement {
319 const wrap = document.createElement("div");
320 this.featureList = document.createElement("div");
321 wrap.appendChild(this.featureList);
323 const addRow = document.createElement("div");
324 addRow.className = "feature-row";
325 const typeSel = document.createElement("select");
326 for (const t of FEATURE_TYPES) {
327 const o = document.createElement("option");
328 o.value = t; o.textContent = FEATURE_SPECS[t].label;
329 typeSel.appendChild(o);
330 }
331 const btnAdd = document.createElement("button");
332 btnAdd.textContent = "Add";
333 btnAdd.addEventListener("click", () => {
334 this.params.features.push(newFeature(typeSel.value as FeatureType));
335 this.featuresChanged();
336 });
337 const btnFlat = document.createElement("button");
338 btnFlat.textContent = "Clear (flat)";
339 btnFlat.addEventListener("click", () => {
340 this.params.features = [];
341 this.featuresChanged();
342 });
343 addRow.append(typeSel, btnAdd, btnFlat);
344 wrap.appendChild(addRow);
346 const rndRow = document.createElement("div");
347 rndRow.className = "feature-row";
348 const btnRnd = document.createElement("button");
349 btnRnd.textContent = "Randomize";
350 btnRnd.title = "Draw a new random combination of features with heights of the order of the scale";
351 const scaleLabel = document.createElement("label");
352 scaleLabel.textContent = "scale";
353 const scaleInp = document.createElement("input");
354 scaleInp.type = "number";
355 scaleInp.step = "0.5";
356 scaleInp.value = formatNumber(this.randomScale * 1e3);
357 scaleInp.addEventListener("change", () => {
358 const v = parseFloat(scaleInp.value);
359 if (isFinite(v) && v > 0) this.randomScale = v * 1e-3;
360 });
361 const scaleUnit = document.createElement("span");
362 scaleUnit.className = "unit";
363 scaleUnit.textContent = "mm";
364 btnRnd.addEventListener("click", () => {
365 this.params.features = randomFeatures(this.params, this.randomScale);
366 this.featuresChanged();
367 });
368 rndRow.append(btnRnd, scaleLabel, scaleInp, scaleUnit);
369 wrap.appendChild(rndRow);
370 this.persistentFeatureControls = [typeSel, btnAdd, btnFlat, btnRnd, scaleInp];
371 this.renderFeatures();
372 return wrap;
373 }
375 private persistentFeatureControls: (HTMLInputElement | HTMLSelectElement | HTMLButtonElement)[] = [];
377 /** Rebuilds the feature rows from params.features. */
378 private renderFeatures(): void {
379 const list = this.featureList;
380 if (!list) return;
381 list.replaceChildren();
382 this.featureControls = [...this.persistentFeatureControls];
383 const features = this.params.features;
384 if (features.length === 0) {
385 const p = document.createElement("p");
386 p.className = "sub";
387 p.textContent = "Flat sheet (no features).";
388 list.appendChild(p);
389 }
390 features.forEach((feat, index) => {
391 const spec = FEATURE_SPECS[feat.type];
392 const box = document.createElement("div");
393 box.className = "feature";
394 box.title = spec.description;
395 const head = document.createElement("div");
396 head.className = "feature-head";
397 const title = document.createElement("span");
398 title.textContent = `${index + 1}. ${spec.label}`;
399 const btnRemove = document.createElement("button");
400 btnRemove.className = "small";
401 btnRemove.textContent = "Remove";
402 btnRemove.addEventListener("click", () => {
403 this.params.features.splice(index, 1);
404 this.featuresChanged();
405 });
406 head.append(title, btnRemove);
407 box.appendChild(head);
408 this.featureControls.push(btnRemove);
409 const rec = feat as unknown as Record<string, number | string>;
410 for (const fld of spec.fields) {
411 const row = document.createElement("div");
412 row.className = "field";
413 const label = document.createElement("label");
414 label.textContent = fld.label;
415 const unit = document.createElement("span");
416 unit.className = "unit";
417 unit.textContent = fld.unit;
418 if (fld.options) {
419 const sel = document.createElement("select");
420 for (const [v, t] of fld.options) {
421 const o = document.createElement("option");
422 o.value = v; o.textContent = t;
423 sel.appendChild(o);
424 }
425 sel.value = String(rec[fld.key]);
426 sel.addEventListener("change", () => {
427 rec[fld.key] = sel.value;
428 this.featuresChanged(false);
429 });
430 row.append(label, sel, unit);
431 this.featureControls.push(sel);
432 } else {
433 const inp = document.createElement("input");
434 inp.type = "number";
435 inp.step = String(fld.step);
436 inp.value = formatNumber((rec[fld.key] as number) / fld.scale);
437 inp.addEventListener("change", () => {
438 const v = parseFloat(inp.value);
439 if (!isFinite(v)) return;
440 rec[fld.key] = v * fld.scale;
441 this.featuresChanged(false);
442 });
443 row.append(label, inp, unit);
444 this.featureControls.push(inp);
445 }
446 box.appendChild(row);
447 }
448 list.appendChild(box);
449 });
450 for (const el of this.featureControls) el.disabled = !this.enabled;
451 }
453 /** After a change of the feature list: rebuild the rows (unless only a value changed), refresh the note, and notify. */
454 private featuresChanged(rebuild = true): void {
455 if (rebuild) this.renderFeatures();
456 this.updateNotes();
457 this.onChange("features", false);
458 }
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 460 /** Shows the fields that apply to the selected excitation and hit sequence only. */
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 461 private updateVisibility(): void {
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 462 for (const c of this.conditional) c.el.hidden = !c.show(this.params);
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 465 /** Rewrites every input from the current parameter values. */
466 refresh(): void {
467 for (const g of groups) {
468 for (const f of g.fields) {
469 const inp = this.inputs.get(f.key) as HTMLInputElement;
470 inp.value = formatNumber((this.params[f.key] as number) / this.scale(f));
471 }
472 }
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 473 for (const k of selectKeys) (this.inputs.get(k) as HTMLSelectElement).value = String(this.params[k]);
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 474 this.renderFeatures();
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 476 this.updateNotes();
477 }
479 /** Enables or disables the fields that cannot change while a simulation runs. */
480 setEnabled(enabled: boolean): void {
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 481 this.enabled = enabled;
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 482 for (const [key, el] of this.inputs) if (!this.liveKeys.has(key)) el.disabled = !enabled;
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 483 for (const el of this.featureControls) el.disabled = !enabled;
487 * Applies URL parameters: any PhysParams key in SI units (booleans as 0/1 or
488 * false/true), `features` in the text form of shape.ts, and the earlier
489 * `bowShape` and `bowAmplitude` keys. Returns the keys that were set.
490 */
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 491 applyUrl(q: URLSearchParams): (keyof PhysParams)[] {
492 const set: (keyof PhysParams)[] = [];
493 for (const [k, v] of q) {
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 494 if (k === "features") {
495 const f = parseFeatures(v);
496 if (f) { this.params.features = f; set.push("features"); }
497 continue;
498 }
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 499 if (!(k in this.params)) continue;
500 const key = k as keyof PhysParams;
282f706Single excitation panel; rigid-body projection optional while ringingJeremy Magland 501 if (selectKeys.includes(key)) {
502 const norm = v === "1" ? "true" : v === "0" ? "false" : v;
503 if (selectValues[key].includes(norm)) {
504 (this.params as unknown as Record<string, string | boolean>)[key] = parseSelect(key, norm);
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 505 set.push(key);
506 }
507 } else {
508 const num = parseFloat(v);
509 if (isFinite(num)) {
510 (this.params as unknown as Record<string, number>)[key] = num;
511 set.push(key);
512 }
513 }
514 }
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 515 if (!set.includes("features") && (q.has("bowShape") || q.has("bowAmplitude"))) {
516 const amp = parseFloat(q.get("bowAmplitude") ?? "");
517 this.params.features = legacyFeatures(q.get("bowShape") ?? "width", isFinite(amp) ? amp : 3e-3);
518 set.push("features");
519 }
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 520 this.refresh();
521 return set;
522 }
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 524 /** Query string (without the leading ?) of every parameter that differs from its default, in the form applyUrl reads. */
525 toQuery(): string {
526 const parts: string[] = [];
527 const enc = (s: string) => encodeURIComponent(s).replace(/%3A/gi, ":").replace(/%2C/gi, ",").replace(/%3B/gi, ";");
528 for (const k of Object.keys(defaultParams) as (keyof PhysParams)[]) {
529 if (k === "features") {
530 const e = encodeFeatures(this.params.features);
531 if (e !== encodeFeatures(defaultParams.features)) parts.push(`features=${enc(e)}`);
532 } else if (this.params[k] !== defaultParams[k]) {
533 const v = this.params[k];
534 parts.push(`${k}=${enc(typeof v === "number" ? formatSI(v) : String(v))}`);
535 }
536 }
537 return parts.join("&");
538 }
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 540 gridInfo(): { grid: Grid; inside: number } {
541 const grid = makeGrid(this.params);
542 return { grid, inside: countInside(grid) };
543 }
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 545 /** Rewrites the derived-quantity notes; call after a change made outside the form (the recorded duration). */
546 updateNotes(): void {
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 547 const p = this.params;
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 548 if (this.hitsNote) {
549 if (p.excitation !== "strike") {
550 this.hitsNote.textContent = "";
551 } else {
552 const hits = hitSchedule(p);
553 const last = hits[hits.length - 1];
554 const decay = p.duration - last.t;
555 const speeds = hits.length <= 8 ? hits.map((h) => h.v.toFixed(1)).join(", ") : `${hits[0].v.toFixed(1)} ... ${last.v.toFixed(1)}`;
556 this.hitsNote.textContent =
557 hits.length === 1
558 ? `One hit at ${speeds} m/s; the recorded duration of ${p.duration} s starts at the impact.`
559 : `${hits.length} hits at ${speeds} m/s, the last at ${last.t.toFixed(2)} s. ` +
560 (decay > 0
561 ? `The recorded duration of ${p.duration} s leaves ${decay.toFixed(2)} s of ring after the last hit.`
562 : `The recorded duration of ${p.duration} s ends before the last hit; raise it to hear the whole series.`);
563 }
564 }
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 565 if (this.gridNote) {
566 const { grid, inside } = this.gridInfo();
567 const m = makeMaterial(p);
568 const f3 = dispersionFrequency(m, grid, 0.03), f10 = dispersionFrequency(m, grid, 0.1);
569 const outline = p.shape === "ellipse" ? "ellipse with a stepped edge" : "rectangle";
570 this.gridNote.textContent = `${outline}; ${grid.nx} × ${grid.ny} grid, ${inside} nodes in the sheet. Bending partials come out low by 3% near ${(f3 / 1e3).toFixed(1)} kHz and by 10% near ${(f10 / 1e3).toFixed(1)} kHz (grid dispersion).`;
571 }
573 const { grid } = this.gridInfo();
574 const { peakToPeak, maxSlope } = shapeStats(initialShape(p, grid), grid);
575 const base = "Real sheet metal is never flat. The features are summed and the mean plane is removed; only the slopes matter to the physics. A negative push amount pushes against a bow across the width.";
576 const stats = p.features.length === 0 ? "" : ` This shape spans ${(peakToPeak * 1e3).toFixed(2)} mm peak to peak with a largest slope of ${maxSlope.toFixed(3)}`;
577 const warn = maxSlope > SLOPE_LIMIT ? `, beyond the shallow-shell range of about ${SLOPE_LIMIT}; reduce the heights or widen the features.` : stats ? "." : "";
578 this.shapeNote.textContent = base + stats + warn;
579 this.shapeNote.className = maxSlope > SLOPE_LIMIT ? "warn" : "sub";
580 }
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 581 if (this.dampingNote) {
582 const m = makeMaterial(p);
583 const f = (hz: number) => t60(m, p, 2 * Math.PI * hz).toFixed(2);
584 this.dampingNote.textContent = `T60: ${f(100)} s at 100 Hz, ${f(1000)} s at 1 kHz, ${f(5000)} s at 5 kHz.`;
585 }
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 586 if (this.numericsNote) {
587 const { grid } = this.gridInfo();
588 const m = makeMaterial(p);
589 const s = Math.max(1, p.inPlaneMass);
590 // The power iteration takes a moment, so its result is kept until the spacing, the material, or the factor changes.
591 const key = [grid.hx, grid.hy, m.D, m.C, m.G, m.nu, m.rhoh, s].join("|");
592 if (!this.omegaCache || this.omegaCache.key !== key) this.omegaCache = { key, omega: maxAngularFrequency(grid, m, s) };
593 const stepsPerSample = Math.ceil(1 / (p.fs * ((CFL * 2) / this.omegaCache.omega)));
594 const dt = 1 / (p.fs * stepsPerSample);
595 const cp = Math.sqrt(m.C / m.rhoh / s);
596 this.numericsNote.textContent =
597 `Time step ${(dt * 1e6).toFixed(2)} µs (${stepsPerSample} per sample), set by the in-plane wave speed of ${cp.toFixed(0)} m/s. ` +
598 `An in-plane mass factor above 1 slows the in-plane waves by its square root and lengthens the step by up to the same factor, at the cost of lowering the in-plane modes, which the bow couples to the sound. ` +
599 `The excitation forces are self-equilibrated, so the projection while ringing only removes rounding drift of the six rigid modes; it is always applied while the sheet is held.`;
600 }
603 get excitation(): Excitation {
604 return this.params.excitation;
605 }
606 get shape(): PlateShape {
607 return this.params.shape;
608 }
8cb683aInitial shape as a list of features, with random configurationsJeremy Magland 609 get features(): ShapeFeature[] {
610 return this.params.features;
↑↓ move↵openescclose