Files
avcf/app.js
T

452 lines
21 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 COLS = 4, COL_W = 250, ROW_H = 190, PAD = 10;
const typeRows = Math.ceil(typeDefs.length / COLS);
const typeTags = typeDefs.map((t, i) => {
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];
}
el.dataset.defaultUnit = t.unit;
return el;
});
const unitTags = unitDefs.map((u, i) => {
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;
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 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);
const pane = document.createElement("div");
pane.className = "lib-pane" + (makeActive ? " active" : "");
pane.dataset.libName = libName;
const area = document.createElement("div");
area.className = "procedures-area";
pane.appendChild(area);
$id("pane-procedures").appendChild(pane);
return area;
}
// ── Default library creation ──────────────────────────────────────────────────
/**
* Creates the default "stdio" library with a "puts" procedure
* (one string input param, no outputs).
*/
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";
}
}
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"]
});
}
}
// ── 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.id === "pane-" + name);
}
}
// ── Event listeners ───────────────────────────────────────────────────────────
/** 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");
}
/* Toolbar block-creation helpers */
const ACTION_REFRESH = {
varref: refreshAll,
boolean: refreshAll,
scalar: refreshAll,
string: refreshAll,
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");
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;
});
/* 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();
}
});
/* 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);
});
/* Procedures pane toolbar */
const procToolbar = $id("pane-procedures").querySelector(".pane-toolbar");
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;
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();
}
});
/* 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);
});
/* 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();
});
/* Tab bar */
document.querySelector(".tab-bar").addEventListener("click", e => {
const btn = e.target.closest(".tab-btn");
if (btn) switchTab(btn.dataset.tab);
closeVariableSubmenu();
});
/* Close variable submenu on outside click */
document.addEventListener("click", e => {
if (!e.target.closest(".btn-group")) closeVariableSubmenu();
});
/* Click anywhere in draw-area (except hooks, which stopPropagation) cancels pending link */
$id("draw-area").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);
});
/* Error modal dismiss */
$("#error-overlay").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;
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() {
$id("main-program").build();
createDefaultMeasurements();
createDefaultLibraries();
createDefaultOpenGLProcedures();
if (localStorage.getItem(STORAGE_KEY)) {
loadWorkspace();
}
setupUI();
}
main();