452 lines
21 KiB
JavaScript
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();
|