/ proof-of-concept / steelsim41
Sign in
proof-of-concept / steelsim41
steelsim41 / src / ui / editor.ts
126 lines · 5.2 KBCodeBlameHistory
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 1// The design editor: a preset selector, the script text, and a status line.
2// The script is compiled on every edit (debounced); the owner is told about
3// each new design or error and supplies the summary shown under the script.
5import { type Design, DesignError, compileDesign } from "../design.ts";
6import { DEFAULT_PRESET, PRESETS } from "../presets.ts";
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 7import { HELPER_NAMES } from "../mhelpers.ts";
9const CUSTOM = "custom";
10const DEBOUNCE_MS = 400;
12export class DesignEditor {
13 /** Called with the compiled design, or null and the message when the script fails. */
14 onDesign: (design: Design | null, error: string | null) => void = () => {};
16 readonly select = document.createElement("select");
17 readonly textarea = document.createElement("textarea");
18 private status = document.createElement("div");
19 private summary = document.createElement("div");
20 private timer: number | null = null;
21 private lastSource = "";
22 design: Design | null = null;
23 error: string | null = null;
25 constructor(root: HTMLElement) {
26 const head = document.createElement("div");
27 head.className = "editor-head";
28 const label = document.createElement("label");
29 label.textContent = "Preset";
30 for (const p of PRESETS) {
31 const o = document.createElement("option");
32 o.value = p.id; o.textContent = p.label;
33 this.select.appendChild(o);
34 }
35 const custom = document.createElement("option");
36 custom.value = CUSTOM; custom.textContent = "Custom (edited)";
37 this.select.appendChild(custom);
38 this.select.value = DEFAULT_PRESET.id;
39 this.select.addEventListener("change", () => {
40 const p = PRESETS.find((q) => q.id === this.select.value);
41 if (!p) return;
42 this.textarea.value = p.source;
43 this.compileNow();
44 });
45 const btnReset = document.createElement("button");
46 btnReset.textContent = "Revert";
47 btnReset.title = "Restore the selected preset's script";
48 btnReset.addEventListener("click", () => {
49 const p = PRESETS.find((q) => q.id === this.select.value) ?? DEFAULT_PRESET;
50 this.textarea.value = p.source;
51 this.select.value = p.id;
52 this.compileNow();
53 });
54 head.append(label, this.select, btnReset);
56 this.textarea.className = "script";
57 this.textarea.spellcheck = false;
58 this.textarea.wrap = "off";
59 this.textarea.value = DEFAULT_PRESET.source;
60 this.textarea.addEventListener("input", () => {
61 if (this.select.value !== CUSTOM && this.textarea.value !== PRESETS.find((q) => q.id === this.select.value)?.source) this.select.value = CUSTOM;
62 if (this.timer !== null) window.clearTimeout(this.timer);
63 this.timer = window.setTimeout(() => this.compileNow(), DEBOUNCE_MS);
64 });
65 this.textarea.addEventListener("keydown", (e) => {
66 if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
67 e.preventDefault();
68 this.compileNow();
69 } else if (e.key === "Tab") {
70 // Insert two spaces rather than leaving the field.
71 e.preventDefault();
72 const t = this.textarea;
73 const s = t.selectionStart, end = t.selectionEnd;
74 t.value = t.value.slice(0, s) + " " + t.value.slice(end);
75 t.selectionStart = t.selectionEnd = s + 2;
76 t.dispatchEvent(new Event("input"));
77 }
78 });
79 this.status.className = "editor-status sub";
80 this.summary.className = "editor-summary";
81 const hint = document.createElement("p");
82 hint.className = "sub";
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 83 hint.textContent = `MATLAB syntax (run by numbl). The script fills in the struct design: material, bounds, inside, thickness, profile, mount, and loss, in SI units; it is applied as you type (or with Ctrl+Enter). The thickness, profile, and inside fields are numbers or handles @(x, y, r, theta) applied to whole grid matrices, so use .*, ./, .^ in them. Helpers: ${HELPER_NAMES.join(", ")}.`;
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 84 root.append(head, this.textarea, this.status, this.summary, hint);
85 }
87 /** Compiles the current text at once (no debounce) and notifies the owner if anything changed. */
88 compileNow(): void {
89 if (this.timer !== null) { window.clearTimeout(this.timer); this.timer = null; }
90 const src = this.textarea.value;
91 if (src === this.lastSource) return;
92 this.lastSource = src;
93 try {
94 this.design = compileDesign(src);
95 this.error = null;
96 } catch (e) {
97 this.design = null;
98 this.error = e instanceof DesignError ? e.message : String(e);
99 }
100 this.setStatus(this.error, "");
101 this.onDesign(this.design, this.error);
102 }
104 /** Replaces the script (from a preset id or a source string) and compiles it. */
105 setScript(source: string, presetId: string | null = null): void {
106 this.textarea.value = source;
107 this.select.value = presetId && PRESETS.some((p) => p.id === presetId) ? presetId : CUSTOM;
108 this.compileNow();
109 }
111 get source(): string {
112 return this.textarea.value;
113 }
115 /** Shows an error (red) or a message, and the summary HTML below it. */
116 setStatus(error: string | null, summaryHtml: string): void {
117 this.status.textContent = error ?? "";
118 this.status.className = error ? "editor-status err" : "editor-status sub";
119 this.summary.innerHTML = summaryHtml;
120 }
122 setEnabled(enabled: boolean): void {
123 this.textarea.disabled = !enabled;
124 this.select.disabled = !enabled;
125 }
↑↓ move↵openescclose