diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9839f83..1246e97 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -98,6 +98,7 @@ Collectively they define two class hierarchies: - Two-click hook link system (`connectedCallback` → `_setupHook(hook)` per hook button) - Flow tracking (`wcForeFlow`, `wcNextFlow` arrays of SVG path elements) - Code generation (`generateCode()`, `generateNextCode()`) +- Validation via `_require(selector)` — throws a descriptive `Error` if a required child element is missing from the template, failing early instead of producing `Cannot read properties of null` - Selection on click (`.selected` class, blue box-shadow ring) - Deletion via X button (top-right, shown on hover) or Delete key - X button omitted for `WCStart` / `WCEnd`; their `delete()` shows an error @@ -113,8 +114,9 @@ Collectively they define two class hierarchies: | `WCUnit` | SI unit definition | none (inert hook) | unit comment | | `WCBoolean` | Boolean variable | none (inert hook) | `int name = 0/1;` | | `WCScalar` | Scalar variable with quantity + unit | none (inert hook) | `type name = value;` | +| `WCVector` | N-dimensional vector with quantity + unit | none (inert hook) | `type name[N] = {v0, v1, ...};` | | `WCString` | String variable (char* + length) | none (inert hook) | `string name = {"text", len};` | -| `WCArray` | Typed C array | none | `type name[] = {values};` | +| `WCArray` | Typed C array (supports vector stride) | none | `type name[size];` | | `WCVarRef` | Read-only variable reference in algorithm view | 1 data out | `""` (visual only) | | `WCExpression` | MathJax-rendered expression | dynamic data-in (per variable), 1 data out | `double name = formula;` | | `WCProcedure` | Procedure definition with typed params | none (inert hook) | `""` (comments only) | @@ -124,7 +126,7 @@ Collectively they define two class hierarchies: - `build()` — creates mandatory START/END blocks - `makeNode(tag)` — deep-clones a template by registry lookup, assigns unique ID - `newNode(tag, parent)` — creates a node and appends to the given parent (or registry default) -- Convenience methods: `newDecisionNode`, `newExpressionNode`, `newVarRefNode`, `newOutputNode`, `newProcCallNode`, `newUnitNode`, `newQuantityNode`, `newBooleanNode`, `newScalarNode`, `newStringNode`, `newArrayNode`, `newProcedureNode` +- Convenience methods: `newDecisionNode`, `newExpressionNode`, `newVarRefNode`, `newOutputNode`, `newProcCallNode`, `newUnitNode`, `newQuantityNode`, `newBooleanNode`, `newScalarNode`, `newVectorNode`, `newStringNode`, `newArrayNode`, `newProcedureNode` - `generateCode()` — traverses all blocks and produces complete C source ### `persist.js` — Workspace persistence (localStorage) @@ -258,12 +260,13 @@ WCExpression uses MathJax v3 (`tex-mml-chtml.js`) for client-side LaTeX renderin - **Fallback**: If MathJax is unavailable, `.expr-rendered` shows the raw LaTeX text - **Persistence**: Only the LaTeX string is saved/restored; variable hooks are regenerated from it on load -## Data Type System (WCBoolean / WCScalar / WCString) +## Data Type System (WCBoolean / WCScalar / WCVector / WCString) -Variable blocks come in three distinct types, **not** interchangeable: +Variable blocks come in four distinct types, **not** interchangeable: - **WCBoolean** (`wc-boolean`): A boolean variable. Has a name and a true/false value selector. No unit, no SI conversion, no quantity dropdown. Generates `int name = 0/1;`. - **WCScalar** (`wc-scalar`): A typed scalar variable. Has a name, numeric value, unit dropdown, and quantity selector with SI conversion display. Generates `quantityName name = siValue;`. +- **WCVector** (`wc-vector`): An N-dimensional vector of scalars. Has a name, dimension N, space-separated values, unit dropdown, and quantity selector with SI conversion display. Generates `type name[N] = {v0, v1, ...};`. Data hook type reports as `"scalar"` (varref outputs element values). Index-aware: WCAssign with an index hook assigns to `name[idx]`; without an index hook assigns to `name[0]` or broadcasts via loop. - **WCString** (`wc-string`): A string variable. Has a name and a text value. Generates `string name = {"text", length};`. The `string` struct (`{char *data; long int length;}`) is emitted once before variable declarations when any string blocks exist. Unlike the previous unified `WCDatum` block, there is no way to switch between types — they are separate custom elements with distinct templates and classes. diff --git a/app.js b/app.js index 9b60846..4de3557 100644 --- a/app.js +++ b/app.js @@ -69,13 +69,8 @@ function createDefaultMeasurements() { { fullname: "katal", type: "catalytic_activity", suffix: "kat", si: "1" }, ]; - const COLS = 4, COL_W = 250, ROW_H = 190, PAD = 10; - const typeRows = Math.ceil(typeDefs.length / COLS); - - const typeTags = typeDefs.map((t, i) => { + const typeTags = typeDefs.map(t => { const el = prog.newQuantityNode(); - el.style.left = PAD + (i % COLS) * COL_W + "px"; - el.style.top = PAD + Math.floor(i / COLS) * ROW_H + "px"; el.querySelector(".quantity-name").value = t.name; for (let j = 0; j < 7; j++) { el.querySelector(`.dim-${j}`).value = t.dims[j]; @@ -84,10 +79,15 @@ function createDefaultMeasurements() { return el; }); - const unitTags = unitDefs.map((u, i) => { + /* Sub-header separating quantities from units */ + const unitHeader = document.createElement("div"); + unitHeader.className = "list-header"; + unitHeader.style.borderTop = "2px solid var(--border)"; + unitHeader.textContent = "Units — Fullname Type Suffix SI Factor Resolved"; + $id("data-area").appendChild(unitHeader); + + const unitTags = unitDefs.map(u => { const el = prog.newUnitNode(); - el.style.left = PAD + (i % COLS) * COL_W + "px"; - el.style.top = PAD + (typeRows + Math.floor(i / COLS)) * ROW_H + "px"; el.querySelector(".unit-fullname").value = u.fullname; el.querySelector(".unit-type").value = u.type; el.querySelector(".unit-suffix").value = u.suffix; @@ -116,154 +116,47 @@ function createDefaultMeasurements() { * @returns {HTMLElement} the #procedures-area element inside the new pane */ function addLibTab(libName, makeActive) { + const tabBar = document.querySelector(".lib-tab-bar"); + if (!tabBar) { showError("Missing .lib-tab-bar element"); return null; } + const paneContainer = $id("pane-procedures"); + if (!paneContainer) { showError("Missing #pane-procedures element"); return null; } + const tab = document.createElement("button"); tab.className = "lib-tab" + (makeActive ? " active" : ""); tab.dataset.libName = libName; tab.textContent = libName; tab.addEventListener("click", () => switchLibTab(libName)); - document.querySelector(".lib-tab-bar").appendChild(tab); + tabBar.appendChild(tab); const pane = document.createElement("div"); pane.className = "lib-pane" + (makeActive ? " active" : ""); pane.dataset.libName = libName; + const hdr = document.createElement("div"); + hdr.className = "list-header"; + hdr.textContent = "Procedure \u00a0 Inputs \u2192 Outputs"; + pane.appendChild(hdr); const area = document.createElement("div"); area.className = "procedures-area"; pane.appendChild(area); - $id("pane-procedures").appendChild(pane); + paneContainer.appendChild(pane); return area; } -// ── Default library creation ────────────────────────────────────────────────── - /** - * Creates the default "stdio" library with a "puts" procedure - * (one string input param, no outputs). + * Creates library tabs from the index, then lazy-loads procedures on first click. */ -function createDefaultLibraries() { - const prog = $id("main-program"); - const libName = "stdio"; - addLibTab(libName, true); - const proc = prog.newProcedureNode(libName); - if (proc) { - proc.querySelector(".proc-name").value = "puts"; - proc._addParamRow(proc.querySelector(".proc-inputs"), "in"); - const row = proc.querySelector(".proc-inputs .proc-param-row"); - if (row) { - row.querySelector(".proc-param-name").value = "str"; - row.querySelector(".proc-param-type").value = "string"; - } +async function initLibraries() { + const index = await loadLibraryIndex(); + let first = true; + for (const entry of index) { + addLibTab(entry.id, first); + first = false; } - refreshProcOptions(); -} - - -// ── OpenGL default procedures ───────────────────────────────────────────────── - -/** - * Creates param rows on a procedure block and sets their name/type values. - * Follows the same pattern as WCProcedure.restoreData(). - * @param {HTMLElement} proc - a wc-procedure element - * @param {[string, string][]} inputs - array of [name, type] pairs - * @param {[string, string][]} outputs - array of [name, type] pairs - */ -function _addProcParams(proc, inputs, outputs) { - for (const [name, type] of inputs) { - proc._addParamRow(proc.querySelector(".proc-inputs"), "in"); - const rows = proc.querySelectorAll(".proc-inputs .proc-param-row"); - const last = rows[rows.length - 1]; - last.querySelector(".proc-param-name").value = name; - last.querySelector(".proc-param-type").value = type; - } - for (const [name, type] of outputs) { - proc._addParamRow(proc.querySelector(".proc-outputs"), "out"); - const rows = proc.querySelectorAll(".proc-outputs .proc-param-row"); - const last = rows[rows.length - 1]; - last.querySelector(".proc-param-name").value = name; - last.querySelector(".proc-param-type").value = type; - } -} - -/** - * Creates default OpenGL procedure definitions organised into three libraries: - * - * glfw — GLFW window management (init, create, swap, poll, close, terminate) - * glad — OpenGL loader initialisation (gladLoadGL) - * gl — OpenGL core functions for a Hello Triangle pipeline - * (shader compile/link, buffer/VAO setup, draw, clear, cleanup) - * - * Also creates a default "vertices" float array with Hello Triangle vertex data - * in the Data tab so the user has a starting point. - * - * Code generation note: - * Output parameters are passed with a `&` prefix in the generated C code - * (e.g. `glCreateShader(type, &shader);`). For OpenGL functions that - * natively return values rather than taking pointer outputs, the generated - * code uses this convention and may need a thin wrapper. - */ -function createDefaultOpenGLProcedures() { - const prog = $id("main-program"); - - const PROC_DEFS = [ - // ── glfw: window management ── - { lib: "glfw", name: "glfwInit", inputs: [], outputs: [["result","int"]] }, - { lib: "glfw", name: "glfwCreateWindow", inputs: [["width","int"],["height","int"],["title","string"]], outputs: [["window","int"]] }, - { lib: "glfw", name: "glfwMakeContextCurrent", inputs: [["window","int"]], outputs: [] }, - { lib: "glfw", name: "glfwSwapBuffers", inputs: [["window","int"]], outputs: [] }, - { lib: "glfw", name: "glfwPollEvents", inputs: [], outputs: [] }, - { lib: "glfw", name: "glfwWindowShouldClose", inputs: [["window","int"]], outputs: [["result","int"]] }, - { lib: "glfw", name: "glfwTerminate", inputs: [], outputs: [] }, - // ── glad: OpenGL loader ── - { lib: "glad", name: "gladLoadGL", inputs: [], outputs: [["result","int"]] }, - // ── gl: OpenGL core Hello Triangle ── - { lib: "gl", name: "glCreateShader", inputs: [["type","unsigned int"]], outputs: [["shader","unsigned int"]] }, - { lib: "gl", name: "glShaderSource", inputs: [["shader","unsigned int"],["count","int"],["source","string"]], outputs: [] }, - { lib: "gl", name: "glCompileShader", inputs: [["shader","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glGetShaderiv", inputs: [["shader","unsigned int"],["pname","unsigned int"]], outputs: [["params","int"]] }, - { lib: "gl", name: "glCreateProgram", inputs: [], outputs: [["program","unsigned int"]] }, - { lib: "gl", name: "glAttachShader", inputs: [["program","unsigned int"],["shader","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glLinkProgram", inputs: [["program","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glGetProgramiv", inputs: [["program","unsigned int"],["pname","unsigned int"]], outputs: [["params","int"]] }, - { lib: "gl", name: "glGenVertexArrays", inputs: [["n","unsigned int"]], outputs: [["arrays","unsigned int"]] }, - { lib: "gl", name: "glBindVertexArray", inputs: [["array","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glGenBuffers", inputs: [["n","unsigned int"]], outputs: [["buffers","unsigned int"]] }, - { lib: "gl", name: "glBindBuffer", inputs: [["target","unsigned int"],["buffer","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glBufferData", inputs: [["target","unsigned int"],["size","int"],["data","string"],["usage","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glVertexAttribPointer", inputs: [["index","unsigned int"],["size","int"],["type","unsigned int"],["normalized","int"],["stride","int"],["pointer","int"]], outputs: [] }, - { lib: "gl", name: "glEnableVertexAttribArray", inputs: [["index","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glUseProgram", inputs: [["program","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glDrawArrays", inputs: [["mode","unsigned int"],["first","int"],["count","int"]], outputs: [] }, - { lib: "gl", name: "glClear", inputs: [["mask","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glClearColor", inputs: [["red","float"],["green","float"],["blue","float"],["alpha","float"]], outputs: [] }, - { lib: "gl", name: "glDeleteShader", inputs: [["shader","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glDeleteProgram", inputs: [["program","unsigned int"]], outputs: [] }, - { lib: "gl", name: "glViewport", inputs: [["x","int"],["y","int"],["width","int"],["height","int"]], outputs: [] }, - ]; - - // Create each library tab once, then populate procedures - const libsDone = {}; - for (const pd of PROC_DEFS) { - if (!libsDone[pd.lib]) { - libsDone[pd.lib] = true; - addLibTab(pd.lib, false); - } - const proc = prog.newProcedureNode(pd.lib); - if (proc) { - proc.querySelector(".proc-name").value = pd.name; - _addProcParams(proc, pd.inputs, pd.outputs); - } - } - refreshProcOptions(); - - // ── Default vertex array for Hello Triangle ── - const arr = prog.newArrayNode(); - if (arr) { - arr.restoreData({ - name: "vertices", - type: "float", - values: ["-0.5", "-0.5", "0.0", "0.5", "-0.5", "0.0", "0.0", "0.5", "0.0"] - }); + if (index.length > 0) { + switchLibTab(index[0].id); + loadLibrary(index[0].id); } } @@ -279,7 +172,7 @@ function switchTab(name) { btn.classList.toggle("active", btn.dataset.tab === name); } for (const pane of document.querySelectorAll(".tab-pane")) { - pane.classList.toggle("active", pane.id === "pane-" + name); + pane.classList.toggle("active", pane.dataset.tab === name); } } @@ -288,9 +181,10 @@ function switchTab(name) { /** Wires all static button click handlers (replaces inline onclick attributes). */ function setupUI() { - function closeVariableSubmenu() { - const sub = document.querySelector(".submenu[data-action='variable-submenu']"); - if (sub) sub.classList.remove("open"); + function closeAllSubmenus() { + for (const sub of document.querySelectorAll(".submenu.open")) { + sub.classList.remove("open"); + } } /* Toolbar block-creation helpers */ @@ -302,118 +196,210 @@ function setupUI() { array: refreshAll, quantity: refreshAll, unit: refreshAll, - proccall: refreshAll, assign: refreshAll }; function handleBlockAction(btn) { const tag = BLOCK_ACTIONS[btn.dataset.action]; if (!tag) return false; const prog = $id("main-program"); + + /* Procedure call: show browser modal first, create block only on selection */ + if (btn.dataset.action === "proccall") { + showProcedureBrowser("", (qname) => { + if (qname) { + const el = prog.newNode(tag); + if (el) { + el.setProcedure(qname); + refreshAll(); + } + } + }); + return true; + } + const el = prog.newNode(tag); if (el && ACTION_REFRESH[btn.dataset.action]) ACTION_REFRESH[btn.dataset.action](); return true; } /* Algorithm pane toolbar */ - const algoToolbar = $id("pane-algorithm").querySelector(".pane-toolbar"); - algoToolbar.addEventListener("click", e => { - const btn = e.target.closest("button"); - if (btn && handleBlockAction(btn)) return; - }); + const algoToolbarPane = $id("pane-algorithm"); + const algoToolbar = algoToolbarPane && algoToolbarPane.querySelector(".pane-toolbar"); + if (algoToolbar) { + algoToolbar.addEventListener("click", e => { + const btn = e.target.closest("button"); + if (!btn) return; + if (handleBlockAction(btn)) return; + if (btn.dataset.action === "add-section" && typeof enterSectionCreateMode === "function") { + enterSectionCreateMode(); + } + }); + } /* Data pane toolbar */ - const dataToolbar = $id("pane-data").querySelector(".pane-toolbar"); - dataToolbar.addEventListener("click", e => { - const btn = e.target.closest("button"); - if (!btn) return; - const action = btn.dataset.action; - const prog = $id("main-program"); - if (action === "variable") { - const sub = document.querySelector(".submenu[data-action='variable-submenu']"); - if (sub) sub.classList.toggle("open"); - } else if (handleBlockAction(btn)) { - closeVariableSubmenu(); - } - }); + const dataToolbarPane = $id("pane-data"); + const dataToolbar = dataToolbarPane && dataToolbarPane.querySelector(".pane-toolbar"); + if (dataToolbar) { + dataToolbar.addEventListener("click", e => { + const btn = e.target.closest("button"); + if (!btn) return; + const prog = $id("main-program"); + if (btn.dataset.action === "variable") { + const sub = document.querySelector(".submenu[data-action='variable-submenu']"); + if (sub) sub.classList.toggle("open"); + } else if (handleBlockAction(btn)) { + closeAllSubmenus(); + } + }); + } /* Measurements pane toolbar */ - const measToolbar = $id("pane-measurements").querySelector(".pane-toolbar"); - measToolbar.addEventListener("click", e => { - const btn = e.target.closest("button"); - if (btn) handleBlockAction(btn); - }); + const measToolbarPane = $id("pane-measurements"); + const measToolbar = measToolbarPane && measToolbarPane.querySelector(".pane-toolbar"); + if (measToolbar) { + measToolbar.addEventListener("click", e => { + const btn = e.target.closest("button"); + if (btn) handleBlockAction(btn); + }); + } /* Procedures pane toolbar */ - const procToolbar = $id("pane-procedures").querySelector(".pane-toolbar"); - procToolbar.addEventListener("click", e => { - const btn = e.target.closest("button"); + const procToolbarPane = $id("pane-procedures"); + const procToolbar = procToolbarPane && procToolbarPane.querySelector(".pane-toolbar"); + if (procToolbar) { + procToolbar.addEventListener("click", e => { + const btn = e.target.closest("button"); + if (!btn) return; + const prog = $id("main-program"); + + if (btn.dataset.action === "add-library") { + const libName = prompt("Library name:", "mylib"); + if (!libName) return; + markLibLoaded(libName); + addLibTab(libName, false); + switchLibTab(libName); + } + + if (btn.dataset.action === "add-procedure") { + const activeLibPane = document.querySelector(".lib-pane.active"); + if (!activeLibPane) return; + const el = prog.newNode("wc-procedure", activeLibPane.querySelector(".procedures-area")); + } + + if (btn.dataset.action === "add-library-index") { + const sub = document.querySelector(".submenu[data-action='library-index-submenu']"); + if (!sub) return; + const index = getLibraryIndex(); + const available = index.filter(e => !isLibraryLoaded(e.id)); + if (available.length === 0) { + showError("All libraries from the index are already loaded."); + return; + } + sub.innerHTML = available.map(e => + `${e.name} (${e.id})` + ).join(""); + if (sub.classList.contains("open")) { + sub.classList.remove("open"); + } else { + closeAllSubmenus(); + sub.classList.add("open"); + } + return; + } + }); + } + + /* Library index submenu item click — load selected library */ + document.addEventListener("click", e => { + const btn = e.target.closest(".submenu[data-action='library-index-submenu'] button"); if (!btn) return; - const prog = $id("main-program"); - - if (btn.dataset.action === "add-library") { - const libName = prompt("Library name:", "mylib"); - if (!libName) return; - addLibTab(libName, false); - switchLibTab(libName); - } - - if (btn.dataset.action === "add-procedure") { - const activeLibPane = document.querySelector(".lib-pane.active"); - if (!activeLibPane) return; - const el = prog.newNode("wc-procedure", activeLibPane.querySelector("#procedures-area")); - if (el) refreshProcOptions(); - } + const libId = btn.dataset.libId; + if (!libId) return; + document.querySelector(".submenu[data-action='library-index-submenu']").classList.remove("open"); + addLibTab(libId, false); + loadLibrary(libId); + switchLibTab(libId); }); - /* Lib tab bar click (delegated) */ - document.querySelector(".lib-tab-bar").addEventListener("click", e => { - const tab = e.target.closest(".lib-tab"); - if (tab) switchLibTab(tab.dataset.libName); - }); + /* Lib tab bar click (delegated) — lazy-load libraries on first click */ + const libTabBar = document.querySelector(".lib-tab-bar"); + if (libTabBar) { + libTabBar.addEventListener("click", async e => { + const tab = e.target.closest(".lib-tab"); + if (!tab) return; + const libId = tab.dataset.libName; + if (!isLibraryLoaded(libId)) { + tab.textContent = libId + " \u2026"; + try { + await loadLibrary(libId); + } catch (e) { + tab.textContent = libId; + } + } + switchLibTab(libId); + }); + } /* Global toolbar */ const globalToolbar = document.querySelector(".global-toolbar"); - globalToolbar.addEventListener("click", e => { - const btn = e.target.closest("button"); - if (!btn) return; - if (btn.dataset.action === "generate") $id("main-program").generateCode(); - if (btn.dataset.action === "save") saveWorkspace(); - if (btn.dataset.action === "load") loadWorkspace(); - if (btn.dataset.action === "new") newWorkspace(); - }); + if (globalToolbar) { + globalToolbar.addEventListener("click", e => { + const btn = e.target.closest("button"); + if (!btn) return; + const prog = $id("main-program"); + if (!prog) { showError("Main program element not found"); return; } + if (btn.dataset.action === "generate") prog.generateCode(); + if (btn.dataset.action === "save") saveWorkspace(); + if (btn.dataset.action === "load") loadWorkspace(); + if (btn.dataset.action === "new") newWorkspace(); + if (btn.dataset.action === "copy-code") copyCode(); + if (btn.dataset.action === "run-tests") { + if (window.APP_TEST_SUITE) APP_TEST_SUITE.run(); + } + }); + } /* Tab bar */ - document.querySelector(".tab-bar").addEventListener("click", e => { - const btn = e.target.closest(".tab-btn"); - if (btn) switchTab(btn.dataset.tab); - closeVariableSubmenu(); - }); + const tabBar = document.querySelector(".tab-bar"); + if (tabBar) { + tabBar.addEventListener("click", e => { + const btn = e.target.closest(".tab-btn"); + if (btn) switchTab(btn.dataset.tab); + closeAllSubmenus(); + }); + } - /* Close variable submenu on outside click */ + /* Close all submenus on outside click */ document.addEventListener("click", e => { - if (!e.target.closest(".btn-group")) closeVariableSubmenu(); + if (!e.target.closest(".btn-group")) closeAllSubmenus(); }); /* Click anywhere in draw-area (except hooks, which stopPropagation) cancels pending link */ - $id("draw-area").addEventListener("click", resetLinkage); + const drawArea = $id("draw-area"); + if (drawArea) drawArea.addEventListener("click", resetLinkage); /* Click on an SVG connection path deletes it */ - $id("connections").addEventListener("click", e => { - const path = e.target.closest("path.flow, path.flow-data"); - if (!path) return; - e.stopPropagation(); - for (const el of document.querySelectorAll(".selected")) el.classList.remove("selected"); - resetLinkage(); - deleteLink(path); - }); + const connectionsSvg = $id("connections"); + if (connectionsSvg) { + connectionsSvg.addEventListener("click", e => { + const path = e.target.closest("path.flow, path.flow-data"); + if (!path) return; + e.stopPropagation(); + for (const el of document.querySelectorAll(".selected")) el.classList.remove("selected"); + resetLinkage(); + deleteLink(path); + }); + } /* Error modal dismiss */ - $("#error-overlay").addEventListener("click", closeError); + const errorOverlay = $("#error-overlay"); + if (errorOverlay) errorOverlay.addEventListener("click", closeError); /* Keyboard: Delete key removes selected block */ document.addEventListener("keydown", ev => { if (ev.key !== "Delete" && ev.key !== "Del") return; if (ev.target.tagName === "INPUT" || ev.target.tagName === "TEXTAREA" || ev.target.tagName === "SELECT") return; + if (window.LOGIC_SECTIONS && LOGIC_SECTIONS.editingId) return; const sel = document.querySelector(".selected"); if (sel && typeof sel.delete === "function") { ev.preventDefault(); @@ -438,14 +424,29 @@ function setupUI() { * wires up all UI event listeners. */ function main() { - $id("main-program").build(); + const prog = $id("main-program"); + if (!prog) { showError("Missing #main-program element"); return; } + window.prog = prog; + prog.build(); createDefaultMeasurements(); - createDefaultLibraries(); - createDefaultOpenGLProcedures(); - if (localStorage.getItem(STORAGE_KEY)) { - loadWorkspace(); + + /* Create default vertex array for Hello Triangle */ + const arr = prog.newArrayNode(); + if (arr) { + arr.restoreData({ + name: "vertices", + type: "float", + values: ["-0.5", "-0.5", "0.0", "0.5", "-0.5", "0.0", "0.0", "0.5", "0.0"] + }); } - setupUI(); + + /* Load library index and create tabs (procedures load on demand) */ + initLibraries().then(() => { + if (localStorage.getItem(STORAGE_KEY)) { + loadWorkspace(); + } + setupUI(); + }); } main(); diff --git a/blocks-algo.js b/blocks-algo.js index 65a9ed5..6218f6a 100644 --- a/blocks-algo.js +++ b/blocks-algo.js @@ -15,6 +15,7 @@ // ── WCStart — program entry point ────────────────────────────────────────── +/** Program entry point. Cannot be deleted. */ class WCStart extends WCBlock { delete() { showError("Cannot delete the start block."); } } @@ -22,14 +23,185 @@ class WCStart extends WCBlock { // ── WCEnd — program exit ─────────────────────────────────────────────────── +/** Program exit block with errno autocomplete. At least one must return 0. */ class WCEnd extends WCBlock { - delete() { showError("Cannot delete the end block."); } - generateOwnCode() { return ""; } + connectedCallback() { + super.connectedCallback(); + this._errnoItems = _ERRNO_ITEMS; + this._selectedCode = 0; + this._el.input = this._require(".end-errno-input"); + this._el.suggestions = this._require(".end-errno-suggestions"); + this._el.desc = this._require(".end-desc-text"); + + this._el.input.addEventListener("input", () => this._onInput()); + this._el.input.addEventListener("focus", () => { if (this._el.suggestions.children.length > 0) this._showSuggestions(); }); + this._el.input.addEventListener("keydown", ev => this._onKeyDown(ev)); + this._el.suggestions.addEventListener("mousedown", ev => { + const sug = ev.target.closest(".end-errno-suggestion"); + if (sug) { + const code = parseInt(sug.dataset.code); + if (!isNaN(code)) this._selectByCode(code); + } + }); + document.addEventListener("click", ev => { + if (!this.contains(ev.target)) this._hideSuggestions(); + }); + + this._el.input.value = "0"; + this._selectByCode(0); + } + + _onInput() { + const q = this._el.input.value.trim().toLowerCase(); + if (!q) { + this._renderSuggestions(this._errnoItems); + this._showSuggestions(); + return; + } + const qCode = parseInt(q); + const matches = this._errnoItems.filter(item => + (!isNaN(qCode) && item.code === qCode) || + item.name.toLowerCase().includes(q) || + item.desc.toLowerCase().includes(q) + ); + this._renderSuggestions(matches); + this._showSuggestions(); + } + + _renderSuggestions(items) { + this._el.suggestions.innerHTML = ""; + for (const item of items) this._addSuggestion(item); + } + + _addSuggestion(item) { + const div = document.createElement("div"); + div.className = "end-errno-suggestion"; + div.dataset.code = item.code; + const codeSpan = document.createElement("span"); + codeSpan.className = "end-suggestion-code"; + codeSpan.textContent = item.code + " " + item.name; + div.appendChild(codeSpan); + const descSpan = document.createElement("span"); + descSpan.className = "end-suggestion-desc"; + descSpan.textContent = item.desc; + div.appendChild(descSpan); + this._el.suggestions.appendChild(div); + } + + _showSuggestions() { this._el.suggestions.style.display = "block"; } + _hideSuggestions() { this._el.suggestions.style.display = "none"; } + + _onKeyDown(ev) { + if (ev.key === "ArrowDown" || ev.key === "ArrowUp") { + ev.preventDefault(); + const items = [...this._el.suggestions.querySelectorAll(".end-errno-suggestion")]; + let idx = items.findIndex(el => el.classList.contains("highlighted")); + if (ev.key === "ArrowDown") idx = Math.min(idx + 1, items.length - 1); + else idx = Math.max(idx - 1, 0); + items.forEach(el => el.classList.remove("highlighted")); + if (items[idx]) { + items[idx].classList.add("highlighted"); + items[idx].scrollIntoView({ block: "nearest" }); + } + } + if (ev.key === "Enter") { + const hl = this._el.suggestions.querySelector(".highlighted"); + if (hl) { + ev.preventDefault(); + this._selectByCode(parseInt(hl.dataset.code)); + } + } + if (ev.key === "Escape") this._hideSuggestions(); + } + + _selectByCode(code) { + this._selectedCode = code; + const item = this._errnoItems.find(i => i.code === code); + if (item) { + this._el.input.value = code + " " + item.name; + this._el.desc.textContent = item.desc; + this.dataset.exitError = code !== 0 ? "true" : ""; + } else { + this._el.input.value = code + " error"; + this._el.desc.textContent = "unknown error"; + this.dataset.exitError = "true"; + } + this._hideSuggestions(); + } + + _enforceZeroEnd() { + if (this._selectedCode === 0) return; + const others = document.querySelectorAll("wc-end"); + for (const other of others) { + if (other !== this && other._selectedCode === 0) return; + } + this._selectByCode(0); + } + + _hasOtherZero() { + const others = document.querySelectorAll("wc-end"); + for (const other of others) { + if (other !== this && other._selectedCode === 0) return true; + } + return false; + } + + delete() { + if (this.id === "main-end") { showError("Cannot delete the default end block."); return; } + if (!this._hasOtherZero()) { showError("At least one end block must return 0."); return; } + super.delete(); + setTimeout(() => { + for (const end of document.querySelectorAll("wc-end")) end._enforceZeroEnd(); + }, 0); + } + + _genStringFrees() { + const freed = new Set(); + let code = ""; + for (const el of document.querySelectorAll("#variable-area > [data-block]")) { + if (el.dataType === "string") { + const name = resolveSourceName(el); + if (name && !freed.has(name)) { + freed.add(name); + code += ` free(${name}.data);\n`; + } + } + } + return code; + } + + generateOwnCode() { + const code = this._selectedCode; + if (code === 0) { + const frees = this._genStringFrees(); + if (frees) return `\n${frees} return 0;\n`; + return "\n return 0;\n"; + } + return `\n exit(${code});\n`; + } + + saveData() { return { exitCode: this._selectedCode }; } + restoreData(d) { + this._selectByCode(d.exitCode !== undefined ? d.exitCode : 0); + } } +function _buildErrnoItems() { + const items = [ + { code: 0, name: "success", desc: "success" }, + { code: 255, name: "error", desc: "generic error" }, + ]; + for (const e of ERRNO_LIST) { + items.push({ code: e.code, name: e.name, desc: e.desc }); + } + return items; +} +const _ERRNO_ITEMS = _buildErrnoItems(); + // ── WCOutput — printf statement ──────────────────────────────────────────── +/** Output description block. Provides inline documentation without generating code. */ class WCOutput extends WCBlock { connectedCallback() { super.connectedCallback(); @@ -48,6 +220,7 @@ class WCOutput extends WCBlock { // ── WCDecision — conditional branch ──────────────────────────────────────── +/** Conditional branch block; splits flow into yes/no branches and emits if/else C code. */ class WCDecision extends WCBlock { connectedCallback() { super.connectedCallback(); @@ -129,6 +302,7 @@ class WCDecision extends WCBlock { // ── WCAssign — variable assignment block ─────────────────────────────────── +/** Variable assignment block; writes to scalar/array/vector/string targets with data hooks. */ class WCAssign extends WCBlock { connectedCallback() { super.connectedCallback(); @@ -238,6 +412,7 @@ class WCAssign extends WCBlock { // ── WCVarRef — read-only variable reference with data output hook ────────── +/** Read-only variable reference with data output hook; syncs type from selected variable. */ class WCVarRef extends WCBlock { connectedCallback() { super.connectedCallback(); @@ -271,6 +446,7 @@ class WCVarRef extends WCBlock { // ── WCExpression — mathematical expression with MathJax rendering & data hooks ── +/** Math expression block with LaTeX rendering via MathJax and dynamic variable data hooks. */ class WCExpression extends WCBlock { get dataType() { const el = this._el?.latex || this.querySelector(".expr-latex"); diff --git a/blocks-data.js b/blocks-data.js index 08ba20d..0788f1b 100644 --- a/blocks-data.js +++ b/blocks-data.js @@ -1,7 +1,7 @@ // ── Data & Measurement tab block definitions ─────────────────────────────── // // Blocks that live in the Data tab (variable area) and Measurements tab: -// WCQuantity, WCUnit, WCBoolean, WCScalar, WCString, WCArray +// WCQuantity, WCUnit, WCBoolean, WCScalar, WCVector, WCString, WCArray // // Each extends WCBlock (from wcblock.js) and provides generateOwnCode(). // @@ -16,11 +16,12 @@ // ── WCQuantity — measurement quantity definition ─────────────────────────── +/** Measurement quantity definition: name, 7 SI base dimensions, min/max range, default unit. */ class WCQuantity extends WCBlock { connectedCallback() { super.connectedCallback(); - this._el.name = this.querySelector(".quantity-name"); - this._el.dims = Array.from({ length: 7 }, (_, i) => this.querySelector(`.dim-${i}`)); + this._el.name = this._require(".quantity-name"); + this._el.dims = Array.from({ length: 7 }, (_, i) => this._require(`.dim-${i}`)); this._el.min = this.querySelector(".quantity-min"); this._el.max = this.querySelector(".quantity-max"); this._el.defaultUnit = this.querySelector(".quantity-default-unit"); @@ -34,9 +35,8 @@ class WCQuantity extends WCBlock { } for (const sel of this.querySelectorAll("select")) { sel.addEventListener("change", () => { - for (const d of $id("variable-area").querySelectorAll("wc-scalar")) { - d.updateSiDisplay(); - } + const va = $id("variable-area"); + if (va) for (const d of va.querySelectorAll("wc-scalar")) d.updateSiDisplay(); }); } setTimeout(() => { refreshQuantityOptions(); refreshDefaultUnitOptions(); }, 0); @@ -52,25 +52,26 @@ class WCQuantity extends WCBlock { }; } restoreData(d) { - this.querySelector(".quantity-name").value = d.name || ""; - for (let i = 0; i < 7; i++) this.querySelector(`.dim-${i}`).value = d.dims ? d.dims[i] : "0"; - this.querySelector(".quantity-min").value = d.min || ""; - this.querySelector(".quantity-max").value = d.max || ""; - this.querySelector(".quantity-default-unit").value = d.defaultUnit || ""; + this._el.name.value = d.name || ""; + for (let i = 0; i < 7; i++) { if (this._el.dims[i]) this._el.dims[i].value = d.dims ? d.dims[i] : "0"; } + if (this._el.min) this._el.min.value = d.min || ""; + if (this._el.max) this._el.max.value = d.max || ""; + if (this._el.defaultUnit) this._el.defaultUnit.value = d.defaultUnit || ""; } } // ── WCUnit — unit of measurement ─────────────────────────────────────────── +/** Unit of measurement: name, associated quantity, suffix, SI factor, recursive SI resolution. */ class WCUnit extends WCBlock { connectedCallback() { super.connectedCallback(); - this._el.fullname = this.querySelector(".unit-fullname"); - this._el.type = this.querySelector(".unit-type"); - this._el.suffix = this.querySelector(".unit-suffix"); - this._el.si = this.querySelector(".unit-si"); - this._el.resolved = this.querySelector(".unit-resolved"); + this._el.fullname = this._require(".unit-fullname"); + this._el.type = this._require(".unit-type"); + this._el.suffix = this._require(".unit-suffix"); + this._el.si = this._require(".unit-si"); + this._el.resolved = this._require(".unit-resolved"); for (const inp of this.querySelectorAll("input")) { inp.addEventListener("input", () => { @@ -118,10 +119,10 @@ class WCUnit extends WCBlock { }; } restoreData(d) { - this.querySelector(".unit-fullname").value = d.fullname || ""; - this.querySelector(".unit-type").value = d.type || ""; - this.querySelector(".unit-suffix").value = d.suffix || ""; - this.querySelector(".unit-si").value = d.si || "1"; + this._el.fullname.value = d.fullname || ""; + this._el.type.value = d.type || ""; + this._el.suffix.value = d.suffix || ""; + this._el.si.value = d.si || "1"; } updateSiDisplay() { @@ -135,15 +136,14 @@ class WCUnit extends WCBlock { // ── WCBoolean — boolean variable ─────────────────────────────────────────── +/** Boolean variable: name, true/false value, optional comment. Generates C `int`. */ class WCBoolean extends WCBlock { - get dataType() { return "boolean"; } - connectedCallback() { super.connectedCallback(); - this._el.name = this.querySelector(".var-name"); - this._el.value = this.querySelector(".var-value"); - this._el.comment = this.querySelector(".var-comment"); - this._el.cname = this.querySelector(".var-cname"); + this._el.name = this._require(".var-name"); + this._el.value = this._require(".var-value"); + this._el.comment = this._require(".var-comment"); + this._el.cname = this._require(".var-cname"); this._el.name.addEventListener("input", () => { this._el.cname.textContent = toCName(this._el.name.value); }); @@ -151,9 +151,9 @@ class WCBoolean extends WCBlock { saveData() { return { name: this._el.name.value, value: this._el.value.value, comment: this._el.comment.value }; } restoreData(d) { - this.querySelector(".var-name").value = d.name || ""; - this.querySelector(".var-value").value = d.value || "false"; - this.querySelector(".var-comment").value = d.comment || ""; + this._el.name.value = d.name || ""; + this._el.value.value = d.value || "false"; + this._el.comment.value = d.comment || ""; this._el.cname.textContent = toCName(this._el.name.value); } @@ -169,18 +169,18 @@ class WCBoolean extends WCBlock { // ── WCScalar — typed scalar variable with quantity, unit, and SI conversion ── +/** Typed scalar variable with quantity, unit, SI conversion, and auto-sync between value and unit. */ class WCScalar extends WCBlock { get dataType() { return "scalar"; } - connectedCallback() { super.connectedCallback(); - this._el.name = this.querySelector(".var-name"); - this._el.value = this.querySelector(".var-value"); - this._el.unit = this.querySelector(".var-unit"); - this._el.type = this.querySelector(".var-type"); - this._el.si = this.querySelector(".var-si"); - this._el.comment = this.querySelector(".var-comment"); - this._el.cname = this.querySelector(".var-cname"); + this._el.name = this._require(".var-name"); + this._el.value = this._require(".var-value"); + this._el.unit = this._require(".var-unit"); + this._el.type = this._require(".var-type"); + this._el.si = this._require(".var-si"); + this._el.comment = this._require(".var-comment"); + this._el.cname = this._require(".var-cname"); this._el.name.addEventListener("input", () => { this._el.cname.textContent = toCName(this._el.name.value); @@ -265,11 +265,11 @@ class WCScalar extends WCBlock { }; } restoreData(d) { - this.querySelector(".var-name").value = d.name || ""; - this.querySelector(".var-value").value = d.value || ""; - this.querySelector(".var-type").value = d.type || ""; - this.querySelector(".var-unit").value = d.unit || ""; - this.querySelector(".var-comment").value = d.comment || ""; + this._el.name.value = d.name || ""; + this._el.value.value = d.value || ""; + this._el.type.value = d.type || ""; + this._el.unit.value = d.unit || ""; + this._el.comment.value = d.comment || ""; this._el.cname.textContent = toCName(this._el.name.value); } @@ -285,17 +285,154 @@ class WCScalar extends WCBlock { } -// ── WCString — string variable ───────────────────────────────────────────── - -class WCString extends WCBlock { - get dataType() { return "string"; } +// ── WCVector — N-dimensional vector with quantity, unit, and SI conversion ── +/** N-dimensional vector variable with quantity, unit, SI conversion; generates C array. */ +class WCVector extends WCBlock { + get dataType() { return "vector"; } connectedCallback() { super.connectedCallback(); - this._el.name = this.querySelector(".var-name"); - this._el.value = this.querySelector(".var-value"); - this._el.comment = this.querySelector(".var-comment"); - this._el.cname = this.querySelector(".var-cname"); + this._el.name = this._require(".var-name"); + this._el.dim = this._require(".var-dim"); + this._el.value = this._require(".var-value"); + this._el.unit = this._require(".var-unit"); + this._el.type = this._require(".var-type"); + this._el.si = this._require(".var-si"); + this._el.comment = this._require(".var-comment"); + this._el.cname = this._require(".var-cname"); + + this._el.name.addEventListener("input", () => { + this._el.cname.textContent = toCName(this._el.name.value); + }); + + setTimeout(() => { + refreshQuantityOptions(); + refreshUnitOptions(); + this.updateSiDisplay(); + }, 0); + + this._el.value.addEventListener("input", () => { + this.syncUnitFromValue(); + this.updateSiDisplay(); + }); + this._el.unit.addEventListener("change", () => { + this.syncValueFromUnit(); + this.updateSiDisplay(); + }); + this._el.type.addEventListener("change", () => { + refreshUnitOptions(); + this.updateSiDisplay(); + }); + } + + getDim() { + return parseInt(this._el.dim.value) || 3; + } + + parseValue() { + const { numVal, unitSuffix } = splitValueAndUnit(this._el.value.value, 0); + return { numVal, unitSuffix }; + } + + siValue() { + const { numVal, unitSuffix } = this.parseValue(); + if (!unitSuffix) return numVal; + const quantityName = this._el.type.value; + for (const u of $id("data-area").querySelectorAll("wc-unit")) { + if ( + u.querySelector(".unit-type").value === quantityName && + u.querySelector(".unit-suffix").value === unitSuffix + ) { + return numVal * u.getSiFactor(); + } + } + return numVal; + } + + syncUnitFromValue() { + const { unitSuffix } = this.parseValue(); + if (unitSuffix && [...this._el.unit.options].some(o => o.value === unitSuffix)) { + this._el.unit.value = unitSuffix; + } + } + + syncValueFromUnit() { + const unitSuffix = this._el.unit.value; + const vals = this._el.value.value.trim().split(/\s+/); + if (unitSuffix && vals.length > 0 && !isNaN(parseFloat(vals[0]))) { + const first = parseFloat(vals[0]); + if (vals.length <= 2) { + this._el.value.value = first + " " + unitSuffix; + } + } + } + + updateSiDisplay() { + const typeName = this._el.type.value; + const { unitSuffix } = this.parseValue(); + if (unitSuffix && typeName) { + const si = this.siValue(); + const defaultUnit = getDefaultUnitSuffix(typeName); + this._el.si.textContent = defaultUnit + ? `= ${si} ${defaultUnit}` + : `= ${si} SI`; + } else { + this._el.si.textContent = ""; + } + } + + saveData() { + return { + name: this._el.name.value, + dim: parseInt(this._el.dim.value) || 3, + value: this._el.value.value, + type: this._el.type.value, + unit: this._el.unit.value, + comment: this._el.comment.value + }; + } + restoreData(d) { + this._el.name.value = d.name || ""; + this._el.dim.value = d.dim || 3; + this._el.value.value = d.value || ""; + this._el.type.value = d.type || ""; + this._el.unit.value = d.unit || ""; + this._el.comment.value = d.comment || ""; + this._el.cname.textContent = toCName(this._el.name.value); + } + + generateOwnCode() { + const name = toCName(this._el.name.value) || "v"; + const typeName = this._el.type.value; + const dim = this.getDim(); + const prefix = commentPrefix(this._el.name.value, this._el.comment.value); + const baseType = typeName || "double"; + const raw = this._el.value.value.trim(); + if (raw) { + const parts = raw.split(/\s+/).filter(s => s); + const siFactor = this.parseValue().unitSuffix ? this.siValue() / this.parseValue().numVal : 1; + const siParts = parts.map(p => { + const n = parseFloat(p); + return isNaN(n) ? 0 : n * siFactor; + }); + return `${prefix}${baseType} ${name}[${dim}] = {${siParts.join(", ")}};\n`; + } + return `${prefix}${baseType} ${name}[${dim}];\n`; + } +} + + +// ── WCString — string variable ───────────────────────────────────────────── + +/** String variable: name, value. Generates C `string` struct with malloc/memcpy. */ +class WCString extends WCBlock { + get dataType() { return "string"; } + connectedCallback() { + super.connectedCallback(); + this._el.name = this._require(".var-name"); + this._el.value = this._require(".var-value"); + this._el.comment = this._require(".var-comment"); + this._el.cname = this._require(".var-cname"); this._el.name.addEventListener("input", () => { this._el.cname.textContent = toCName(this._el.name.value); }); @@ -303,9 +440,9 @@ class WCString extends WCBlock { saveData() { return { name: this._el.name.value, value: this._el.value.value, comment: this._el.comment.value }; } restoreData(d) { - this.querySelector(".var-name").value = d.name || ""; - this.querySelector(".var-value").value = d.value || ""; - this.querySelector(".var-comment").value = d.comment || ""; + this._el.name.value = d.name || ""; + this._el.value.value = d.value || ""; + this._el.comment.value = d.comment || ""; this._el.cname.textContent = toCName(this._el.name.value); } @@ -322,27 +459,26 @@ class WCString extends WCBlock { // ── WCArray — typed C array variable ─────────────────────────────────────── +/** Typed C array variable with dynamic element list and optional vector dimension. */ class WCArray extends WCBlock { get dataType() { return "array"; } - - constructor() { - super(); - this._values = []; - } - connectedCallback() { super.connectedCallback(); - this._el.name = this.querySelector(".var-name"); - this._el.type = this.querySelector(".array-type"); - this._el.elements = this.querySelector(".array-elements"); - this._el.count = this.querySelector(".array-count"); - this._el.comment = this.querySelector(".var-comment"); - this._el.cname = this.querySelector(".var-cname"); + this._el.name = this._require(".var-name"); + this._el.type = this._require(".array-type"); + this._el.elements = this._require(".array-elements"); + this._el.count = this._require(".array-count"); + this._el.comment = this._require(".var-comment"); + this._el.cname = this._require(".var-cname"); + this._el.vecDim = this.querySelector(".array-vec-dim"); this._el.name.addEventListener("input", () => { this._el.cname.textContent = toCName(this._el.name.value); refreshVarRefOptions(); }); + if (this._el.vecDim) { + this._el.vecDim.addEventListener("input", () => this._updateCount()); + } setTimeout(() => this._renderElements(), 0); } @@ -441,17 +577,37 @@ class WCArray extends WCBlock { _addElement(value) { this._values.push(value); - this._renderElements(); + const row = this._makeElementRow(this._values.length - 1); + this._el.elements.insertBefore(row, this._el.elements.lastChild); + const newRow = this._el.elements.lastChild; + newRow.querySelector(".array-element-idx").textContent = this._values.length; + this._updateCount(); } _removeElement(idx) { this._values.splice(idx, 1); - this._renderElements(); + const row = this._el.elements.querySelector(`[data-idx="${idx}"]`); + if (row) row.remove(); + const rows = this._el.elements.children; + for (let i = 0; i < rows.length - 1; i++) { + rows[i].dataset.idx = i; + rows[i].querySelector(".array-element-idx").textContent = i; + } + this._updateCount(); } _updateCount() { const n = this._values.length; - this._el.count.textContent = n + " element" + (n === 1 ? "" : "s"); + const dim = this.getVecDim(); + if (dim > 1) { + this._el.count.textContent = n + " element" + (n === 1 ? "" : "s") + " (\u00D7" + dim + " = " + (n * dim) + ")"; + } else { + this._el.count.textContent = n + " element" + (n === 1 ? "" : "s"); + } + } + + getVecDim() { + return this._el.vecDim ? (parseInt(this._el.vecDim.value) || 1) : 1; } saveData() { @@ -459,7 +615,8 @@ class WCArray extends WCBlock { name: this._el.name.value, type: this._el.type.value, values: [...this._values], - comment: this._el.comment.value + comment: this._el.comment.value, + vecDim: this.getVecDim() }; } restoreData(d) { @@ -467,6 +624,7 @@ class WCArray extends WCBlock { this._el.type.value = d.type || "float"; this._el.comment.value = d.comment || ""; this._el.cname.textContent = toCName(this._el.name.value); + if (this._el.vecDim) this._el.vecDim.value = d.vecDim || 1; if (Array.isArray(d.values)) { this._values = [...d.values]; } else if (typeof d.values === "string" && d.values) { @@ -480,11 +638,14 @@ class WCArray extends WCBlock { generateOwnCode() { const name = toCName(this._el.name.value) || "arr"; const type = this._el.type.value || "float"; + const dim = this.getVecDim(); + const count = this._values.length || 1; + const totalSize = dim > 1 ? dim + "*" + count : count; const elements = this._values.filter(s => s).join(", "); const prefix = commentPrefix(this._el.name.value, this._el.comment.value); - if (elements) { - return `${prefix}${type} ${name}[] = {${elements}};\n`; + if (elements && dim <= 1) { + return `${prefix}${type} ${name}[${totalSize}] = {${elements}};\n`; } - return `${prefix}${type} ${name}[1];\n`; + return `${prefix}${type} ${name}[${totalSize}];\n`; } } diff --git a/blocks-proc.js b/blocks-proc.js index 299b36e..8128482 100644 --- a/blocks-proc.js +++ b/blocks-proc.js @@ -8,46 +8,71 @@ // // Load order dependencies: // wcblock.js → WCBlock base class -// helpers.js → refreshProcOptions, findDataSource, resolveSourceName +// helpers.js → findDataSource, resolveSourceName, escapeString, toCName, commentPrefix // dom.js → $id // draw.js → deleteLink +/** @type {Object} Maps library IDs to icon image paths for procedure call blocks. */ +const LIB_ICONS = { + stdio: "lib-icons/c.svg", + stdlib: "lib-icons/c.svg", + string: "lib-icons/c.svg", + math: "lib-icons/c.svg", +}; + // ── WCProcedure — procedure definition ───────────────────────────────────── +/** Procedure definition block with inputs/outputs/description/return-style. Lives in the Procedures tab. */ class WCProcedure extends WCBlock { connectedCallback() { super.connectedCallback(); - this._el.name = this.querySelector(".proc-name"); - this._el.inputs = this.querySelector(".proc-inputs"); - this._el.outputs = this.querySelector(".proc-outputs"); + this._el.naturalName = this._require(".proc-natural-name"); + this._el.techName = this._require(".proc-tech-name"); + this._el.inputs = this._require(".proc-inputs"); + this._el.outputs = this._require(".proc-outputs"); + this._el.description = this.querySelector(".proc-description"); + this._el.descBody = this.querySelector(".proc-desc-body"); - this.querySelector(".proc-add-input").addEventListener("click", () => { + this._require(".proc-add-input").addEventListener("click", () => { + if (this.dataset.readonly) return; this._addParamRow(this._el.inputs, "in"); }); - this.querySelector(".proc-add-output").addEventListener("click", () => { + this._require(".proc-add-output").addEventListener("click", () => { + if (this.dataset.readonly) return; this._addParamRow(this._el.outputs, "out"); }); - this._el.name.addEventListener("input", () => refreshProcOptions()); + this._el.naturalName.addEventListener("input", () => { + if (!this.dataset.readonly) { + this._el.techName.textContent = toCName(this._el.naturalName.value) || "proc"; + } + }); + + this._el.returnStyle = this.querySelector(".proc-return-style"); + if (this._el.returnStyle) { + this.dataset.returnStyle = this._el.returnStyle.value; + this._el.returnStyle.addEventListener("change", () => { + if (this.dataset.readonly) return; + this.dataset.returnStyle = this._el.returnStyle.value; + }); + } } _addParamRow(container, dir) { const row = document.createElement("div"); row.className = "proc-param-row"; + const readonly = this.dataset.readonly; row.innerHTML = - `` + - `` + - `×`; - row.querySelector(".proc-param-del").addEventListener("click", () => { - row.remove(); - refreshProcOptions(); - }); - for (const inp of row.querySelectorAll("input")) { - inp.addEventListener("input", () => refreshProcOptions()); + `` + + `` + + (readonly ? "" : `×`); + if (!readonly) { + row.querySelector(".proc-param-del").addEventListener("click", () => { + row.remove(); + }); } container.appendChild(row); - refreshProcOptions(); } getLibrary() { @@ -56,7 +81,7 @@ class WCProcedure extends WCBlock { } getQualifiedName() { - return this.getLibrary() + "/" + (this._el.name.value || "unnamed"); + return this.getLibrary() + "/" + (this._el.techName.textContent || "unnamed"); } getInputParams() { @@ -77,13 +102,28 @@ class WCProcedure extends WCBlock { saveData() { return { library: this.getLibrary(), - name: this._el.name.value, + naturalName: this._el.naturalName.value, + name: this._el.techName.textContent, inputs: this.getInputParams(), - outputs: this.getOutputParams() + outputs: this.getOutputParams(), + returnStyle: this._el.returnStyle ? this._el.returnStyle.value : (this.dataset.returnStyle || "pointer"), + readonly: !!this.dataset.readonly, + formatString: this.dataset.formatString !== undefined, + description: this._el.descBody ? this._el.descBody.textContent : "", + descOpen: this._el.description ? this._el.description.open : false }; } restoreData(d) { - this._el.name.value = d.name || "proc"; + this._el.naturalName.value = d.naturalName || ""; + this._el.techName.textContent = d.name || "proc"; + if (d.readonly) this.dataset.readonly = "true"; + if (d.formatString) this.dataset.formatString = "true"; + if (d.returnStyle) { + this.dataset.returnStyle = d.returnStyle; + if (this._el.returnStyle) this._el.returnStyle.value = d.returnStyle; + } + if (this._el.descBody && d.description) this._el.descBody.textContent = d.description; + if (this._el.description && d.descOpen) this._el.description.open = true; for (const p of (d.inputs || [])) { this._addParamRow(this._el.inputs, "in"); const rows = this._el.inputs.querySelectorAll(".proc-param-row"); @@ -106,20 +146,91 @@ class WCProcedure extends WCBlock { // ── WCProcCall — procedure call block ────────────────────────────────────── +/** @type {RegExp} Matches C printf-format specifiers, capturing the type character. */ +const _FMT_SPEC_RE = /%(?:[+\- #0]*\d*(?:\.\d+)?)([diufFeEgGxXoscpaAn%])/g; + +/** + * Maps a printf format specifier character to a C data type. + * @param {string} spec Single format character (e.g. "d", "f", "s") + * @returns {string} "int", "double", or "string" + */ +function _specDataType(spec) { + if ("di".includes(spec)) return "int"; + if ("u".includes(spec)) return "int"; + if ("fFeEgGaA".includes(spec)) return "double"; + if ("s".includes(spec)) return "string"; + return "int"; +} + +/** Procedure call block with dynamic argument fields, format-string support, and error-handling hooks. Lives in the Algorithm tab. */ class WCProcCall extends WCBlock { connectedCallback() { super.connectedCallback(); - this._el.select = this.querySelector(".proccall-select"); - this._el.nameInput = this.querySelector(".proccall-name"); - this._el.inputs = this.querySelector(".proccall-inputs"); - this._el.outputs = this.querySelector(".proccall-outputs"); + this._selectedProc = ""; + this._fmtProc = null; + this._fmtInput = null; + this._fmtObserver = null; + this._el.icon = this._require(".proccall-icon"); + this._el.natName = this._require(".proccall-nat-name"); + this._el.techName = this._require(".proccall-tech-name"); + this._el.changeBtn = this._require(".proccall-change-btn"); + this._el.nameInput = this._require(".proccall-name"); + this._el.nameRow = this._require(".proccall-name-row"); + this._el.inputs = this._require(".proccall-inputs"); + this._el.outputs = this._require(".proccall-outputs"); + this._el.errHook = this.querySelector(".proccall-error-hook"); + this._el.fmtargs = this._require(".proccall-fmtargs"); + this._el.fmtargsList = this._require(".proccall-fmtargs-list"); + this._el.addFmtArgBtn = this._require(".proccall-add-fmtarg"); + this._el.addFmtArgBtn.addEventListener("click", () => this._addFormatArg()); - this._el.select.addEventListener("change", () => this._rebuildParams()); + const openBrowser = () => { + const current = this._selectedProc; + showProcedureBrowser(current, (qname) => { + if (qname) this.setProcedure(qname); + }); + }; + + this._el.natName.addEventListener("click", openBrowser); + this._el.changeBtn.addEventListener("click", openBrowser); this._initTimer = setTimeout(() => this._rebuildParams(), 0); } + setProcedure(qname) { + this._selectedProc = qname || ""; + const libName = qname ? qname.split("/")[0] : ""; + if (this._el.natName) { + if (qname) { + let natName = qname.split("/")[1] || qname; + let techName = qname.split("/")[1] || qname; + for (const proc of document.querySelectorAll("wc-procedure")) { + if (proc.getQualifiedName() === qname) { + const nat = proc.querySelector(".proc-natural-name"); + if (nat && nat.value) natName = nat.value; + techName = (proc.querySelector(".proc-tech-name") || {}).textContent || techName; + break; + } + } + this._el.natName.textContent = natName; + this._el.natName.title = qname; + this._el.techName.textContent = techName; + this._el.techName.title = qname; + this._el.icon.src = LIB_ICONS[libName] || ""; + this._el.icon.alt = libName; + } else { + this._el.natName.textContent = "(select procedure)"; + this._el.natName.title = ""; + this._el.techName.textContent = ""; + this._el.techName.title = ""; + this._el.icon.src = ""; + this._el.icon.alt = ""; + } + } + this._rebuildParams(); + } + _rebuildParams() { - const qname = this._el.select.dataset.restoredValue || this._el.select.value; + const qname = this._selectedProc; for (const container of [this._el.inputs, this._el.outputs]) { for (const hook of container.querySelectorAll(".hook")) { @@ -134,19 +245,173 @@ class WCProcCall extends WCBlock { this._el.inputs.innerHTML = ""; this._el.outputs.innerHTML = ""; - if (!qname) return; - - for (const proc of document.querySelectorAll("wc-procedure")) { - if (proc.getQualifiedName() === qname) { - for (const p of proc.getInputParams()) { - this._createArgField(this._el.inputs, p, "in"); + let returnStyle = "pointer"; + if (qname) { + for (const proc of document.querySelectorAll("wc-procedure")) { + if (proc.getQualifiedName() === qname) { + returnStyle = proc.dataset.returnStyle || "pointer"; + this._cachedReturnStyle = returnStyle; + for (const p of proc.getInputParams()) { + this._createArgField(this._el.inputs, p, "in"); + } + if (returnStyle !== "error") { + for (const p of proc.getOutputParams()) { + this._createArgField(this._el.outputs, p, "out"); + } + } + break; } - for (const p of proc.getOutputParams()) { - this._createArgField(this._el.outputs, p, "out"); - } - break; } } + + this._setupFormatStringArgs(); + + const isError = returnStyle === "error"; + const hasReturnValue = returnStyle === "error" || returnStyle === "value"; + if (this._el.errHook) { + this._el.errHook.classList.toggle("visible", hasReturnValue); + } + if (this._el.nameRow) { + this._el.nameRow.style.display = isError ? "none" : ""; + } + } + + _setupFormatStringArgs() { + const qname = this._selectedProc; + let fmtProc = null; + if (qname) { + for (const proc of document.querySelectorAll("wc-procedure")) { + if (proc.getQualifiedName() === qname && proc.dataset.formatString !== undefined) { + fmtProc = proc; + break; + } + } + } + this._fmtProc = fmtProc; + + if (this._fmtObserver) { this._fmtObserver.disconnect(); this._fmtObserver = null; } + + if (!fmtProc) { + this._el.fmtargs.style.display = "none"; + this._el.addFmtArgBtn.style.display = "none"; + this._el.fmtargsList.innerHTML = ""; + return; + } + + this._el.fmtargs.style.display = ""; + this._fmtInput = this._el.inputs.querySelector(".proccall-arg-input"); + + if (this._fmtInput) { + this._fmtInput.addEventListener("input", () => this._rebuildFormatArgs()); + } + + this._fmtObserver = new MutationObserver(() => { + if (this._isFmtDynamic()) this._ensureFmtUnconnected(); + }); + this._fmtObserver.observe(this._el.fmtargsList, { attributes: true, attributeFilter: ["data-connected"], subtree: true }); + + this._rebuildFormatArgs(); + } + + _isFmtDynamic() { + if (!this._fmtInput) return false; + const hook = this._fmtInput.parentElement.querySelector(".hook"); + return hook ? findDataSource(hook) !== null : false; + } + + _rebuildFormatArgs() { + this._el.fmtargsList.innerHTML = ""; + const dynamic = this._isFmtDynamic(); + this._el.addFmtArgBtn.style.display = dynamic ? "" : "none"; + + if (dynamic) { + this._ensureFmtUnconnected(); + return; + } + + const fmt = this._fmtInput ? this._fmtInput.value : ""; + const specs = this._parseFormatSpecifiers(fmt); + for (let i = 0; i < specs.length; i++) { + this._createFormatArg(specs[i], i); + } + } + + _parseFormatSpecifiers(fmt) { + const specs = []; + let m; + _FMT_SPEC_RE.lastIndex = 0; + while ((m = _FMT_SPEC_RE.exec(fmt)) !== null) { + if (m[1] === "%") continue; + specs.push({ full: m[0], spec: m[1] }); + } + return specs; + } + + _createFormatArg(spec, index) { + const row = document.createElement("div"); + row.className = "proccall-arg-row"; + + const hook = document.createElement("button"); + hook.className = "hook"; + hook.dataset.direction = "in"; + hook.dataset.linkType = "data"; + hook.dataset.type = _specDataType(spec.spec); + hook.dataset.exit = "left"; + hook.title = `expects ${_specDataType(spec.spec)}`; + + const label = document.createElement("span"); + label.className = "proccall-arg-label"; + label.textContent = `arg${index + 1}:`; + + const input = document.createElement("input"); + input.className = "proccall-arg-input"; + input.dataset.dir = "in"; + input.dataset.paramName = `fmtArg${index}`; + input.placeholder = spec.full; + + row.appendChild(hook); + row.appendChild(label); + row.appendChild(input); + this._el.fmtargsList.appendChild(row); + this._setupHook(hook); + } + + _addFormatArg() { + const idx = this._el.fmtargsList.querySelectorAll(".proccall-arg-row").length; + const row = document.createElement("div"); + row.className = "proccall-arg-row"; + + const hook = document.createElement("button"); + hook.className = "hook"; + hook.dataset.direction = "in"; + hook.dataset.linkType = "data"; + hook.dataset.type = "scalar"; + hook.dataset.exit = "left"; + hook.title = "expects scalar"; + + const label = document.createElement("span"); + label.className = "proccall-arg-label"; + label.textContent = `arg${idx + 1}:`; + + const input = document.createElement("input"); + input.className = "proccall-arg-input"; + input.dataset.dir = "in"; + input.dataset.paramName = `fmtArg${idx}`; + input.placeholder = `arg${idx + 1}`; + + row.appendChild(hook); + row.appendChild(label); + row.appendChild(input); + this._el.fmtargsList.appendChild(row); + this._setupHook(hook); + this._ensureFmtUnconnected(); + } + + _ensureFmtUnconnected() { + if (!this._isFmtDynamic()) return; + const hooks = this._el.fmtargsList.querySelectorAll(".hook"); + const hasFree = [...hooks].some(h => !h.dataset.connected); + if (!hasFree) this._addFormatArg(); } _createArgField(container, param, dir) { @@ -192,24 +457,92 @@ class WCProcCall extends WCBlock { return block.dataType === "string" ? name + ".data" : name; } + generateCode(visited = new Set()) { + if (visited.has(this)) return ""; + visited.add(this); + const own = this.generateOwnCode(); + if (this._el.errHook && this._el.errHook.classList.contains("visible") && + Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook)) { + return own; + } + return own + this.generateNextCode(visited); + } + generateOwnCode() { - const qname = this._el.select.value; + const qname = this._selectedProc; if (!qname) return ""; const parts = qname.split("/"); const procName = parts[1] || qname; + let returnStyle = this._cachedReturnStyle || "pointer"; + const inputs = []; const outputs = []; for (const row of this._el.inputs.querySelectorAll(".proccall-arg-row")) { const hook = row.querySelector(".hook"); const inp = row.querySelector(".proccall-arg-input"); const source = hook ? findDataSource(hook) : null; - inputs.push(source ? this._sourceName(source) : (inp.value || inp.placeholder)); + if (source) { + inputs.push(this._sourceName(source)); + } else { + let val = inp.value || inp.placeholder; + if (inp.dataset.paramType === "string") val = `"${escapeString(val)}"`; + inputs.push(val); + } } for (const row of this._el.outputs.querySelectorAll(".proccall-arg-row")) { const inp = row.querySelector(".proccall-arg-input"); outputs.push("&" + (inp.value || inp.placeholder)); } + if (this._fmtProc) { + for (const row of this._el.fmtargsList.querySelectorAll(".proccall-arg-row")) { + const hook = row.querySelector(".hook"); + const inp = row.querySelector(".proccall-arg-input"); + const source = hook ? findDataSource(hook) : null; + if (source) { + inputs.push(this._sourceName(source)); + } else { + let val = inp.value || inp.placeholder; + if (hook.dataset.type === "string") val = `"${escapeString(val)}"`; + inputs.push(val); + } + } + } + + const errPaths = this._el.errHook && this._el.errHook.classList.contains("visible") + ? Array.from(this.wcNextFlow).filter(p => p.fromHook === this._el.errHook) + : []; + const hasErrPath = errPaths.length > 0; + + if (returnStyle === "error") { + const call = `${procName}(${inputs.join(", ")})`; + if (hasErrPath) { + const errCode = errPaths[0].toElement.generateCode(new Set()); + let okCode = ""; + for (const p of this.wcNextFlow) { + if (p.fromHook !== this._el.errHook) { + okCode += p.toElement.generateCode(new Set()); + } + } + return `\nif (${call} != 0) {\n${errCode}} else {\n${okCode}}\n`; + } + return `\n${call};\n`; + } + + if (returnStyle === "value" && outputs.length === 1) { + const outVar = outputs[0].replace(/^&/, ""); + if (hasErrPath) { + const errCode = errPaths[0].toElement.generateCode(new Set()); + let okCode = ""; + for (const p of this.wcNextFlow) { + if (p.fromHook !== this._el.errHook) { + okCode += p.toElement.generateCode(new Set()); + } + } + return `\nif ((${outVar} = ${procName}(${inputs.join(", ")})) != 0) {\n${errCode}} else {\n${okCode}}\n`; + } + return `\n${procName}(${inputs.join(", ")});\n`; + } const args = [...inputs, ...outputs].join(", "); return `\n${procName}(${args});\n`; } @@ -219,19 +552,27 @@ class WCProcCall extends WCBlock { for (const inp of this.querySelectorAll(".proccall-arg-input")) { args[inp.dataset.dir + ":" + inp.dataset.paramName] = inp.value; } - return { selected: this._el.select.value, name: this._el.nameInput.value, args }; + return { selected: this._selectedProc, name: this._el.nameInput.value, args }; } restoreData(d) { if (this._initTimer) { clearTimeout(this._initTimer); this._initTimer = null; } - this._el.select.dataset.restoredValue = d.selected || ""; - this._el.select.value = d.selected || ""; + this.setProcedure(d.selected || ""); this._el.nameInput.value = d.name || ""; - this._rebuildParams(); if (d.args) { for (const inp of this.querySelectorAll(".proccall-arg-input")) { const key = inp.dataset.dir + ":" + inp.dataset.paramName; if (d.args[key] !== undefined) inp.value = d.args[key]; } } + if (this._fmtProc && d.args) { + this._rebuildFormatArgs(); + for (const inp of this.querySelectorAll(".proccall-arg-input")) { + const key = inp.dataset.dir + ":" + inp.dataset.paramName; + if (key.startsWith("in:fmtArg") && d.args[key] !== undefined) { + inp.value = d.args[key]; + } + } + } } } + \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..68ecbff --- /dev/null +++ b/docs/API.md @@ -0,0 +1,1132 @@ +# API Reference + +Generated from JSDoc annotations in 16 source files. + + +--- +## dom.js + +### `const` svgNamespace +*Line 19* + +SVG namespace URI, required when creating SVG elements via JS. + +### `const` STORAGE_KEY +*Line 22* + +localStorage key for workspace persistence (save/load/new). + + +--- +## helpers.js + +### `function` splitValueAndUnit +*Line 29* + +Parses a string like "5 km" into a numeric value and an optional unit suffix. +Falls back to parsing the entire string as a bare number when no suffix is present. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `str` | `string` | - e.g. "5 km", "42", "" | +| `defaultNum` | `number` | - fallback when parseFloat yields NaN | + +**Returns:** `{ numVal: number, unitSuffix: string|null ` — } + +### `function` rebuildSelect +*Line 50* + +Replaces all options in a , preserving the current selection if it still exists. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `sel` | `HTMLSelectElement` | - the select element to rebuild | +| `placeholder` | `string` | - text for the first (empty-value) option | +| `values` | `string[]` | - option value/label pairs to add | +| `current` | `string` | - currently selected value to attempt to preserve | + +### `function` refreshQuantityOptions +*Line 69* + +Refreshes every quantity-name dropdown (.var-type, .unit-type) by scanning +all wc-quantity blocks in the data area. + +### `function` refreshUnitOptions +*Line 92* + +Refreshes the unit-suffix dropdown on every datum block to show only units +that match the datum's currently selected quantity. + +### `function` getDefaultUnitSuffix +*Line 124* + +Looks up the default unit suffix for a given quantity name. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `quantityName` | `string` | | + +**Returns:** `string` — default unit suffix, or "" if not found + +### `function` refreshDefaultUnitOptions +*Line 141* + +Refreshes the default-unit dropdown on every quantity block with suffixes +of matching unit blocks. + +### `function` refreshVarRefOptions +*Line 171* + +Refreshes the variable-select dropdown on every wc-varref block in the +algorithm pane. Scans all boolean, scalar, string, and array blocks. + +### `function` switchLibTab +*Line 204* + +Activates the clicked library sub-tab and shows its corresponding pane. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `name` | `string` | - library name (matches data-lib-name attribute) | + +### `const` _dataSourceCache +*Line 214* + +Type: `WeakMap` Cache mapping data hooks to their source blocks. + +### `function` findDataSource +*Line 222* + +Finds the block connected to a given data hook via any .flow-data path. +Results are cached in _dataSourceCache. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `hook` | `HTMLElement` | - the data hook button element | + +**Returns:** `HTMLElement|null` — the source block, or null if nothing is connected + +### `function` copyCode +*Line 234* + +Copies the generated C code text to the clipboard. + +### `function` escapeString +*Line 245* + +Escapes special characters for embedding in a C string literal. +Handles backslash, double-quote, newline, carriage return, and tab. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `str` | `string` | - raw string | + +**Returns:** `string` — escaped string safe for C "..." literals + +### `const` C_KEYWORDS +*Line 253* + +Type: `Set` C reserved keywords — toCName appends "_" to avoid clashes. + +### `function` toCName +*Line 268* + +Converts a natural-language name into a valid C identifier. +Lowercases, replaces non-alphanumeric chars with underscores, +collapses runs of underscores, strips leading/trailing underscores, +and prepends an underscore if the result starts with a digit. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `str` | `string` | - natural name like "My Variable 2" | + +**Returns:** `string` — valid C identifier like "my_variable_2" + +### `function` deriveNaturalName +*Line 284* + +Derives a human-readable natural name from a C function/tech name. +Converts camelCase and snake_case to words, strips common prefixes. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `techName` | `string` | - e.g. "SDL_CreateWindow", "glfwInit", "puts" | + +**Returns:** `string` — e.g. "Create Window", "Init GLFW", "Puts" + +### `function` commentPrefix +*Line 316* + +Builds a C comment prefix from a variable's natural name and an optional comment. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `naturalName` | `string` | - the human-readable variable name | +| `comment` | `string` | - the optional comment text | + +**Returns:** `string` — e.g. "// My Flag: indicates status\n" or "" when no comment + +### `function` resolveSourceName +*Line 330* + +Resolves the ultimate C variable name from a block by checking for +.var-name, .expr-name, and following .varref-select chains. +Returns the empty string when nothing is found. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `block` | `HTMLElement` | - any block that may hold a variable reference | +| `[visited]` | `Set` | - cycle detection set (internal use) | + +**Returns:** `string` — the C name, or "" + +### `function` refreshBlockWarnings +*Line 349* + +Check every algorithm block and flag those without an incoming logic connection. + +### `function` refreshAll +*Line 356* + +Runs all refresh passes in dependency order. + + +--- +## error.js + +### `function` showError +*Line 12* + +Shows the error modal with the given message. +The overlay is absolutely positioned and blocks all interaction until dismissed. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `msg` | `string` | - message to display inside the error box | + +### `function` closeError +*Line 25* + +Hides the error modal. Triggered by clicking the × button, +the overlay backdrop, or called programmatically. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `[ev]` | `MouseEvent` | - event to check target matching | + + +--- +## errno-data.js + +### `const` ERRNO_LIST +*Line 6* + +Type: `Array<{code:number, name:string, desc:string` >} + + +--- +## draw.js + +### `const` EXT +*Line 12* + +Type: `number` Pixels the path extends from a hook in its exit direction. + +### `function` routeBetween +*Line 31* + +Routes a 45°-constrained polyline between two points. +Returns the SVG path data (everything after the initial "M") as a string +suitable for appending to an existing moveto. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `x1` | `number` | - start x | +| `y1` | `number` | - start y | +| `x2` | `number` | - end x | +| `y2` | `number` | - end y | + +**Returns:** `string` — SVG path data segment (e.g. "L 100 50 L 150 50") + +### `function` resolveHook +*Line 72* + +Resolves a hook DOM element, preferring the explicitly stored reference +and falling back to the block's first .hook child. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `path` | `SVGPathElement` | | +| `which` | `string` | - "from" or "to" | + +**Returns:** `HTMLElement` — the hook button + +### `function` getHookExit +*Line 88* + +Computes the extended control point for a hook: the hook centre pushed +`EXT` pixels in the hook's declared exit direction. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `hook` | `HTMLElement` | - the hook button element | +| `svgRect` | `DOMRect` | - bounding rect of the SVG viewport | + +**Returns:** `{ x: number, y: number ` — } + +### `function` drawpath +*Line 109* + +Draws (or redraws) an SVG connection path between two linked block hooks. +Each segment starts by extending EXT pixels in the source hook's exit +direction and ends by approaching from the target hook's exit direction. +The middle portion uses 45°-constrained routing. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `path` | `SVGPathElement` | - path element with fromHook / toHook references | + +### `function` _linkFlow +*Line 143* + +Programmatically connects two hooks with a logic flow link. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `fromBlock` | `HTMLElement` | - source block element | +| `fromHookSel` | `string` | - CSS selector for the source hook (e.g. ".hook", "#hook-fore") | +| `toBlock` | `HTMLElement` | - target block element | +| `toHookSel` | `string` | - CSS selector for the target hook | + +### `function` startShadowLink +*Line 168* + +Begins showing a dashed shadow path from the given hook to the cursor. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `hook` | `HTMLElement` | - the source hook button | + +### `function` onShadowMove +*Line 179* + +Updates the shadow path endpoint to follow the mouse. + +### `function` endShadowLink +*Line 201* + +Removes the shadow path and stops tracking the mouse. + +### `function` resetLinkage +*Line 211* + +Cancels any pending link operation and removes the shadow. + +### `function` deleteLink +*Line 223* + +Removes an SVG connection path and cleans up both blocks' flow arrays. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `path` | `SVGPathElement` | - the flow path element to delete | + + +--- +## sections.js + +### `const` _sec +*Line 23* + +Internal state for the sections subsystem. +@type {{ + list: Array<{id:string, points:Array<{x:number,y:number}>, title:string}>, + nextId: number, + creating: boolean, + curPoints: Array<{x:number,y:number}>, + editingId: string|null, + editOverlay: HTMLElement|null, + previewPoly: SVGPolygonElement|null, + previewLine: SVGLineElement|null, + ghostCircles: SVGCircleElement[] +}} + +### `function` _gridSize +*Line 38* + +**Returns:** `number` — The CSS `--grid-size` value, defaulting to 25. + +### `function` _snap +*Line 49* + +Snaps a coordinate value to the nearest grid multiple. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `v` | `number` | | + +**Returns:** `number` — + +### `function` _snapAngle +*Line 57* + +Constrains a new point to 0°, 45°, or 90° relative to the previous point. +@param {{x:number,y:number}} prev +@param {{x:number,y:number}} raw + +**Returns:** `{x:number,y:number` — } + +### `function` _xy +*Line 73* + +Converts a mouse event to grid-snapped coordinates relative to #draw-area. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `ev` | `MouseEvent` | | + +**Returns:** `{x:number, y:number` — } + +### `function` _svgPt +*Line 86* + +Converts absolute page coordinates to SVG-relative coordinates. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `x` | `number` | | +| `y` | `number` | | + +**Returns:** `{x:number, y:number` — } + +### `function` _shoelace +*Line 99* + +Computes the signed area of a polygon via the shoelace formula. +Used to reject tiny/collinear sections. +@param {Array<{x:number,y:number}>} pts + +**Returns:** `number` — + +### `function` _ptInPoly +*Line 116* + +Ray-casting point-in-polygon test. +@param {Array<{x:number,y:number}>} pts Polygon vertices in order + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `px` | `number` | | +| `py` | `number` | | + +**Returns:** `boolean` — + +### `function` _segmentsIntersect +*Line 134* + +Checks whether two line segments AB and CD intersect (including endpoints). +@param {{x:number,y:number}} a +@param {{x:number,y:number}} b +@param {{x:number,y:number}} c +@param {{x:number,y:number}} d + +**Returns:** `boolean` — + +### `function` _rectPolyOverlap +*Line 152* + +Checks whether a rectangle overlaps a polygon (any edge crossing or +one fully containing the other). +@param {Array<{x:number,y:number}>} pts Polygon vertices + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `rx` | `number` | Rectangle left | +| `ry` | `number` | Rectangle top | +| `rw` | `number` | Rectangle width | +| `rh` | `number` | Rectangle height | + +**Returns:** `boolean` — + +### `function` _pointsAttr +*Line 180* + +Converts a points array to an SVG `points` attribute string ("x,y x2,y2 …"). +@param {Array<{x:number,y:number}>} pts + +**Returns:** `string` — + +### `function` _topLeftOf +*Line 189* + +Returns the top-left-most point in a polygon (used for label positioning). +@param {Array<{x:number,y:number}>} pts + +**Returns:** `{x:number,y:number` — } + +### `function` _ensureSectionGroup +*Line 199* + +**Returns:** `SVGGElement|null` — The ``, creating it if needed. + +### `function` renderSection +*Line 215* + +Renders one section as SVG polygon + text label. +@param {{id:string, points:Array<{x:number,y:number}>, title:string}} s + +### `function` _removeSectionEls +*Line 242* + +Removes all SVG elements for a given section ID. @param {string} id + +### `function` renderAllSections +*Line 249* + +Re-renders every section from scratch. + +### `function` _showOverlay +*Line 258* + +Shows the dimming overlay that blocks interaction with blocks/links during section editing. + +### `function` _hideOverlay +*Line 270* + +Hides the dimming overlay. + +### `function` _clearPreview +*Line 282* + +Removes all preview visuals (polygon, line, ghost circles). + +### `function` _updatePreview +*Line 296* + +Updates the visual preview during polygon point placement. +@param {Array<{x:number,y:number}>} pts Points placed so far +@param {{x:number,y:number}|null} mouse Current snapped mouse position + +### `function` _onCreateClick +*Line 335* + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `ev` | `MouseEvent` | Handles a click during polygon creation (adds point or closes polygon). | + +### `function` _onCreateMove +*Line 357* + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `ev` | `MouseEvent` | Updates the preview line during mouse movement in create mode. | + +### `function` _onCreateDblClick +*Line 365* + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `ev` | `MouseEvent` | Finishes polygon creation on double-click. | + +### `function` _finishSection +*Line 375* + +Finalises a section from the placed points, adds it to the list, renders it, +and exits create mode. +@param {Array<{x:number,y:number}>} pts + +### `function` _cancelCreate +*Line 386* + +Cancels section creation without saving. + +### `function` _exitCreateMode +*Line 391* + +Cleans up all create-mode event listeners and preview elements. + +### `function` _onCreateKey +*Line 406* + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `ev` | `KeyboardEvent` | Handles Escape (cancel) / Enter (finish) during creation. | + +### `function` enterCreateMode +*Line 413* + +Enters polygon creation mode: sets up event listeners and the dimming overlay. + +### `function` _editTitle +*Line 432* + +Shows an inline text input (via SVG foreignObject) to edit a section's title. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `id` | `string` | Section ID | + +### `function` _commitTitle +*Line 471* + +Commits an edited section title and removes the editing foreignObject. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `id` | `string` | | +| `title` | `string` | | +| `fo` | `SVGForeignObjectElement` | | + +### `function` _cancelEdit +*Line 485* + +Cancels section title editing without saving. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `id` | `string` | | + +### `function` _hideOverlayIfIdle +*Line 495* + +Hides the overlay if neither creation nor editing is active. + +### `function` blockOnSectionBorder +*Line 507* + +Checks whether a block's bounding rectangle overlaps any section polygon. +Called during drag to prevent blocks from sitting on section borders. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `block` | `HTMLElement` | A WCBlock element | + +**Returns:** `boolean` — + +### `function` saveSections +*Line 520* + +**Returns:** `Array<{id:string, title:string, points:Array<{x:number,y:number` — >}>} + +### `function` restoreSections +*Line 530* + +Deserialises and restores sections from saved data. +@param {Array<{id?:string, title?:string, points?:Array<{x:number,y:number}>}>} data + +### `function` _onSectionClick +*Line 546* + +Handles a click on a section polygon: selects it, shows draggable vertex handles. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `ev` | `MouseEvent` | | + +### `function` _deselectSection +*Line 613* + +Deselects a section when clicking outside all section elements. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `ev` | `MouseEvent` | | + +### `function` deleteSelectedSection +*Line 624* + +Deletes the currently selected/editing section. + +### `function` initSections +*Line 638* + +Wires up section-related event listeners (SVG click, document click, delete key). + + +--- +## wcblock.js + +### `class` WCBlock +*Line 18* + +Base class for all draggable, linkable flowchart and measurement blocks. + + +--- +## blocks-algo.js + +### `class` WCStart +*Line 19* + +Program entry point. Cannot be deleted. + +### `class` WCEnd +*Line 27* + +Program exit block with errno autocomplete. At least one must return 0. + +### `class` WCOutput +*Line 36* + +Output description block. Provides inline documentation without generating code. + +### `class` WCDecision +*Line 55* + +Conditional branch block; splits flow into yes/no branches and emits if/else C code. + +### `class` WCAssign +*Line 137* + +Variable assignment block; writes to scalar/array/vector/string targets with data hooks. + +### `class` WCVarRef +*Line 247* + +Read-only variable reference with data output hook; syncs type from selected variable. + +### `class` WCExpression +*Line 281* + +Math expression block with LaTeX rendering via MathJax and dynamic variable data hooks. + + +--- +## blocks-data.js + +### `class` WCQuantity +*Line 20* + +Measurement quantity definition: name, 7 SI base dimensions, min/max range, default unit. + +### `class` WCUnit +*Line 67* + +Unit of measurement: name, associated quantity, suffix, SI factor, recursive SI resolution. + +### `class` WCBoolean +*Line 140* + +Boolean variable: name, true/false value, optional comment. Generates C `int`. + +### `class` WCScalar +*Line 173* + +Typed scalar variable with quantity, unit, SI conversion, and auto-sync between value and unit. + +### `class` WCVector +*Line 291* + +N-dimensional vector variable with quantity, unit, SI conversion; generates C array. + +### `class` WCString +*Line 428* + +String variable: name, value. Generates C `string` struct with malloc/memcpy. + +### `class` WCArray +*Line 463* + +Typed C array variable with dynamic element list and optional vector dimension. + + +--- +## blocks-proc.js + +### `const` LIB_ICONS +*Line 17* + +Type: `Object` Maps library IDs to icon image paths for procedure call blocks. + +### `class` WCProcedure +*Line 27* + +Procedure definition block with inputs/outputs/description/return-style. Lives in the Procedures tab. + +### `const` _FMT_SPEC_RE +*Line 150* + +Type: `RegExp` Matches C printf-format specifiers, capturing the type character. + +### `function` _specDataType +*Line 157* + +Maps a printf format specifier character to a C data type. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `spec` | `string` | Single format character (e.g. "d", "f", "s") | + +**Returns:** `string` — "int", "double", or "string" + +### `class` WCProcCall +*Line 166* + +Procedure call block with dynamic argument fields, format-string support, and error-handling hooks. Lives in the Algorithm tab. + + +--- +## wcprogram.js + +### `class` WCProgram +*Line 24* + +Root container custom element; owns the flowchart workspace, block creation, and code generation. + +### `const` INCLUDE_MAP +*Line 252* + +Maps library names to their C #include paths. +Libraries not listed here use the default `<{name}.h>` convention. + +### `const` BLOCK_REGISTRY +*Line 263* + +Type: `ObjectHTMLElement)|null` >} + +### `const` BLOCK_ACTIONS +*Line 265* + +Type: `Object` Maps toolbar action names to block tag names. + +### `function` registerBlock +*Line 275* + +Registers a custom element class and adds it to the block registry and action map. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `tag` | `string` | Custom element tag (e.g. "wc-decision") | +| `cls` | `typeof WCBlock` | Class constructor | +| `prefix` | `string` | ID prefix for auto-generated block IDs | +| `creatable` | `string|null` | Action name for toolbar buttons, or null if not toolbar-creatable | +| `getParent` | `(()=>HTMLElement)|null` | Function returning the parent container, or null for algorithm pane | + +### `const` _connObserver +*Line 299* + +Type: `MutationObserver` Watches SVG connections for changes and refreshes assign-block value visibility. + + +--- +## persist.js + +### `function` saveBlockData +*Line 18* + +Reads the position and type-specific data from a single block element. +Delegates to the block's saveData() method for type-specific fields. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `block` | `HTMLElement` | - a WCBlock or WCProgram child element | + +**Returns:** `{ x: number, y: number, ... ` — } + +### `function` saveWorkspace +*Line 33* + +Serialises the entire workspace to localStorage under STORAGE_KEY. +Captures all blocks from algorithm, measurements, and data panes, +plus every SVG connection path. + +### `function` restoreBlock +*Line 97* + +Deep-clones a block by tag name and restores position + data onto it. +Delegates to the element's restoreData() method for type-specific fields. +@param {{ id: string, tag: string, data: object }} info - saved block descriptor + +**Returns:** `HTMLElement|null` — the restored element, or null for unknown tags + +### `function` restoreGroup +*Line 120* + +Restores a group of block descriptors into a container in a single pass. +Creates each block, appends it, then calls restoreData on all of them. +@param {{ id: string, tag: string, data: object }[]} infos - saved block descriptors + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `container` | `HTMLElement` | - parent element to append blocks into | + +### `function` loadWorkspace +*Line 134* + +Deserialises the workspace from localStorage and reconstructs all blocks, +connections, and internal state. Existing user blocks are removed first. + +### `function` newWorkspace +*Line 270* + +Clears the saved workspace (removes localStorage entry) and reloads the page, +returning the app to its pristine default state. + + +--- +## lib-loader.js + +### `const` LOADED_LIBS +*Line 13* + +Type: `Set` Tracks which libraries have been loaded (prevents re-loading). + +### `function` _addProcParams +*Line 24* + +Creates param rows on a procedure block and sets their name/type values. +Follows the same pattern as WCProcedure.restoreData(). + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `proc` | `HTMLElement` | A wc-procedure element | +| `inputs` | `Array<[string,string]>` | Array of [name, type] pairs | +| `outputs` | `Array<[string,string]>` | Array of [name, type] pairs | + +### `function` getLibraryIndex +*Line 115* + +Returns the loaded library index (synchronous after loadLibraryIndex). + +**Returns:** `Array<{id:string, name:string, group?:string` — >} + +### `function` isLibraryLoaded +*Line 125* + +Checks if a library has been loaded yet. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `libId` | `string` | | + +**Returns:** `boolean` — + +### `function` markLibLoaded +*Line 134* + +Marks a library as loaded (for user-created libs that don't have JSON files). + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `libId` | `string` | | + + +--- +## proc-browser.js + +### `function` _buildTreeData +*Line 16* + +Builds the tree data structure from all wc-procedure blocks and the library index. + +**Returns:** `Array<{label:string, items:Array<{qname:string, naturalName:string, techName:string, readonly:boolean` — >, userDefined:boolean}>} + +### `function` _createGroupElement +*Line 83* + +Creates a collapsible group DOM element in the procedure browser tree. +@param {{label:string, items:Array, userDefined:boolean}} group + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `[selectedQname]` | `string` | QName to pre-select | + +**Returns:** `HTMLElement` — + +### `function` _applySearchFilter +*Line 163* + +Filters the procedure tree by a search query, showing/hiding groups and items. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `query` | `string` | | +| `overlay` | `HTMLElement` | The procedure browser overlay element | + +### `function` _escapeHtml +*Line 230* + +Escapes HTML special characters for safe innerHTML insertion. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `str` | `string` | | + +**Returns:** `string` — + +### `function` _confirmSelection +*Line 240* + +Confirms a procedure selection, closes the browser, and invokes the callback. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `qname` | `string` | Qualified procedure name | + +### `function` _cancelBrowser +*Line 250* + +Cancels procedure selection, closes the browser, invokes callback with null. + +### `function` showProcedureBrowser +*Line 264* + +Opens the procedure browser modal, builds the tree, and wires up search/cancel/select. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `selectedQname` | `string` | | +| `callback` | `(qname:string|null)=>void` | | + + +--- +## templates.js + +### `function` _clearWorkspace +*Line 16* + +Removes all user-created blocks, connections, library tabs and panes, +resetting flow arrays and LOADED_LIBS. Used by templates (new project) and +persist.js (load workspace, with keepStorage:true to preserve localStorage). +@param {{ keepStorage?: boolean }} [opts] + + +--- +## app.js + +### `function` createDefaultMeasurements +*Line 19* + +Pre-populates the Measurements tab with SI base and coherent derived types/units. +22 types and 22 units arranged in a 4-column grid. +No prefixed units are created except kilogram (the SI base unit for mass). + +### `function` addLibTab +*Line 118* + +Creates a library tab and its associated pane in the Procedures tab. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `libName` | `string` | - library identifier | +| `[makeActive]` | `boolean` | - whether to mark the tab/pane as active | + +**Returns:** `HTMLElement` — the #procedures-area element inside the new pane + +### `function` switchTab +*Line 170* + +Activates the clicked tab and shows its corresponding pane. + +**Parameters:** + +| Name | Type | Description | +|------|------|------------| +| `name` | `string` | - tab identifier (matches data-tab attribute and pane id suffix) | + +### `function` setupUI +*Line 183* + +Wires all static button click handlers (replaces inline onclick attributes). + +### `function` main +*Line 426* + +Bootstraps the application: builds the flowchart canvas with start/end blocks, +creates default SI measurements, optionally loads a saved workspace, then +wires up all UI event listeners. + diff --git a/draw.js b/draw.js index 7a50e95..77a8274 100644 --- a/draw.js +++ b/draw.js @@ -8,8 +8,13 @@ // Depends on: dom.js (svgNamespace, $, $id) // -const EXT = 25; // pixels the path extends from a hook in its exit direction +/** @type {number} Pixels the path extends from a hook in its exit direction. */ +const EXT = 25; + +/** @type {{block: HTMLElement, hook: HTMLElement}|null} First hook clicked in a two-click link operation. */ let linkageFirst = null; + +/** @type {SVGPathElement|null} The shadow path element shown while linking, or null. */ let shadowPath = null; @@ -67,7 +72,9 @@ function routeBetween(x1, y1, x2, y2) { function resolveHook(path, which) { const key = which === "from" ? "fromHook" : "toHook"; const elKey = which === "from" ? "fromElement" : "toElement"; - return path[key] || path[elKey].querySelector(".hook"); + if (path[key]) return path[key]; + const el = path[elKey]; + return el ? el.querySelector(".hook") : null; } @@ -79,6 +86,7 @@ function resolveHook(path, which) { * @returns {{ x: number, y: number }} */ function getHookExit(hook, svgRect) { + if (!hook) return { x: 0, y: 0 }; const r = hook.getBoundingClientRect(); const x = (r.left + r.right) / 2 - svgRect.left; const y = (r.top + r.bottom) / 2 - svgRect.top; @@ -99,11 +107,14 @@ function getHookExit(hook, svgRect) { * @param {SVGPathElement} path - path element with fromHook / toHook references */ function drawpath(path) { + if (!path || !path.isConnected) return; const svg = path.closest("svg"); + if (!svg) return; const svgRect = svg.getBoundingClientRect(); const fromHook = resolveHook(path, "from"); const toHook = resolveHook(path, "to"); + if (!fromHook || !toHook || !fromHook.isConnected || !toHook.isConnected) return; const rFrom = fromHook.getBoundingClientRect(); const rTo = toHook.getBoundingClientRect(); @@ -123,6 +134,39 @@ function drawpath(path) { } +/** + * Programmatically connects two hooks with a logic flow link. + * @param {HTMLElement} fromBlock - source block element + * @param {string} fromHookSel - CSS selector for the source hook (e.g. ".hook", "#hook-fore") + * @param {HTMLElement} toBlock - target block element + * @param {string} toHookSel - CSS selector for the target hook + */ +function _linkFlow(fromBlock, fromHookSel, toBlock, toHookSel) { + const fromHook = fromBlock.querySelector(fromHookSel); + const toHook = toBlock.querySelector(toHookSel); + if (!fromHook || !toHook) return; + const path = document.createElementNS(svgNamespace, "path"); + path.toElement = toBlock; + path.fromElement = fromBlock; + path.fromHook = fromHook; + path.toHook = toHook; + toHook.dataset.connected = ""; + path.dataset.linkType = "logic"; + path.classList.add("flow"); + if (fromHook.classList.contains("proccall-error-hook")) { + path.classList.add("flow-error-source"); + path.classList.add("flow-error"); + } else if (fromBlock.classList.contains("error-path")) { + path.classList.add("flow-error"); + } + toBlock.wcForeFlow.push(path); + fromBlock.wcNextFlow.push(path); + document.getElementById("connections").appendChild(path); + drawpath(path); + refreshErrorPaths(); +} + + // ── Shadow (preview) link ───────────────────────────────────────────────────── /** @@ -137,12 +181,15 @@ function startShadowLink(hook) { shadowPath._hook = hook; svg.appendChild(shadowPath); document.addEventListener("mousemove", onShadowMove); + const area = $id("draw-area"); + if (area) area.classList.add("linking"); } /** Updates the shadow path endpoint to follow the mouse. */ function onShadowMove(ev) { if (!shadowPath) return; const svg = shadowPath.closest("svg"); + if (!svg) return; const svgRect = svg.getBoundingClientRect(); const mx = ev.clientX - svgRect.left; const my = ev.clientY - svgRect.top; @@ -160,13 +207,15 @@ function onShadowMove(ev) { ); } -/** Removes the shadow path and stops tracking the mouse. */ +/** Removes the shadow path, stops tracking the mouse, and hides empty input hooks. */ function endShadowLink() { if (shadowPath) { shadowPath.remove(); shadowPath = null; } document.removeEventListener("mousemove", onShadowMove); + const area = $id("draw-area"); + if (area) area.classList.remove("linking"); } @@ -198,10 +247,18 @@ function deleteLink(path) { if (i !== -1) tgt.wcForeFlow.splice(i, 1); } + if (path.toHook) { + delete path.toHook.dataset.connected; + _dataSourceCache.delete(path.toHook); + } + path.remove(); path.fromElement = null; path.toElement = null; path.fromHook = null; path.toHook = null; + + refreshBlockWarnings(); + refreshErrorPaths(); } diff --git a/errno-data.js b/errno-data.js new file mode 100644 index 0000000..373f234 --- /dev/null +++ b/errno-data.js @@ -0,0 +1,137 @@ +// ── Errno table: code, name, description ──────────────────────────────────── +// Generated from the system's errno definitions. +// Used by WCEnd blocks to select exit codes. + +/** @type {Array<{code:number, name:string, desc:string}>} */ +const ERRNO_LIST = [ + { code: 1, name: 'EPERM', desc: 'Operation not permitted' }, + { code: 2, name: 'ENOENT', desc: 'No such file or directory' }, + { code: 3, name: 'ESRCH', desc: 'No such process' }, + { code: 4, name: 'EINTR', desc: 'Interrupted system call' }, + { code: 5, name: 'EIO', desc: 'Input/output error' }, + { code: 6, name: 'ENXIO', desc: 'No such device or address' }, + { code: 7, name: 'E2BIG', desc: 'Argument list too long' }, + { code: 8, name: 'ENOEXEC', desc: 'Exec format error' }, + { code: 9, name: 'EBADF', desc: 'Bad file descriptor' }, + { code: 10, name: 'ECHILD', desc: 'No child processes' }, + { code: 11, name: 'EAGAIN', desc: 'Resource temporarily unavailable' }, + { code: 12, name: 'ENOMEM', desc: 'Cannot allocate memory' }, + { code: 13, name: 'EACCES', desc: 'Permission denied' }, + { code: 14, name: 'EFAULT', desc: 'Bad address' }, + { code: 15, name: 'ENOTBLK', desc: 'Block device required' }, + { code: 16, name: 'EBUSY', desc: 'Device or resource busy' }, + { code: 17, name: 'EEXIST', desc: 'File exists' }, + { code: 18, name: 'EXDEV', desc: 'Invalid cross-device link' }, + { code: 19, name: 'ENODEV', desc: 'No such device' }, + { code: 20, name: 'ENOTDIR', desc: 'Not a directory' }, + { code: 21, name: 'EISDIR', desc: 'Is a directory' }, + { code: 22, name: 'EINVAL', desc: 'Invalid argument' }, + { code: 23, name: 'ENFILE', desc: 'Too many open files in system' }, + { code: 24, name: 'EMFILE', desc: 'Too many open files' }, + { code: 25, name: 'ENOTTY', desc: 'Inappropriate ioctl for device' }, + { code: 26, name: 'ETXTBSY', desc: 'Text file busy' }, + { code: 27, name: 'EFBIG', desc: 'File too large' }, + { code: 28, name: 'ENOSPC', desc: 'No space left on device' }, + { code: 29, name: 'ESPIPE', desc: 'Illegal seek' }, + { code: 30, name: 'EROFS', desc: 'Read-only file system' }, + { code: 31, name: 'EMLINK', desc: 'Too many links' }, + { code: 32, name: 'EPIPE', desc: 'Broken pipe' }, + { code: 33, name: 'EDOM', desc: 'Numerical argument out of domain' }, + { code: 34, name: 'ERANGE', desc: 'Numerical result out of range' }, + { code: 35, name: 'EDEADLOCK', desc: 'Resource deadlock avoided' }, + { code: 36, name: 'ENAMETOOLONG', desc: 'File name too long' }, + { code: 37, name: 'ENOLCK', desc: 'No locks available' }, + { code: 38, name: 'ENOSYS', desc: 'Function not implemented' }, + { code: 39, name: 'ENOTEMPTY', desc: 'Directory not empty' }, + { code: 40, name: 'ELOOP', desc: 'Too many levels of symbolic links' }, + { code: 42, name: 'ENOMSG', desc: 'No message of desired type' }, + { code: 43, name: 'EIDRM', desc: 'Identifier removed' }, + { code: 44, name: 'ECHRNG', desc: 'Channel number out of range' }, + { code: 45, name: 'EL2NSYNC', desc: 'Level 2 not synchronized' }, + { code: 46, name: 'EL3HLT', desc: 'Level 3 halted' }, + { code: 47, name: 'EL3RST', desc: 'Level 3 reset' }, + { code: 48, name: 'ELNRNG', desc: 'Link number out of range' }, + { code: 49, name: 'EUNATCH', desc: 'Protocol driver not attached' }, + { code: 50, name: 'ENOCSI', desc: 'No CSI structure available' }, + { code: 51, name: 'EL2HLT', desc: 'Level 2 halted' }, + { code: 52, name: 'EBADE', desc: 'Invalid exchange' }, + { code: 53, name: 'EBADR', desc: 'Invalid request descriptor' }, + { code: 54, name: 'EXFULL', desc: 'Exchange full' }, + { code: 55, name: 'ENOANO', desc: 'No anode' }, + { code: 56, name: 'EBADRQC', desc: 'Invalid request code' }, + { code: 57, name: 'EBADSLT', desc: 'Invalid slot' }, + { code: 59, name: 'EBFONT', desc: 'Bad font file format' }, + { code: 60, name: 'ENOSTR', desc: 'Device not a stream' }, + { code: 61, name: 'ENODATA', desc: 'No data available' }, + { code: 62, name: 'ETIME', desc: 'Timer expired' }, + { code: 63, name: 'ENOSR', desc: 'Out of streams resources' }, + { code: 64, name: 'ENONET', desc: 'Machine is not on the network' }, + { code: 65, name: 'ENOPKG', desc: 'Package not installed' }, + { code: 66, name: 'EREMOTE', desc: 'Object is remote' }, + { code: 67, name: 'ENOLINK', desc: 'Link has been severed' }, + { code: 68, name: 'EADV', desc: 'Advertise error' }, + { code: 69, name: 'ESRMNT', desc: 'Srmount error' }, + { code: 70, name: 'ECOMM', desc: 'Communication error on send' }, + { code: 71, name: 'EPROTO', desc: 'Protocol error' }, + { code: 72, name: 'EMULTIHOP', desc: 'Multihop attempted' }, + { code: 73, name: 'EDOTDOT', desc: 'RFS specific error' }, + { code: 74, name: 'EBADMSG', desc: 'Bad message' }, + { code: 75, name: 'EOVERFLOW', desc: 'Value too large for defined data type' }, + { code: 76, name: 'ENOTUNIQ', desc: 'Name not unique on network' }, + { code: 77, name: 'EBADFD', desc: 'File descriptor in bad state' }, + { code: 78, name: 'EREMCHG', desc: 'Remote address changed' }, + { code: 79, name: 'ELIBACC', desc: 'Can not access a needed shared library' }, + { code: 80, name: 'ELIBBAD', desc: 'Accessing a corrupted shared library' }, + { code: 81, name: 'ELIBSCN', desc: '.lib section in a.out corrupted' }, + { code: 82, name: 'ELIBMAX', desc: 'Attempting to link in too many shared libraries' }, + { code: 83, name: 'ELIBEXEC', desc: 'Cannot exec a shared library directly' }, + { code: 84, name: 'EILSEQ', desc: 'Invalid or incomplete multibyte or wide character' }, + { code: 85, name: 'ERESTART', desc: 'Interrupted system call should be restarted' }, + { code: 86, name: 'ESTRPIPE', desc: 'Streams pipe error' }, + { code: 87, name: 'EUSERS', desc: 'Too many users' }, + { code: 88, name: 'ENOTSOCK', desc: 'Socket operation on non-socket' }, + { code: 89, name: 'EDESTADDRREQ', desc: 'Destination address required' }, + { code: 90, name: 'EMSGSIZE', desc: 'Message too long' }, + { code: 91, name: 'EPROTOTYPE', desc: 'Protocol wrong type for socket' }, + { code: 92, name: 'ENOPROTOOPT', desc: 'Protocol not available' }, + { code: 93, name: 'EPROTONOSUPPORT', desc: 'Protocol not supported' }, + { code: 94, name: 'ESOCKTNOSUPPORT', desc: 'Socket type not supported' }, + { code: 95, name: 'ENOTSUP', desc: 'Operation not supported' }, + { code: 96, name: 'EPFNOSUPPORT', desc: 'Protocol family not supported' }, + { code: 97, name: 'EAFNOSUPPORT', desc: 'Address family not supported by protocol' }, + { code: 98, name: 'EADDRINUSE', desc: 'Address already in use' }, + { code: 99, name: 'EADDRNOTAVAIL', desc: 'Cannot assign requested address' }, + { code: 100, name: 'ENETDOWN', desc: 'Network is down' }, + { code: 101, name: 'ENETUNREACH', desc: 'Network is unreachable' }, + { code: 102, name: 'ENETRESET', desc: 'Network dropped connection on reset' }, + { code: 103, name: 'ECONNABORTED', desc: 'Software caused connection abort' }, + { code: 104, name: 'ECONNRESET', desc: 'Connection reset by peer' }, + { code: 105, name: 'ENOBUFS', desc: 'No buffer space available' }, + { code: 106, name: 'EISCONN', desc: 'Transport endpoint is already connected' }, + { code: 107, name: 'ENOTCONN', desc: 'Transport endpoint is not connected' }, + { code: 108, name: 'ESHUTDOWN', desc: 'Cannot send after transport endpoint shutdown' }, + { code: 109, name: 'ETOOMANYREFS', desc: 'Too many references: cannot splice' }, + { code: 110, name: 'ETIMEDOUT', desc: 'Connection timed out' }, + { code: 111, name: 'ECONNREFUSED', desc: 'Connection refused' }, + { code: 112, name: 'EHOSTDOWN', desc: 'Host is down' }, + { code: 113, name: 'EHOSTUNREACH', desc: 'No route to host' }, + { code: 114, name: 'EALREADY', desc: 'Operation already in progress' }, + { code: 115, name: 'EINPROGRESS', desc: 'Operation now in progress' }, + { code: 116, name: 'ESTALE', desc: 'Stale file handle' }, + { code: 117, name: 'EUCLEAN', desc: 'Structure needs cleaning' }, + { code: 118, name: 'ENOTNAM', desc: 'Not a XENIX named type file' }, + { code: 119, name: 'ENAVAIL', desc: 'No XENIX semaphores available' }, + { code: 120, name: 'EISNAM', desc: 'Is a named type file' }, + { code: 121, name: 'EREMOTEIO', desc: 'Remote I/O error' }, + { code: 122, name: 'EDQUOT', desc: 'Disk quota exceeded' }, + { code: 123, name: 'ENOMEDIUM', desc: 'No medium found' }, + { code: 124, name: 'EMEDIUMTYPE', desc: 'Wrong medium type' }, + { code: 125, name: 'ECANCELED', desc: 'Operation canceled' }, + { code: 126, name: 'ENOKEY', desc: 'Required key not available' }, + { code: 127, name: 'EKEYEXPIRED', desc: 'Key has expired' }, + { code: 128, name: 'EKEYREVOKED', desc: 'Key has been revoked' }, + { code: 129, name: 'EKEYREJECTED', desc: 'Key was rejected by service' }, + { code: 130, name: 'EOWNERDEAD', desc: 'Owner died' }, + { code: 131, name: 'ENOTRECOVERABLE', desc: 'State not recoverable' }, + { code: 132, name: 'ERFKILL', desc: 'Operation not possible due to RF-kill' }, +]; diff --git a/error.js b/error.js index 1374c97..2dbe147 100644 --- a/error.js +++ b/error.js @@ -11,7 +11,9 @@ */ function showError(msg) { const el = $("#error-overlay"); - el.querySelector(".error-msg").textContent = msg; + if (!el) { console.error("ACK: " + msg); return; } + const msgEl = el.querySelector(".error-msg"); + if (msgEl) msgEl.textContent = msg; el.classList.add("active"); } @@ -21,11 +23,13 @@ function showError(msg) { * @param {MouseEvent} [ev] - event to check target matching */ function closeError(ev) { + const el = $("#error-overlay"); + if (!el) return; if ( !ev || ev.target === ev.currentTarget || ev.target.classList.contains("error-close") ) { - $("#error-overlay").classList.remove("active"); + el.classList.remove("active"); } } diff --git a/helpers.js b/helpers.js index 3276c22..a88c263 100644 --- a/helpers.js +++ b/helpers.js @@ -11,11 +11,13 @@ // getDefaultUnitSuffix() → lookup default unit for a quantity name // refreshDefaultUnitOptions() → sync default-unit dropdowns on quantity blocks // refreshVarRefOptions() → sync variable-select dropdowns -// refreshProcOptions() → sync procedure-select dropdowns // switchLibTab() → activate library sub-tab // findDataSource() → find block connected to a data hook // escapeString() → escape special chars for C string literal +// refreshBlockWarnings() → check every algorithm block for missing input +// refreshErrorPaths() → mark blocks only reachable via error-flow paths // refreshAll() → run all refresh passes in order +// copyCode() → copy generated C code to clipboard /** @@ -66,9 +68,14 @@ function rebuildSelect(sel, placeholder, values, current) { * all wc-quantity blocks in the data area. */ function refreshQuantityOptions() { + const dataArea = $id("data-area"); + if (!dataArea) return; const qNames = Array.from( - $id("data-area").querySelectorAll("wc-quantity") - ).map(q => (q.querySelector(".quantity-name").value || "unnamed")); + dataArea.querySelectorAll("wc-quantity") + ).map(q => { + const el = q.querySelector(".quantity-name"); + return el ? (el.value || "unnamed") : "unnamed"; + }); document.querySelectorAll( "#variable-area .var-type, #data-area .unit-type" @@ -84,18 +91,28 @@ function refreshQuantityOptions() { * that match the datum's currently selected quantity. */ function refreshUnitOptions() { - $id("variable-area").querySelectorAll("wc-scalar").forEach(d => { - const quantityName = d.querySelector(".var-type").value; + const varArea = $id("variable-area"); + const dataArea = $id("data-area"); + if (!varArea || !dataArea) return; + varArea.querySelectorAll("wc-scalar").forEach(d => { + const typeEl = d.querySelector(".var-type"); const sel = d.querySelector(".var-unit"); + if (!typeEl || !sel) return; + const quantityName = typeEl.value; const suffixes = Array.from( - $id("data-area").querySelectorAll("wc-unit") + dataArea.querySelectorAll("wc-unit") ) .filter( - u => - u.querySelector(".unit-type").value === quantityName && - u.querySelector(".unit-suffix").value + u => { + const t = u.querySelector(".unit-type"); + const s = u.querySelector(".unit-suffix"); + return t && s && t.value === quantityName && s.value; + } ) - .map(u => u.querySelector(".unit-suffix").value); + .map(u => { + const s = u.querySelector(".unit-suffix"); + return s ? s.value : ""; + }); rebuildSelect(sel, "(unit)", suffixes, sel.value); }); } @@ -106,9 +123,13 @@ function refreshUnitOptions() { * @returns {string} default unit suffix, or "" if not found */ function getDefaultUnitSuffix(quantityName) { - for (const q of $id("data-area").querySelectorAll("wc-quantity")) { - if ((q.querySelector(".quantity-name").value || "unnamed") === quantityName) { - return q.querySelector(".quantity-default-unit").value || ""; + const dataArea = $id("data-area"); + if (!dataArea) return ""; + for (const q of dataArea.querySelectorAll("wc-quantity")) { + const nameEl = q.querySelector(".quantity-name"); + if (nameEl && (nameEl.value || "unnamed") === quantityName) { + const unitEl = q.querySelector(".quantity-default-unit"); + return unitEl ? (unitEl.value || "") : ""; } } return ""; @@ -119,18 +140,27 @@ function getDefaultUnitSuffix(quantityName) { * of matching unit blocks. */ function refreshDefaultUnitOptions() { - $id("data-area").querySelectorAll("wc-quantity").forEach(q => { - const quantityName = q.querySelector(".quantity-name").value || "unnamed"; + const dataArea = $id("data-area"); + if (!dataArea) return; + dataArea.querySelectorAll("wc-quantity").forEach(q => { + const nameEl = q.querySelector(".quantity-name"); const sel = q.querySelector(".quantity-default-unit"); + if (!nameEl || !sel) return; + const quantityName = nameEl.value || "unnamed"; const suffixes = Array.from( - $id("data-area").querySelectorAll("wc-unit") + dataArea.querySelectorAll("wc-unit") ) .filter( - u => - u.querySelector(".unit-type").value === quantityName && - u.querySelector(".unit-suffix").value + u => { + const t = u.querySelector(".unit-type"); + const s = u.querySelector(".unit-suffix"); + return t && s && t.value === quantityName && s.value; + } ) - .map(u => u.querySelector(".unit-suffix").value); + .map(u => { + const s = u.querySelector(".unit-suffix"); + return s ? s.value : ""; + }); rebuildSelect(sel, "(none)", suffixes, sel.value); }); } @@ -142,11 +172,15 @@ function refreshDefaultUnitOptions() { function refreshVarRefOptions() { const selects = document.querySelectorAll(".varref-select, .assign-target"); if (!selects.length) return; + const varArea = $id("variable-area"); const vars = []; - for (const b of $id("variable-area").querySelectorAll("wc-boolean, wc-scalar, wc-string, wc-array")) { - const name = (b.querySelector(".var-name") || {}).value || "unnamed"; - const tag = b.tagName.toLowerCase().replace("wc-", ""); - vars.push({ id: b.id, label: tag + ": " + name }); + if (varArea) { + for (const b of varArea.querySelectorAll("wc-boolean, wc-scalar, wc-string, wc-array, wc-vector")) { + const nameEl = b.querySelector(".var-name"); + const name = nameEl ? (nameEl.value || "unnamed") : "unnamed"; + const tag = b.tagName.toLowerCase().replace("wc-", ""); + vars.push({ id: b.id, label: tag + ": " + name }); + } } for (const sel of selects) { const current = sel.dataset.restoredValue || sel.value; @@ -164,31 +198,6 @@ function refreshVarRefOptions() { } } -/** - * Refreshes the procedure-select dropdown on every wc-proccall block. - * Scans all wc-procedure blocks and populates options as "library/name". - */ -function refreshProcOptions() { - const proccalls = document.querySelectorAll("wc-proccall"); - if (!proccalls.length) return; - const options = Array.from(document.querySelectorAll("wc-procedure")).map(p => p.getQualifiedName()); - for (const call of proccalls) { - const sel = call.querySelector(".proccall-select"); - const current = sel.dataset.restoredValue || sel.value; - delete sel.dataset.restoredValue; - sel.innerHTML = '(select procedure)'; - for (const opt of options) { - const o = document.createElement("option"); - o.value = opt; - o.textContent = opt; - sel.appendChild(o); - } - if (current && [...sel.options].some(o => o.value === current)) { - sel.value = current; - } - } -} - /** * Activates the clicked library sub-tab and shows its corresponding pane. * @param {string} name - library name (matches data-lib-name attribute) @@ -202,18 +211,32 @@ function switchLibTab(name) { } } +/** @type {WeakMap} Cache mapping data hooks to their source blocks. */ +const _dataSourceCache = new WeakMap(); + /** * Finds the block connected to a given data hook via any .flow-data path. + * Results are cached in _dataSourceCache. * @param {HTMLElement} hook - the data hook button element * @returns {HTMLElement|null} the source block, or null if nothing is connected */ function findDataSource(hook) { + if (_dataSourceCache.has(hook)) return _dataSourceCache.get(hook); for (const path of document.querySelectorAll("path.flow-data")) { - if (path.toHook === hook) return path.fromElement; + if (path.toHook === hook) { + _dataSourceCache.set(hook, path.fromElement); + return path.fromElement; + } } return null; } +/** Copies the generated C code text to the clipboard. */ +function copyCode() { + const el = $id("c-code"); + if (el && el.textContent) navigator.clipboard.writeText(el.textContent).catch(() => {}); +} + /** * Escapes special characters for embedding in a C string literal. * Handles backslash, double-quote, newline, carriage return, and tab. @@ -223,9 +246,18 @@ function findDataSource(hook) { function escapeString(str) { return str .replace(/\\/g, "\\\\").replace(/"/g, '\\"') - .replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"); + .replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t") + .replace(/\f/g, "\\f").replace(/\v/g, "\\v").replace(/\0/g, "\\0"); } +/** @type {Set} C reserved keywords — toCName appends "_" to avoid clashes. */ +const C_KEYWORDS = new Set([ + "auto","break","case","char","const","continue","default","do","double", + "else","enum","extern","float","for","goto","if","int","long","register", + "return","short","signed","sizeof","static","struct","switch","typedef", + "union","unsigned","void","volatile","while" +]); + /** * Converts a natural-language name into a valid C identifier. * Lowercases, replaces non-alphanumeric chars with underscores, @@ -240,7 +272,40 @@ function toCName(str) { s = s.replace(/_+/g, "_"); s = s.replace(/^_|_$/g, ""); if (/^\d/.test(s)) s = "_" + s; - return s || "var"; + if (!s || C_KEYWORDS.has(s)) s = (s || "var") + "_"; + return s; +} + +/** + * Derives a human-readable natural name from a C function/tech name. + * Converts camelCase and snake_case to words, strips common prefixes. + * @param {string} techName - e.g. "SDL_CreateWindow", "glfwInit", "puts" + * @returns {string} e.g. "Create Window", "Init GLFW", "Puts" + */ +function deriveNaturalName(techName) { + if (!techName) return ""; + let s = techName; + s = s.replace(/^(SDL_|glfw|gl_?)/i, ''); + const known = { + puts: "Print String", + printf: "Print Formatted", + getchar: "Get Char", + putchar: "Put Char", + perror: "Print Error", + malloc: "Allocate Memory", + atoi: "Parse Int", + atof: "Parse Float", + strlen: "String Length", + strcmp: "Compare Strings", + fabs: "Abs Float", + fmod: "Float Modulo", + }; + if (known[s]) return known[s]; + s = s.replace(/([a-z])([A-Z])/g, '$1 $2'); + s = s.replace(/_/g, ' '); + s = s.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2'); + s = s.replace(/\s+/g, ' ').trim(); + return s.charAt(0).toUpperCase() + s.slice(1); } /** @@ -281,11 +346,189 @@ function resolveSourceName(block, visited) { return ""; } +/** + * Opens a modal dialog that shows the block's editable fields in a compact, + * full-width layout for easier editing. + * @param {HTMLElement} block + */ +function showBlockEditor(block) { + let overlay = $id("block-editor-overlay"); + if (!overlay) { + overlay = document.createElement("div"); + overlay.id = "block-editor-overlay"; + overlay.className = "block-editor-overlay"; + overlay.addEventListener("click", ev => { if (ev.target === overlay) overlay.classList.remove("active"); }); + document.body.appendChild(overlay); + + const editorDiv = document.createElement("div"); + editorDiv.className = "block-editor-modal"; + editorDiv.innerHTML = + '' + + ' Edit Block' + + ' ×' + + '' + + '' + + ''; + overlay.appendChild(editorDiv); + + editorDiv.querySelector(".block-editor-close").addEventListener("click", () => overlay.classList.remove("active")); + editorDiv.querySelector(".block-editor-cancel").addEventListener("click", () => overlay.classList.remove("active")); + } + + const body = overlay.querySelector(".block-editor-body"); + body.innerHTML = ""; + + /* Colour picker row; error blocks default to red, others to blue */ + const defaultColor = block.classList.contains("error-path") || block.tagName === "WC-END" ? "#ef5350" : "#4a90d9"; + const colorRow = document.createElement("div"); + colorRow.className = "block-editor-color-row"; + colorRow.innerHTML = + 'Accent colour' + + '' + + 'Reset'; + body.appendChild(colorRow); + + const colorPicker = colorRow.querySelector(".block-editor-color-picker"); + const origColor = block.dataset.blockColor || ""; + + colorRow.querySelector(".block-editor-color-reset").addEventListener("click", () => { + colorPicker.value = defaultColor; + }); + + const clone = block.cloneNode(true); + clone.style.position = "static"; + clone.style.width = ""; + clone.style.height = ""; + // Remove interactive buttons from clone (edit, delete, resize) + for (const btn of clone.querySelectorAll(".edit-btn, .delete-btn, .resize-handle, .hook")) btn.remove(); + // Restore hidden inputs and remove their labels so the modal shows editable fields + for (const label of clone.querySelectorAll(".field-display")) { + const input = label.nextElementSibling; + if (input && (input.tagName === "INPUT" || input.tagName === "SELECT" || input.tagName === "TEXTAREA")) { + input.style.display = ""; + } + label.remove(); + } + body.appendChild(clone); + + overlay.querySelector(".block-editor-title").textContent = "Edit " + (block.tagName || "Block"); + overlay.classList.add("active"); + + overlay.querySelector(".block-editor-apply").onclick = () => { + // Sync inputs/selects/textareas from clone back to original + const srcFields = block.querySelectorAll("input, select, textarea"); + const cloneFields = clone.querySelectorAll("input, select, textarea"); + for (let i = 0; i < srcFields.length && i < cloneFields.length; i++) { + if (srcFields[i].type !== "hidden") srcFields[i].value = cloneFields[i].value; + } + // Trigger change and input events so block internals sync + for (const el of srcFields) { + el.dispatchEvent(new Event("change", { bubbles: true })); + el.dispatchEvent(new Event("input", { bubbles: true })); + } + // For END blocks: parse and select the errno from the synced value + if (block.tagName === "WC-END" && typeof block._selectByCode === "function") { + const val = block._el?.input?.value || ""; + const code = parseInt(val); + if (!isNaN(code)) block._selectByCode(code); + } + + /* Apply colour */ + const newColor = colorPicker.value; + if (newColor !== defaultColor) { + block.dataset.blockColor = newColor; + block.style.borderColor = newColor; + block.style.borderWidth = "2px"; + } else { + delete block.dataset.blockColor; + block.style.borderColor = ""; + block.style.borderWidth = ""; + } + + overlay.classList.remove("active"); + }; +} + +/** Check every algorithm block and flag those without an incoming logic connection. */ +function refreshBlockWarnings() { + for (const block of document.querySelectorAll("#main-program > [data-block]")) { + if (typeof block._checkLogicInput === "function") block._checkLogicInput(); + } + + /* Warn when a wc-proccall has no connection from its error (!) output hook. */ + for (const block of document.querySelectorAll("wc-proccall")) { + const errHook = block.querySelector(".proccall-error-hook"); + if (!errHook) continue; + const hasConnection = block.wcNextFlow.some(p => p.fromHook === errHook); + block.classList.toggle("warn-error-unlinked", !hasConnection); + } + + /* Warn when an error-path END block returns 0 (success in an error branch). */ + for (const block of document.querySelectorAll("wc-end.error-path")) { + block.classList.toggle("warn-zero-in-error", block._selectedCode === 0); + } +} + +/** + * Marks algorithm blocks that are reachable ONLY via error-flow paths. + * Adds/removes the `.error-path` class so they render with a red highlight. + */ +function refreshErrorPaths() { + for (const el of document.querySelectorAll("[data-block].error-path")) { + el.classList.remove("error-path"); + } + for (const p of document.querySelectorAll("path.flow.flow-error")) { + p.classList.remove("flow-error"); + } + + const errorHooks = document.querySelectorAll("path.flow.flow-error-source"); + if (errorHooks.length === 0) return; + + const errorRoots = new Set(); + for (const path of errorHooks) { + if (path.toElement) errorRoots.add(path.toElement); + path.classList.add("flow-error"); + } + + const normalReachable = new Set(); + const start = $id("main-start"); + if (start) { + const q = [start]; + while (q.length) { + const el = q.pop(); + if (normalReachable.has(el)) continue; + normalReachable.add(el); + for (const p of el.wcNextFlow) { + if (p.classList.contains("flow-error-source")) continue; + if (p.toElement) q.push(p.toElement); + } + } + } + + const eq = [...errorRoots]; + while (eq.length) { + const el = eq.pop(); + if (normalReachable.has(el)) continue; + if (el.classList.contains("error-path")) continue; + el.classList.add("error-path"); + for (const p of el.wcNextFlow) { + if (p.toElement) { + p.classList.add("flow-error"); + eq.push(p.toElement); + } + } + } +} + /** Runs all refresh passes in dependency order. */ function refreshAll() { refreshQuantityOptions(); refreshUnitOptions(); refreshDefaultUnitOptions(); refreshVarRefOptions(); - refreshProcOptions(); + refreshBlockWarnings(); + refreshErrorPaths(); } diff --git a/icons/bash.svg b/icons/bash.svg new file mode 100644 index 0000000..857dd04 --- /dev/null +++ b/icons/bash.svg @@ -0,0 +1 @@ +GNU Bash \ No newline at end of file diff --git a/icons/stop-circle.svg b/icons/stop-circle.svg new file mode 100644 index 0000000..ae7f76a --- /dev/null +++ b/icons/stop-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/index.html b/index.html index e9b0ec3..611f112 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ Visual Programming Tool + @@ -18,22 +19,26 @@ - + - END + END + + exit: + + + + + + + success + - - - - - in - out - - + @@ -43,9 +48,11 @@ if bool - yes - no in + + yes + no + @@ -133,6 +140,29 @@ + + + + + + [] + = + + + (unit) + + + + + (no type) + + + + v + + + + @@ -160,6 +190,9 @@ [] 0 elements + + × + arr @@ -215,8 +248,10 @@ idx ] - in - out + + + + @@ -226,8 +261,8 @@ - PROCEDURE - + + proc @@ -235,13 +270,26 @@ + input + → outputs + output + + return: + + by pointer + by value + error int + + + + Description + + @@ -249,10 +297,11 @@ + CALL - - (select procedure) - + + + change @@ -261,8 +310,15 @@ - in - out + + + + add arg + + + + + ! + @@ -293,7 +349,7 @@ - + @@ -304,15 +360,20 @@ - - - + + + + + + + + @@ -324,7 +385,7 @@ - + @@ -335,14 +396,16 @@ scalar string array + vector + Name Value Type/Unit C Name Comment - + @@ -351,15 +414,22 @@ + Quantities — Name L M T I Θ N J Min Max Default Unit - + + + + + + + @@ -368,13 +438,17 @@ - + Generate code Save Load New - + Copy + Tests + + + @@ -383,6 +457,53 @@ + + + + + + Select Procedure + + + + + + + + + + + + New Project + + Project Name + + + + Template + + + ■ + Console Program + Basic C program with stdio + + + △ + SDL3 Hello Triangle + SDL3 + OpenGL colored triangle + + + + + Cancel + Create + + + + + + + + + + + +