/ proof-of-concept / steelsim41
Sign in
proof-of-concept / steelsim41
steelsim41 / src / ui / editor.ts
189 lines · 8.2 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";
15const CUSTOM = "custom";
16const DEBOUNCE_MS = 400;
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 18/** Colors of the page's dark theme (see style.css). */
19const highlight = HighlightStyle.define([
20 { tag: tags.comment, color: "#7f8a96", fontStyle: "italic" },
21 { tag: tags.keyword, color: "#8fb8f0" },
22 { tag: tags.string, color: "#e6b06b" },
23 { tag: tags.number, color: "#b5cea8" },
24 { tag: tags.operator, color: "#c9ced4" },
25 { tag: tags.variableName, color: "#dfe2e6" },
26 { tag: tags.atom, color: "#c586c0" },
27 { tag: tags.bracket, color: "#c9ced4" },
28]);
30const theme = EditorView.theme(
31 {
32 "&": { backgroundColor: "#0f1113", color: "#dfe2e6", border: "1px solid #2c3036", borderRadius: "4px", fontSize: "12px", height: "340px" },
33 "&.cm-focused": { outline: "1px solid #3b8ce6" },
34 ".cm-scroller": { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", lineHeight: "1.4", overflow: "auto" },
35 ".cm-content": { caretColor: "#f0b429" },
36 ".cm-cursor, .cm-dropCursor": { borderLeftColor: "#f0b429" },
37 "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": { backgroundColor: "#2b4a70 !important" },
38 ".cm-activeLine": { backgroundColor: "#1a1d21" },
39 ".cm-gutters": { backgroundColor: "#15171a", color: "#5c646d", border: "none", borderRight: "1px solid #2c3036" },
40 ".cm-activeLineGutter": { backgroundColor: "#1a1d21", color: "#9aa0a8" },
41 ".cm-matchingBracket": { backgroundColor: "#2b4a70", outline: "none" },
42 "&.cm-editor.cm-disabled": { opacity: "0.6" },
43 },
44 { dark: true },
45);
48 /** Called with the compiled design, or null and the message when the script fails. */
49 onDesign: (design: Design | null, error: string | null) => void = () => {};
51 readonly select = document.createElement("select");
53 private editable = new Compartment();
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 54 private status = document.createElement("div");
55 private summary = document.createElement("div");
56 private timer: number | null = null;
57 private lastSource = "";
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 58 /** Set while the text is being replaced programmatically, so the change is not debounced as an edit. */
59 private replacing = false;
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 60 design: Design | null = null;
61 error: string | null = null;
63 constructor(root: HTMLElement) {
64 const head = document.createElement("div");
65 head.className = "editor-head";
66 const label = document.createElement("label");
67 label.textContent = "Preset";
68 for (const p of PRESETS) {
69 const o = document.createElement("option");
70 o.value = p.id; o.textContent = p.label;
71 this.select.appendChild(o);
72 }
73 const custom = document.createElement("option");
74 custom.value = CUSTOM; custom.textContent = "Custom (edited)";
75 this.select.appendChild(custom);
76 this.select.value = DEFAULT_PRESET.id;
77 this.select.addEventListener("change", () => {
78 const p = PRESETS.find((q) => q.id === this.select.value);
79 if (!p) return;
82 });
83 const btnReset = document.createElement("button");
84 btnReset.textContent = "Revert";
85 btnReset.title = "Restore the selected preset's script";
86 btnReset.addEventListener("click", () => {
87 const p = PRESETS.find((q) => q.id === this.select.value) ?? DEFAULT_PRESET;
90 this.compileNow();
91 });
92 head.append(label, this.select, btnReset);
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 94 const wrap = document.createElement("div");
95 wrap.className = "script";
96 this.view = new EditorView({
97 parent: wrap,
98 state: EditorState.create({
99 doc: DEFAULT_PRESET.source,
100 extensions: [
101 lineNumbers(),
102 highlightActiveLineGutter(),
103 highlightActiveLine(),
104 drawSelection(),
105 history(),
106 bracketMatching(),
107 StreamLanguage.define(octave),
108 syntaxHighlighting(highlight),
109 theme,
110 this.editable.of(EditorView.editable.of(true)),
111 keymap.of([
112 { key: "Mod-Enter", run: () => { this.compileNow(); return true; } },
113 indentWithTab,
114 ...defaultKeymap,
115 ...historyKeymap,
116 ]),
117 EditorView.updateListener.of((u) => {
118 if (!u.docChanged || this.replacing) return;
119 const src = this.source;
120 if (this.select.value !== CUSTOM && src !== PRESETS.find((q) => q.id === this.select.value)?.source) this.select.value = CUSTOM;
121 if (this.timer !== null) window.clearTimeout(this.timer);
122 this.timer = window.setTimeout(() => this.compileNow(), DEBOUNCE_MS);
123 }),
124 ],
125 }),
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 128 this.status.className = "editor-status sub";
129 this.summary.className = "editor-summary";
7710254Self-contained design scripts; 3D by default; resizable, less verbose panelJeremy Magland 130 // How to write a script: useful once, in the way every time, so it folds away.
131 const hint = document.createElement("details");
132 hint.className = "blurb";
133 const hintSum = document.createElement("summary");
134 hintSum.textContent = "Writing a script";
135 const hintText = document.createElement("p");
136 hintText.className = "sub";
137 hintText.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. Each script is self-contained: nothing is in scope but numbl's own functions, so define the shapes you need as ordinary MATLAB functions at the bottom of the script.";
138 hint.append(hintSum, hintText);
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 139 root.append(head, wrap, this.status, this.summary, hint);
140 }
142 /** Replaces the whole text without triggering the debounced compile. */
143 private setText(source: string): void {
144 this.replacing = true;
145 this.view.dispatch({ changes: { from: 0, to: this.view.state.doc.length, insert: source } });
146 this.replacing = false;
149 /** Compiles the current text at once (no debounce) and notifies the owner if anything changed. */
150 compileNow(): void {
151 if (this.timer !== null) { window.clearTimeout(this.timer); this.timer = null; }
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 153 if (src === this.lastSource) return;
154 this.lastSource = src;
155 try {
156 this.design = compileDesign(src);
157 this.error = null;
158 } catch (e) {
159 this.design = null;
160 this.error = e instanceof DesignError ? e.message : String(e);
161 }
162 this.setStatus(this.error, "");
163 this.onDesign(this.design, this.error);
164 }
166 /** Replaces the script (from a preset id or a source string) and compiles it. */
167 setScript(source: string, presetId: string | null = null): void {
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 169 this.select.value = presetId && PRESETS.some((p) => p.id === presetId) ? presetId : CUSTOM;
170 this.compileNow();
171 }
173 get source(): string {
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 174 return this.view.state.doc.toString();
177 /** Shows an error (red) or a message, and the summary HTML below it. */
178 setStatus(error: string | null, summaryHtml: string): void {
179 this.status.textContent = error ?? "";
180 this.status.className = error ? "editor-status err" : "editor-status sub";
181 this.summary.innerHTML = summaryHtml;
182 }
184 setEnabled(enabled: boolean): void {
ccaeb59MATLAB syntax highlighting in the script editor; default exaggeration 1Jeremy Magland 185 this.view.dispatch({ effects: this.editable.reconfigure(EditorView.editable.of(enabled)) });
186 this.view.dom.classList.toggle("cm-disabled", !enabled);
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 187 this.select.disabled = !enabled;
188 }
↑↓ move↵openescclose