/ proof-of-concept / steelsim41
Sign in
proof-of-concept / steelsim41
steelsim41 / src / ui / editor.ts
184 lines · 7.8 KBCodeBlameHistory
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 1// The design editor: a preset selector, the script in a CodeMirror view with
2// MATLAB (Octave) syntax highlighting, and a status line. The script is
3// compiled on every edit (debounced); the owner is told about each new
4// design or error and supplies the summary shown under the script.
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 6import { EditorState, Compartment } from "@codemirror/state";
7import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection } from "@codemirror/view";
8import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
9import { StreamLanguage, syntaxHighlighting, HighlightStyle, bracketMatching } from "@codemirror/language";
10import { octave } from "@codemirror/legacy-modes/mode/octave";
11import { tags } from "@lezer/highlight";
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 12import { type Design, DesignError, compileDesign } from "../design.ts";
13import { DEFAULT_PRESET, PRESETS } from "../presets.ts";
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 14import { HELPER_NAMES } from "../mhelpers.ts";
16const CUSTOM = "custom";
17const DEBOUNCE_MS = 400;
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 19/** Colors of the page's dark theme (see style.css). */
20const highlight = HighlightStyle.define([
21 { tag: tags.comment, color: "#7f8a96", fontStyle: "italic" },
22 { tag: tags.keyword, color: "#8fb8f0" },
23 { tag: tags.string, color: "#e6b06b" },
24 { tag: tags.number, color: "#b5cea8" },
25 { tag: tags.operator, color: "#c9ced4" },
26 { tag: tags.variableName, color: "#dfe2e6" },
27 { tag: tags.atom, color: "#c586c0" },
28 { tag: tags.bracket, color: "#c9ced4" },
29]);
31const theme = EditorView.theme(
32 {
33 "&": { backgroundColor: "#0f1113", color: "#dfe2e6", border: "1px solid #2c3036", borderRadius: "4px", fontSize: "12px", height: "340px" },
34 "&.cm-focused": { outline: "1px solid #3b8ce6" },
35 ".cm-scroller": { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", lineHeight: "1.4", overflow: "auto" },
36 ".cm-content": { caretColor: "#f0b429" },
37 ".cm-cursor, .cm-dropCursor": { borderLeftColor: "#f0b429" },
38 "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": { backgroundColor: "#2b4a70 !important" },
39 ".cm-activeLine": { backgroundColor: "#1a1d21" },
40 ".cm-gutters": { backgroundColor: "#15171a", color: "#5c646d", border: "none", borderRight: "1px solid #2c3036" },
41 ".cm-activeLineGutter": { backgroundColor: "#1a1d21", color: "#9aa0a8" },
42 ".cm-matchingBracket": { backgroundColor: "#2b4a70", outline: "none" },
43 "&.cm-editor.cm-disabled": { opacity: "0.6" },
44 },
45 { dark: true },
46);
49 /** Called with the compiled design, or null and the message when the script fails. */
50 onDesign: (design: Design | null, error: string | null) => void = () => {};
52 readonly select = document.createElement("select");
54 private editable = new Compartment();
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 55 private status = document.createElement("div");
56 private summary = document.createElement("div");
57 private timer: number | null = null;
58 private lastSource = "";
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 59 /** Set while the text is being replaced programmatically, so the change is not debounced as an edit. */
60 private replacing = false;
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 61 design: Design | null = null;
62 error: string | null = null;
64 constructor(root: HTMLElement) {
65 const head = document.createElement("div");
66 head.className = "editor-head";
67 const label = document.createElement("label");
68 label.textContent = "Preset";
69 for (const p of PRESETS) {
70 const o = document.createElement("option");
71 o.value = p.id; o.textContent = p.label;
72 this.select.appendChild(o);
73 }
74 const custom = document.createElement("option");
75 custom.value = CUSTOM; custom.textContent = "Custom (edited)";
76 this.select.appendChild(custom);
77 this.select.value = DEFAULT_PRESET.id;
78 this.select.addEventListener("change", () => {
79 const p = PRESETS.find((q) => q.id === this.select.value);
80 if (!p) return;
83 });
84 const btnReset = document.createElement("button");
85 btnReset.textContent = "Revert";
86 btnReset.title = "Restore the selected preset's script";
87 btnReset.addEventListener("click", () => {
88 const p = PRESETS.find((q) => q.id === this.select.value) ?? DEFAULT_PRESET;
91 this.compileNow();
92 });
93 head.append(label, this.select, btnReset);
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 95 const wrap = document.createElement("div");
96 wrap.className = "script";
97 this.view = new EditorView({
98 parent: wrap,
99 state: EditorState.create({
100 doc: DEFAULT_PRESET.source,
101 extensions: [
102 lineNumbers(),
103 highlightActiveLineGutter(),
104 highlightActiveLine(),
105 drawSelection(),
106 history(),
107 bracketMatching(),
108 StreamLanguage.define(octave),
109 syntaxHighlighting(highlight),
110 theme,
111 this.editable.of(EditorView.editable.of(true)),
112 keymap.of([
113 { key: "Mod-Enter", run: () => { this.compileNow(); return true; } },
114 indentWithTab,
115 ...defaultKeymap,
116 ...historyKeymap,
117 ]),
118 EditorView.updateListener.of((u) => {
119 if (!u.docChanged || this.replacing) return;
120 const src = this.source;
121 if (this.select.value !== CUSTOM && src !== PRESETS.find((q) => q.id === this.select.value)?.source) this.select.value = CUSTOM;
122 if (this.timer !== null) window.clearTimeout(this.timer);
123 this.timer = window.setTimeout(() => this.compileNow(), DEBOUNCE_MS);
124 }),
125 ],
126 }),
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 129 this.status.className = "editor-status sub";
130 this.summary.className = "editor-summary";
131 const hint = document.createElement("p");
132 hint.className = "sub";
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 133 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(", ")}.`;
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 134 root.append(head, wrap, this.status, this.summary, hint);
135 }
137 /** Replaces the whole text without triggering the debounced compile. */
138 private setText(source: string): void {
139 this.replacing = true;
140 this.view.dispatch({ changes: { from: 0, to: this.view.state.doc.length, insert: source } });
141 this.replacing = false;
144 /** Compiles the current text at once (no debounce) and notifies the owner if anything changed. */
145 compileNow(): void {
146 if (this.timer !== null) { window.clearTimeout(this.timer); this.timer = null; }
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 148 if (src === this.lastSource) return;
149 this.lastSource = src;
150 try {
151 this.design = compileDesign(src);
152 this.error = null;
153 } catch (e) {
154 this.design = null;
155 this.error = e instanceof DesignError ? e.message : String(e);
156 }
157 this.setStatus(this.error, "");
158 this.onDesign(this.design, this.error);
159 }
161 /** Replaces the script (from a preset id or a source string) and compiles it. */
162 setScript(source: string, presetId: string | null = null): void {
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 164 this.select.value = presetId && PRESETS.some((p) => p.id === presetId) ? presetId : CUSTOM;
165 this.compileNow();
166 }
168 get source(): string {
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 169 return this.view.state.doc.toString();
172 /** Shows an error (red) or a message, and the summary HTML below it. */
173 setStatus(error: string | null, summaryHtml: string): void {
174 this.status.textContent = error ?? "";
175 this.status.className = error ? "editor-status err" : "editor-status sub";
176 this.summary.innerHTML = summaryHtml;
177 }
179 setEnabled(enabled: boolean): void {
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 180 this.view.dispatch({ effects: this.editable.reconfigure(EditorView.editable.of(enabled)) });
181 this.view.dom.classList.toggle("cm-disabled", !enabled);
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 182 this.select.disabled = !enabled;
183 }
↑↓ move↵openescclose