- WCBlock.generateCode checks _isFlowBlock (set from #draw-area presence) before emitting labels/gotos; variable blocks in #variable-area skip this - Remove unconditional vertices array creation in main() — it was only relevant for the SDL3 triangle template, which should create it itself if needed
443 lines
17 KiB
JavaScript
443 lines
17 KiB
JavaScript
// ── 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 =>
|
|
`<button data-lib-id="${e.id}">${e.name} (${e.id})</button>`
|
|
).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();
|
|
|
|
/* Load library index and create tabs (procedures load on demand) */
|
|
initLibraries().then(() => {
|
|
if (localStorage.getItem(STORAGE_KEY)) {
|
|
loadWorkspace();
|
|
}
|
|
setupUI();
|
|
});
|
|
}
|
|
|
|
main();
|