Files
avcf/templates.js
adrien 68da5592ef refactor: unwrap nested if-else in error-path code generation
- Replace if/else wrapping in WCProcCall.generateOwnCode with branch-aware
  generation via _branchDescendants, _emitBranch, and _generateCallWithError
- Shared blocks (reachable from both error and success paths) now appear
  only once after the if-block, avoiding deeply nested code
- Add 8 browser tests covering merge, decision passthrough, value style,
  sequential calls, and shared-only error paths
- Increase template block spacing (16px -> 40px) for readability
- Add error WCEnd blocks to console and SDL3 templates with error-hook
  connections to all failure-mode proc calls
- Hide editable WCProcCall input fields behind field-display spans,
  matching existing algorithm-block convention
2026-06-23 00:51:54 +02:00

474 lines
20 KiB
JavaScript

// ── New Project modal & templates ──────────────────────────────────────────
//
// Each template is a function that receives the project name and builds the
// initial workspace (blocks, variables, connections, libraries) in place.
//
// Load order: loaded after persist.js, before app.js
// Depends on: dom.js ($id), helpers.js, draw.js, wcblock.js, blocks-*.js,
// wcprogram.js, persist.js, lib-loader.js
/**
* Removes all user-created blocks, connections, library tabs and panes,
* resetting flow arrays and LOADED_LIBS. Used by templates (new project) and
* persist.js (load workspace, with keepStorage:true to preserve localStorage).
* @param {{ keepStorage?: boolean }} [opts]
*/
function _clearWorkspace(opts = {}) {
const { keepStorage } = opts;
const conn = $id("connections");
if (conn) {
for (const p of conn.querySelectorAll("path.flow, path.flow-data")) p.remove();
}
for (const block of document.querySelectorAll("[data-block]")) {
if (block.wcForeFlow) block.wcForeFlow = [];
if (block.wcNextFlow) block.wcNextFlow = [];
}
const prog = $id("main-program");
if (prog) {
for (const block of [...prog.children]) {
if (block.id !== "main-start" && block.id !== "main-end") block.remove();
}
}
const dataArea = $id("data-area");
if (dataArea) {
for (const block of [...dataArea.querySelectorAll("[data-block]")]) block.remove();
}
const varArea = $id("variable-area");
if (varArea) {
for (const block of [...varArea.querySelectorAll("[data-block]")]) block.remove();
}
const libTabBar = document.querySelector(".lib-tab-bar");
if (libTabBar) libTabBar.innerHTML = "";
for (const pane of document.querySelectorAll(".lib-pane")) pane.remove();
for (const tab of document.querySelectorAll(".lib-tab")) tab.remove();
LOADED_LIBS.clear();
if (!keepStorage) localStorage.removeItem(STORAGE_KEY);
}
function _pos(el, left, top) {
el.style.left = left + "px";
el.style.top = top + "px";
}
const ALG_GAP = 40;
function _placeBelow(prev, block, gap = ALG_GAP) {
block.style.top = (prev.offsetTop + prev.offsetHeight + gap) + "px";
}
function _createAssign(prog, targetId, value) {
const el = prog.newNode("wc-assign");
if (!el) return null;
el.style.left = "50px";
el._el.target.value = targetId;
el._el.value.value = String(value);
el._syncType();
el._updateValueVisibility();
return el;
}
function _createVarRef(prog, varId) {
const el = prog.newVarRefNode();
if (!el) return null;
el.style.left = "50px";
el._el.select.value = varId;
const target = varId ? $id(varId) : null;
const hook = el.querySelector(".hook-out");
if (hook) hook.dataset.type = target ? (target.dataType === "vector" ? "scalar" : target.dataType) : "scalar";
return el;
}
function _createProcCall(prog, qname, resultName) {
const el = prog.newProcCallNode();
if (!el) return null;
el.style.left = "50px";
if (el._el.nameInput) el._el.nameInput.value = resultName || "";
el.setProcedure(qname);
return el;
}
function _createScalarVar(prog, name) {
const el = prog.newScalarNode();
if (!el) return null;
el.style.left = "700px";
const nameEl = el.querySelector(".var-name");
if (nameEl) nameEl.value = name;
return el;
}
function _createBooleanVar(prog, name, value) {
const el = prog.newBooleanNode();
if (!el) return null;
el.style.left = "700px";
const nameEl = el.querySelector(".var-name");
if (nameEl) nameEl.value = name;
const valEl = el.querySelector(".var-value");
if (valEl) valEl.value = value || "false";
return el;
}
async function _loadLib(libId) {
const index = await loadLibraryIndex();
const entry = index.find(e => e.id === libId);
if (entry) await loadLibrary(libId);
}
const TEMPLATES = {};
TEMPLATES.console = {
name: "Console Program",
icon: "\u25A0",
desc: "Basic C program with stdio",
async create(prog, projectName) {
_clearWorkspace();
await _loadLib("stdio");
refreshAll();
const start = $id("main-start");
const end = $id("main-end");
_pos(start, 50, 50);
const printfCall = _createProcCall(prog, "stdio/printf", "");
_placeBelow(start, printfCall);
if (printfCall) {
const arg = printfCall.querySelector(".proccall-inputs .proccall-arg-input");
if (arg) arg.value = '"Hello, World!\\n"';
}
const errEnd = prog.newNode("wc-end");
_pos(errEnd, 360, start.offsetTop);
errEnd._selectByCode(1);
errEnd._enforceZeroEnd();
_pos(end, 50, printfCall.offsetTop + printfCall.offsetHeight + ALG_GAP);
if (start && end && printfCall && errEnd) {
_linkFlow(start, ".hook", printfCall, ".hook-in");
_linkFlow(printfCall, ".hook-out", end, ".hook");
_linkFlow(printfCall, ".proccall-error-hook", errEnd, ".hook");
}
}
};
TEMPLATES["sdl3-triangle"] = {
name: "SDL3 Hello Triangle",
icon: "\u25B3",
desc: "SDL3 + OpenGL colored triangle",
async create(prog, projectName) {
_clearWorkspace();
await _loadLib("sdl3");
await _loadLib("gl");
refreshAll();
const start = $id("main-start");
const end = $id("main-end");
_pos(start, 50, 50);
/* Error exit — right of the init chain, collects all failure paths */
const errEnd = prog.newNode("wc-end");
_pos(errEnd, 420, start.offsetTop);
errEnd._selectByCode(1);
errEnd._enforceZeroEnd();
/* Variables column (x=700) */
let varTop = start;
function varBelow(block) { _placeBelow(varTop, block, 20); varTop = block; }
const varRunning = _createBooleanVar(prog, "running", "true"); varBelow(varRunning);
const varWindow = _createScalarVar(prog, "window"); varBelow(varWindow);
const varRenderer = _createScalarVar(prog, "renderer"); varBelow(varRenderer);
const varVS = _createScalarVar(prog, "vs"); varBelow(varVS);
const varFS = _createScalarVar(prog, "fs"); varBelow(varFS);
const varProgram = _createScalarVar(prog, "program"); varBelow(varProgram);
const varVAO = _createScalarVar(prog, "vao"); varBelow(varVAO);
/* ── Initialisation chain (x=50) — each block placed below the previous ── */
let prev = start;
function algBelow(block) { _placeBelow(prev, block); prev = block; }
const initCall = _createProcCall(prog, "sdl3/SDL_Init", "ret"); algBelow(initCall);
if (initCall) {
const inputs = initCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "SDL_INIT_VIDEO";
}
const setMetaCall = _createProcCall(prog, "sdl3/SDL_SetAppMetadata", ""); algBelow(setMetaCall);
if (setMetaCall) {
const inputs = setMetaCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = '"' + projectName + '"';
if (inputs[1]) inputs[1].value = '"1.0"';
if (inputs[2]) inputs[2].value = '"com.example.triangle"';
}
const winCall = _createProcCall(prog, "sdl3/SDL_CreateWindow", "window"); algBelow(winCall);
if (winCall) {
const inputs = winCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = '"Hello Triangle"';
if (inputs[1]) inputs[1].value = "640";
if (inputs[2]) inputs[2].value = "480";
if (inputs[3]) inputs[3].value = "0";
}
const renCall = _createProcCall(prog, "sdl3/SDL_CreateRenderer", "renderer"); algBelow(renCall);
if (renCall) {
const inputs = renCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "window";
if (inputs[1]) inputs[1].value = "NULL";
}
const vsAssign = _createAssign(prog, varVS ? varVS.id : "", '"#version 330\\nlayout(location=0) in vec3 pos; void main(){gl_Position=vec4(pos,1);}"'); algBelow(vsAssign);
const fsAssign = _createAssign(prog, varFS ? varFS.id : "", '"#version 330\\nout vec4 c; void main(){c=vec4(1,0,0,1);}"'); algBelow(fsAssign);
const vsCreateCall = _createProcCall(prog, "gl/glCreateShader", "vs"); algBelow(vsCreateCall);
if (vsCreateCall) {
const inputs = vsCreateCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "GL_VERTEX_SHADER";
}
const vsSrcCall = _createProcCall(prog, "gl/glShaderSource", ""); algBelow(vsSrcCall);
if (vsSrcCall) {
const inputs = vsSrcCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "vs";
if (inputs[1]) inputs[1].value = "1";
if (inputs[2]) inputs[2].value = "vs";
}
const vsCompCall = _createProcCall(prog, "gl/glCompileShader", ""); algBelow(vsCompCall);
if (vsCompCall) {
const inputs = vsCompCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "vs";
}
const fsCreateCall = _createProcCall(prog, "gl/glCreateShader", "fs"); algBelow(fsCreateCall);
if (fsCreateCall) {
const inputs = fsCreateCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "GL_FRAGMENT_SHADER";
}
const fsSrcCall = _createProcCall(prog, "gl/glShaderSource", ""); algBelow(fsSrcCall);
if (fsSrcCall) {
const inputs = fsSrcCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "fs";
if (inputs[1]) inputs[1].value = "1";
if (inputs[2]) inputs[2].value = "fs";
}
const fsCompCall = _createProcCall(prog, "gl/glCompileShader", ""); algBelow(fsCompCall);
if (fsCompCall) {
const inputs = fsCompCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "fs";
}
const progCreate = _createProcCall(prog, "gl/glCreateProgram", "program"); algBelow(progCreate);
const progAttach1 = _createProcCall(prog, "gl/glAttachShader", ""); algBelow(progAttach1);
if (progAttach1) {
const inputs = progAttach1.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "program";
if (inputs[1]) inputs[1].value = "vs";
}
const progAttach2 = _createProcCall(prog, "gl/glAttachShader", ""); algBelow(progAttach2);
if (progAttach2) {
const inputs = progAttach2.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "program";
if (inputs[1]) inputs[1].value = "fs";
}
const progLink = _createProcCall(prog, "gl/glLinkProgram", ""); algBelow(progLink);
if (progLink) {
const inputs = progLink.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "program";
}
const vaoGen = _createProcCall(prog, "gl/glGenVertexArrays", "vao"); algBelow(vaoGen);
if (vaoGen) {
const inputs = vaoGen.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "1";
}
const vaoBind = _createProcCall(prog, "gl/glBindVertexArray", ""); algBelow(vaoBind);
if (vaoBind) {
const inputs = vaoBind.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "vao";
}
const bufGen = _createProcCall(prog, "gl/glGenBuffers", ""); algBelow(bufGen);
if (bufGen) {
const inputs = bufGen.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "1";
}
const bufBind = _createProcCall(prog, "gl/glBindBuffer", ""); algBelow(bufBind);
if (bufBind) {
const inputs = bufBind.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "GL_ARRAY_BUFFER";
if (inputs[1]) inputs[1].value = "vbo";
}
/* ── Main loop ── */
const loopDec = prog.newDecisionNode(); loopDec.style.left = "50px"; algBelow(loopDec);
const pollCall = _createProcCall(prog, "sdl3/SDL_PollEvent", "event"); algBelow(pollCall);
if (pollCall) {
const inputs = pollCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "event";
}
const checkEvent = prog.newDecisionNode(); checkEvent.style.left = "50px"; algBelow(checkEvent);
const stopRunning = _createAssign(prog, varRunning ? varRunning.id : "", "false"); algBelow(stopRunning);
const clearCall = _createProcCall(prog, "gl/glClear", ""); algBelow(clearCall);
if (clearCall) {
const inputs = clearCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "GL_COLOR_BUFFER_BIT";
}
const useProgCall = _createProcCall(prog, "gl/glUseProgram", ""); algBelow(useProgCall);
if (useProgCall) {
const inputs = useProgCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "program";
}
const drawCall = _createProcCall(prog, "gl/glDrawArrays", ""); algBelow(drawCall);
if (drawCall) {
const inputs = drawCall.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "GL_TRIANGLES";
if (inputs[1]) inputs[1].value = "0";
if (inputs[2]) inputs[2].value = "3";
}
const renClear = _createProcCall(prog, "sdl3/SDL_RenderClear", ""); algBelow(renClear);
if (renClear) {
const inputs = renClear.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "renderer";
}
const renPresent = _createProcCall(prog, "sdl3/SDL_RenderPresent", ""); algBelow(renPresent);
if (renPresent) {
const inputs = renPresent.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "renderer";
}
/* ── Cleanup (after loop body) ── */
const renDestroy = _createProcCall(prog, "sdl3/SDL_DestroyRenderer", ""); algBelow(renDestroy);
if (renDestroy) {
const inputs = renDestroy.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "renderer";
}
const winDestroy = _createProcCall(prog, "sdl3/SDL_DestroyWindow", ""); algBelow(winDestroy);
if (winDestroy) {
const inputs = winDestroy.querySelectorAll(".proccall-inputs .proccall-arg-input");
if (inputs[0]) inputs[0].value = "window";
}
const quitCall = _createProcCall(prog, "sdl3/SDL_Quit", ""); algBelow(quitCall);
_pos(end, 50, prev.offsetTop + prev.offsetHeight + ALG_GAP);
/* ── Wire up flow connections ── */
_linkFlow(start, ".hook", initCall, ".hook-in");
if (initCall) _linkFlow(initCall, ".hook-out", setMetaCall, ".hook-in");
if (setMetaCall) _linkFlow(setMetaCall, ".hook-out", winCall, ".hook-in");
if (winCall) _linkFlow(winCall, ".hook-out", renCall, ".hook-in");
if (renCall) _linkFlow(renCall, ".hook-out", vsAssign, "#hook-fore");
if (vsAssign) _linkFlow(vsAssign, "#hook-next", fsAssign, "#hook-fore");
if (fsAssign) _linkFlow(fsAssign, "#hook-next", vsCreateCall, ".hook-in");
if (vsCreateCall) _linkFlow(vsCreateCall, ".hook-out", vsSrcCall, ".hook-in");
if (vsSrcCall) _linkFlow(vsSrcCall, ".hook-out", vsCompCall, ".hook-in");
if (vsCompCall) _linkFlow(vsCompCall, ".hook-out", fsCreateCall, ".hook-in");
if (fsCreateCall) _linkFlow(fsCreateCall, ".hook-out", fsSrcCall, ".hook-in");
if (fsSrcCall) _linkFlow(fsSrcCall, ".hook-out", fsCompCall, ".hook-in");
if (fsCompCall) _linkFlow(fsCompCall, ".hook-out", progCreate, ".hook-in");
if (progCreate) _linkFlow(progCreate, ".hook-out", progAttach1, ".hook-in");
if (progAttach1) _linkFlow(progAttach1, ".hook-out", progAttach2, ".hook-in");
if (progAttach2) _linkFlow(progAttach2, ".hook-out", progLink, ".hook-in");
if (progLink) _linkFlow(progLink, ".hook-out", vaoGen, ".hook-in");
if (vaoGen) _linkFlow(vaoGen, ".hook-out", vaoBind, ".hook-in");
if (vaoBind) _linkFlow(vaoBind, ".hook-out", bufGen, ".hook-in");
if (bufGen) _linkFlow(bufGen, ".hook-out", bufBind, ".hook-in");
if (bufBind) _linkFlow(bufBind, ".hook-out", loopDec, "#hook-fore");
if (loopDec) _linkFlow(loopDec, "#hook-out1", pollCall, ".hook-in");
if (pollCall) _linkFlow(pollCall, ".hook-out", checkEvent, "#hook-fore");
if (checkEvent) _linkFlow(checkEvent, "#hook-out1", stopRunning, ".hook-in");
if (stopRunning) _linkFlow(stopRunning, "#hook-next", clearCall, ".hook-in");
if (checkEvent) _linkFlow(checkEvent, "#hook-out0", clearCall, ".hook-in");
if (clearCall) _linkFlow(clearCall, ".hook-out", useProgCall, ".hook-in");
if (useProgCall) _linkFlow(useProgCall, ".hook-out", drawCall, ".hook-in");
if (drawCall) _linkFlow(drawCall, ".hook-out", renClear, ".hook-in");
if (renClear) _linkFlow(renClear, ".hook-out", renPresent, ".hook-in");
if (loopDec) _linkFlow(loopDec, "#hook-out0", renDestroy, ".hook-in");
if (renDestroy) _linkFlow(renDestroy, ".hook-out", winDestroy, ".hook-in");
if (winDestroy) _linkFlow(winDestroy, ".hook-out", quitCall, ".hook-in");
if (quitCall) _linkFlow(quitCall, ".hook-out", end, ".hook");
/* ── Error paths — connect all proc calls with error hooks to the error END ── */
for (const block of [initCall, setMetaCall, winCall, renCall, renClear, renPresent]) {
if (block) _linkFlow(block, ".proccall-error-hook", errEnd, ".hook");
}
}
};
function showNewProjectModal() {
const overlay = $id("new-project-overlay");
if (!overlay) return;
const nameInput = $id("project-name");
if (nameInput) nameInput.value = "Untitled";
/* Reset to default template */
_selectedTemplateId = "console";
for (const c of document.querySelectorAll(".template-card")) c.classList.remove("selected");
const def = document.querySelector('.template-card[data-template="console"]');
if (def) def.classList.add("selected");
overlay.style.display = "";
}
function hideNewProjectModal() {
const overlay = $id("new-project-overlay");
if (overlay) overlay.style.display = "none";
}
/* Expose for app.js */
window.TEMPLATES = TEMPLATES;
window.showNewProjectModal = showNewProjectModal;
window.hideNewProjectModal = hideNewProjectModal;
/* Wire up modal interactivity on DOMContentLoaded */
let _selectedTemplateId = "console";
document.addEventListener("DOMContentLoaded", () => {
const overlay = $id("new-project-overlay");
if (!overlay) return;
const list = overlay.querySelector("#template-list");
/* Template card selection via event delegation */
if (list) {
list.addEventListener("click", e => {
const card = e.target.closest(".template-card");
if (!card) return;
for (const c of list.children) c.classList.remove("selected");
card.classList.add("selected");
_selectedTemplateId = card.dataset.template;
});
}
/* Cancel button */
const cancelBtn = overlay.querySelector(".modal-cancel");
if (cancelBtn) cancelBtn.addEventListener("click", hideNewProjectModal);
/* Create button */
const createBtn = overlay.querySelector(".modal-create");
if (createBtn) {
createBtn.addEventListener("click", async () => {
const nameInput = $id("project-name");
const name = nameInput ? nameInput.value.trim() || "Untitled" : "Untitled";
const template = TEMPLATES[_selectedTemplateId];
if (!template) return;
hideNewProjectModal();
const prog = $id("main-program");
if (!prog) return;
try {
await template.create(prog, name);
} catch (e) {
showError("Template creation failed: " + (e.message || e));
return;
}
refreshAll();
});
}
/* Close on overlay click (background) */
overlay.addEventListener("click", e => {
if (e.target === overlay) hideNewProjectModal();
});
});