// ── Application entry point & orchestration ─────────────────────────────────── // // Wires all modules together: builds the initial workspace, creates default // SI measurements, sets up event listeners, and exposes tab switching. // // Depends on: dom.js ($, $id), helpers.js (refresh*), wcblock.js, blocks-algo.js, // blocks-data.js, blocks-proc.js, wcprogram.js, persist.js // // This is loaded last and calls main() at module top level (which runs after // all deferred scripts have executed, i.e. after full HTML parsing). // /** * 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 createDefaultMeasurements() { const prog = $id("main-program"); const typeDefs = [ { name: "length", dims: [1,0,0,0,0,0,0], unit: "m" }, { name: "mass", dims: [0,1,0,0,0,0,0], unit: "kg" }, { name: "time", dims: [0,0,1,0,0,0,0], unit: "s" }, { name: "current", dims: [0,0,0,1,0,0,0], unit: "A" }, { name: "temperature", dims: [0,0,0,0,1,0,0], unit: "K" }, { name: "amount", dims: [0,0,0,0,0,1,0], unit: "mol" }, { name: "luminous_intensity", dims: [0,0,0,0,0,0,1], unit: "cd" }, { name: "angle", dims: [0,0,0,0,0,0,0], unit: "rad" }, { name: "frequency", dims: [0,0,-1,0,0,0,0], unit: "Hz" }, { name: "force", dims: [1,1,-2,0,0,0,0], unit: "N" }, { name: "energy", dims: [2,1,-2,0,0,0,0], unit: "J" }, { name: "power", dims: [2,1,-3,0,0,0,0], unit: "W" }, { name: "pressure", dims: [-1,1,-2,0,0,0,0], unit: "Pa" }, { name: "charge", dims: [0,0,1,1,0,0,0], unit: "C" }, { name: "voltage", dims: [2,1,-3,-1,0,0,0], unit: "V" }, { name: "capacitance", dims: [-2,-1,4,2,0,0,0], unit: "F" }, { name: "resistance", dims: [2,1,-3,-2,0,0,0], unit: "\u03A9" }, { name: "conductance", dims: [-2,-1,3,2,0,0,0], unit: "S" }, { name: "magnetic_flux", dims: [2,1,-2,-1,0,0,0], unit: "Wb" }, { name: "magnetic_field", dims: [0,1,-2,-1,0,0,0], unit: "T" }, { name: "inductance", dims: [2,1,-2,-2,0,0,0], unit: "H" }, { name: "catalytic_activity", dims: [0,0,-1,0,0,1,0], unit: "kat" }, ]; const unitDefs = [ { fullname: "meter", type: "length", suffix: "m", si: "1" }, { fullname: "kilogram", type: "mass", suffix: "kg", si: "1" }, { fullname: "second", type: "time", suffix: "s", si: "1" }, { fullname: "ampere", type: "current", suffix: "A", si: "1" }, { fullname: "kelvin", type: "temperature", suffix: "K", si: "1" }, { fullname: "mole", type: "amount", suffix: "mol", si: "1" }, { fullname: "candela", type: "luminous_intensity", suffix: "cd", si: "1" }, { fullname: "radian", type: "angle", suffix: "rad", si: "1" }, { fullname: "hertz", type: "frequency", suffix: "Hz", si: "1" }, { fullname: "newton", type: "force", suffix: "N", si: "1" }, { fullname: "joule", type: "energy", suffix: "J", si: "1" }, { fullname: "watt", type: "power", suffix: "W", si: "1" }, { fullname: "pascal", type: "pressure", suffix: "Pa", si: "1" }, { fullname: "coulomb", type: "charge", suffix: "C", si: "1" }, { fullname: "volt", type: "voltage", suffix: "V", si: "1" }, { fullname: "farad", type: "capacitance", suffix: "F", si: "1" }, { fullname: "ohm", type: "resistance", suffix: "\u03A9", si: "1" }, { fullname: "siemens", type: "conductance", suffix: "S", si: "1" }, { fullname: "weber", type: "magnetic_flux", suffix: "Wb", si: "1" }, { fullname: "tesla", type: "magnetic_field", suffix: "T", si: "1" }, { fullname: "henry", type: "inductance", suffix: "H", si: "1" }, { fullname: "katal", type: "catalytic_activity", suffix: "kat", si: "1" }, ]; const typeTags = typeDefs.map(t => { const el = prog.newQuantityNode(); el.querySelector(".quantity-name").value = t.name; for (let j = 0; j < 7; j++) { el.querySelector(`.dim-${j}`).value = t.dims[j]; } el.dataset.defaultUnit = t.unit; return el; }); /* 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.querySelector(".unit-fullname").value = u.fullname; el.querySelector(".unit-type").value = u.type; el.querySelector(".unit-suffix").value = u.suffix; el.querySelector(".unit-si").value = u.si; el.updateSiDisplay(); return el; }); for (const el of typeTags) { const sel = el.querySelector(".quantity-default-unit"); if ([...sel.options].some(o => o.value === el.dataset.defaultUnit)) { sel.value = el.dataset.defaultUnit; } } refreshAll(); } // ── Library tab creation helper ────────────────────────────────────────────── /** * Creates a library tab and its associated pane in the Procedures tab. * @param {string} libName - library identifier * @param {boolean} [makeActive] - whether to mark the tab/pane as active * @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)); 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); paneContainer.appendChild(pane); return area; } /** * Creates library tabs from the index, then lazy-loads procedures on first click. */ async function initLibraries() { const index = await loadLibraryIndex(); let first = true; for (const entry of index) { addLibTab(entry.id, first); first = false; } if (index.length > 0) { switchLibTab(index[0].id); loadLibrary(index[0].id); } } // ── Tab switching ───────────────────────────────────────────────────────────── /** * Activates the clicked tab and shows its corresponding pane. * @param {string} name - tab identifier (matches data-tab attribute and pane id suffix) */ function switchTab(name) { for (const btn of document.querySelectorAll(".tab-btn")) { btn.classList.toggle("active", btn.dataset.tab === name); } for (const pane of document.querySelectorAll(".tab-pane")) { pane.classList.toggle("active", pane.dataset.tab === name); } } // ── Event listeners ─────────────────────────────────────────────────────────── /** Wires all static button click handlers (replaces inline onclick attributes). */ function setupUI() { function closeAllSubmenus() { for (const sub of document.querySelectorAll(".submenu.open")) { sub.classList.remove("open"); } } /* Toolbar block-creation helpers */ const ACTION_REFRESH = { varref: refreshAll, boolean: refreshAll, scalar: refreshAll, string: refreshAll, array: refreshAll, quantity: refreshAll, unit: 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 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 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 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 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 => `` ).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 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) — 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"); 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 */ 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 all submenus on outside click */ document.addEventListener("click", e => { if (!e.target.closest(".btn-group")) closeAllSubmenus(); }); /* Click anywhere in draw-area (except hooks, which stopPropagation) cancels pending link */ const drawArea = $id("draw-area"); if (drawArea) drawArea.addEventListener("click", resetLinkage); /* Click on an SVG connection path deletes it */ 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 */ 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(); sel.delete(); } }); /* Click outside any block deselects */ document.addEventListener("click", ev => { if (!ev.target.closest("[data-block]")) { for (const el of document.querySelectorAll(".selected")) el.classList.remove("selected"); } }); } // ── Entry point ─────────────────────────────────────────────────────────────── /** * 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. */ function main() { const prog = $id("main-program"); if (!prog) { showError("Missing #main-program element"); return; } window.prog = prog; prog.build(); createDefaultMeasurements(); /* 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"] }); } /* Load library index and create tabs (procedures load on demand) */ initLibraries().then(() => { if (localStorage.getItem(STORAGE_KEY)) { loadWorkspace(); } setupUI(); }); } main();