Files
avcf/app.js
T
adrien d760c272f2 feat: remove code output footer, move actions to top bar, download .ll
- Remove global-toolbar (footer with code display) entirely
- Move Generate, Save, Load, New, Tests buttons to algorithm pane toolbar
- Replace prog.generateCode() with prog.generateAndDownload() — creates
  a Blob from the IR and triggers a program.ll download via a temp <a>
- Remove copyCode function from helpers.js (no longer needed)
- Remove global-toolbar and .code-output CSS rules
- Add .action-btn styling for the text buttons in pane-toolbar
2026-06-23 20:40:42 +02:00

432 lines
16 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 &nbsp; Type &nbsp; Suffix &nbsp; SI Factor &nbsp; 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;
const prog = $id("main-program");
if (btn.dataset.action === "add-section" && typeof enterSectionCreateMode === "function") {
enterSectionCreateMode();
}
if (btn.dataset.action === "generate") prog.generateAndDownload();
if (btn.dataset.action === "save") saveWorkspace();
if (btn.dataset.action === "load") loadWorkspace();
if (btn.dataset.action === "new") newWorkspace();
if (btn.dataset.action === "run-tests") {
if (window.APP_TEST_SUITE) APP_TEST_SUITE.run();
}
});
}
/* 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);
});
}
/* 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();