overhaul: modular architecture, tests, doc generation, warning system, and UX improvements

- Modular block files: split into blocks-algo, blocks-data, blocks-proc
- New modules: sections.js (labelled canvas areas), proc-browser.js (procedure tree modal), lib-loader.js (lazy library loading), templates.js (project templates), errno-data.js (errno table)
- Test infrastructure: browser test suite (28 tests) and Node pure-function tests (75 tests)
- Warning system: unified .block-warn for disconnected inputs, unconnected error hooks, and zero-exit-in-error-path
- Error path highlighting: red flow paths and block borders for error-only subgraphs
- Input hooks: hidden by default, shown during linking; outputs always at bottom
- Block editor modal: pencil icon, colour picker for accent, compact field labels
- Doc generation: standalone node script extracts JSDoc -> docs/API.md (124 declarations)
- Persistence: save/restore sections, errno selection, block colours
- Fixes: _segmentsIntersect bug, WCEnd crash from closest(this), drawpath guard on detached elements
This commit is contained in:
2026-06-22 18:31:23 +02:00
parent 36fc63780a
commit b8e8ef9113
44 changed files with 9064 additions and 596 deletions
+136
View File
@@ -0,0 +1,136 @@
// ── On-demand library loader ────────────────────────────────────────────────
//
// Fetches procedure definitions from JSON files in the libraries/ directory.
// At startup only the index is loaded; individual libraries are fetched when
// the user clicks their tab for the first time.
//
// Depends on: app.js (addLibTab, deriveNaturalName)
// blocks-proc.js (WCProcedure)
// wcprogram.js (BLOCK_REGISTRY)
/** @type {Set<string>} Tracks which libraries have been loaded (prevents re-loading). */
const LOADED_LIBS = new Set();
/** @type {Array<{id:string, name:string, group?:string}>} */
let LIB_INDEX = [];
/**
* 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 {Array<[string,string]>} inputs Array of [name, type] pairs
* @param {Array<[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;
}
}
/**
* Fetches libraries/index.json and returns the library metadata array.
* Called once at startup.
* @returns {Promise<Array<{id:string, name:string, group?:string}>>}
*/
async function loadLibraryIndex() {
try {
const res = await fetch("libraries/index.json");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
LIB_INDEX = await res.json();
} catch (e) {
showError("Could not load library index: " + e.message);
LIB_INDEX = [];
}
return LIB_INDEX;
}
/**
* Loads a single library by its id (e.g. "stdio", "sdl3").
* Creates the tab if it doesn't exist yet, then populates all procedures.
* Idempotent — subsequent calls for the same lib are no-ops.
*/
async function loadLibrary(libId) {
if (LOADED_LIBS.has(libId)) return;
LOADED_LIBS.add(libId);
const prog = $id("main-program");
if (!prog) return;
let data;
try {
const res = await fetch(`libraries/${libId}.json`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data = await res.json();
} catch (e) {
showError(`Could not load library "${libId}": ${e.message}`);
return;
}
const procs = data.procedures;
if (!procs || !procs.length) return;
/* Mark this lib as the active tab if it's the first one loaded */
const isFirst = LOADED_LIBS.size === 1;
/* Ensure the tab exists — but don't duplicate if already created from index */
if (!document.querySelector(`.lib-tab[data-lib-name="${libId}"]`)) {
addLibTab(libId, isFirst);
}
for (const pd of procs) {
const proc = prog.newProcedureNode(libId);
if (!proc) continue;
proc.querySelector(".proc-tech-name").textContent = pd.name;
proc.querySelector(".proc-natural-name").value = pd.naturalName || deriveNaturalName(pd.name);
if (pd.desc) {
const db = proc.querySelector(".proc-desc-body");
if (db) db.textContent = pd.desc;
}
proc.dataset.readonly = "true";
if (pd.returnStyle === "value") proc.dataset.returnStyle = "value";
if (pd.returnStyle === "error") proc.dataset.returnStyle = "error";
if (pd.formatString) proc.dataset.formatString = "true";
_addProcParams(proc, pd.inputs || [], pd.outputs || []);
}
}
/**
* Returns the loaded library index (synchronous after loadLibraryIndex).
* @returns {Array<{id:string, name:string, group?:string}>}
*/
function getLibraryIndex() {
return LIB_INDEX;
}
/**
* Checks if a library has been loaded yet.
* @param {string} libId
* @returns {boolean}
*/
function isLibraryLoaded(libId) {
return LOADED_LIBS.has(libId);
}
/**
* Marks a library as loaded (for user-created libs that don't have JSON files).
* @param {string} libId
*/
function markLibLoaded(libId) {
LOADED_LIBS.add(libId);
}