376 lines
12 KiB
JavaScript
376 lines
12 KiB
JavaScript
// ── Algorithm-tab block definitions ────────────────────────────────────────
|
|
//
|
|
// Blocks that live on the Algorithm (flowchart) canvas:
|
|
// WCStart, WCEnd, WCOutput, WCDecision, WCAssign, WCExpression, WCVarRef
|
|
//
|
|
// Each extends WCBlock (from wcblock.js) and provides generateOwnCode().
|
|
//
|
|
// Load order dependencies:
|
|
// wcblock.js → WCBlock base class
|
|
// helpers.js → findDataSource, resolveSourceName, escapeString, toCName,
|
|
// commentPrefix, refreshVarRefOptions
|
|
// dom.js → $id
|
|
// error.js → showError
|
|
|
|
|
|
// ── WCStart — program entry point ──────────────────────────────────────────
|
|
|
|
class WCStart extends WCBlock {
|
|
delete() { showError("Cannot delete the start block."); }
|
|
}
|
|
|
|
|
|
// ── WCEnd — program exit ───────────────────────────────────────────────────
|
|
|
|
class WCEnd extends WCBlock {
|
|
delete() { showError("Cannot delete the end block."); }
|
|
generateOwnCode() { return ""; }
|
|
}
|
|
|
|
|
|
// ── WCOutput — printf statement ────────────────────────────────────────────
|
|
|
|
class WCOutput extends WCBlock {
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this._el.textarea = this.querySelector("textarea");
|
|
}
|
|
|
|
saveData() { return { textarea: this._el.textarea.value }; }
|
|
restoreData(d) { this.querySelector("textarea").value = d.textarea || ""; }
|
|
|
|
generateOwnCode() {
|
|
const msg = escapeString(this._el.textarea.value || "");
|
|
return `\nprintf("${msg}");\n`;
|
|
}
|
|
}
|
|
|
|
|
|
// ── WCDecision — conditional branch ────────────────────────────────────────
|
|
|
|
class WCDecision extends WCBlock {
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this._el.boolIn = this.querySelector(".decision-data-in");
|
|
}
|
|
|
|
_getBoolName(block) {
|
|
return resolveSourceName(block) || "flag";
|
|
}
|
|
|
|
_branchDescendants(roots) {
|
|
const d = new Set(), q = [...roots];
|
|
for (const el of q) {
|
|
if (d.has(el)) continue;
|
|
d.add(el);
|
|
if (el instanceof WCDecision) continue;
|
|
for (const p of el.wcNextFlow) {
|
|
if (!d.has(p.toElement)) q.push(p.toElement);
|
|
}
|
|
}
|
|
return d;
|
|
}
|
|
|
|
_emitBranch(roots, stop, visited) {
|
|
const v = new Set(visited);
|
|
let c = "";
|
|
const walk = el => {
|
|
if (v.has(el) || stop.has(el)) return;
|
|
v.add(el);
|
|
if (el instanceof WCDecision) {
|
|
c += el.generateCode(v);
|
|
} else {
|
|
c += el.generateOwnCode();
|
|
for (const p of el.wcNextFlow) walk(p.toElement);
|
|
}
|
|
};
|
|
for (const el of roots) walk(el);
|
|
return c;
|
|
}
|
|
|
|
generateCode(visited = new Set()) {
|
|
const yesTargets = [], noTargets = [];
|
|
for (const path of this.wcNextFlow) {
|
|
const hook = path.fromHook;
|
|
if (!hook || hook.id === "hook-out1") {
|
|
yesTargets.push(path.toElement);
|
|
} else if (hook.id === "hook-out0") {
|
|
noTargets.push(path.toElement);
|
|
}
|
|
}
|
|
|
|
const yesAll = this._branchDescendants(yesTargets);
|
|
const noAll = this._branchDescendants(noTargets);
|
|
const shared = new Set([...yesAll].filter(el => noAll.has(el) && el !== this));
|
|
|
|
const yesCode = this._emitBranch(yesTargets, shared, visited);
|
|
const noCode = this._emitBranch(noTargets, shared, visited);
|
|
const sharedCode = this._emitBranch([...shared], new Set(), visited);
|
|
|
|
for (const el of yesAll) visited.add(el);
|
|
for (const el of noAll) visited.add(el);
|
|
|
|
const source = findDataSource(this._el.boolIn);
|
|
const cond = source ? this._getBoolName(source) : "0";
|
|
let out = "";
|
|
if (yesCode || noCode) {
|
|
if (yesCode) out += `if (${cond}) {\n${yesCode}}`;
|
|
if (noCode) {
|
|
out += yesCode
|
|
? ` else {\n${noCode}}`
|
|
: `if (!(${cond})) {\n${noCode}}`;
|
|
}
|
|
}
|
|
out += sharedCode;
|
|
return out ? `\n${out}\n` : "";
|
|
}
|
|
}
|
|
|
|
|
|
// ── WCAssign — variable assignment block ───────────────────────────────────
|
|
|
|
class WCAssign extends WCBlock {
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this._el.target = this.querySelector(".assign-target");
|
|
this._el.value = this.querySelector(".assign-value");
|
|
this._el.dataIn = this.querySelector(".assign-row .hook");
|
|
this._el.indexArea = this.querySelector(".assign-index-area");
|
|
this._el.indexInput = this.querySelector(".assign-index");
|
|
this._el.indexHook = this.querySelector(".assign-index-area .hook");
|
|
|
|
this._el.target.addEventListener("change", () => this._syncType());
|
|
setTimeout(() => {
|
|
refreshVarRefOptions();
|
|
this._syncType();
|
|
this._updateValueVisibility();
|
|
}, 0);
|
|
}
|
|
|
|
_updateValueVisibility() {
|
|
const hasConn = findDataSource(this._el.dataIn) !== null;
|
|
this._el.value.classList.toggle("hidden", hasConn);
|
|
const hasIdxConn = findDataSource(this._el.indexHook) !== null;
|
|
this._el.indexInput.classList.toggle("hidden", hasIdxConn);
|
|
}
|
|
|
|
_syncType() {
|
|
const id = this._el.target.value;
|
|
const target = id ? $id(id) : null;
|
|
const type = target ? target.dataType : "scalar";
|
|
this._el.dataIn.dataset.type = type;
|
|
this._el.dataIn.title = "expects " + type;
|
|
this._el.indexArea.style.display = type === "array" ? "flex" : "none";
|
|
}
|
|
|
|
_getSourceVarName(block) {
|
|
return resolveSourceName(block) || "x";
|
|
}
|
|
|
|
_genStringLiteral(name, raw) {
|
|
const escaped = escapeString(raw);
|
|
const len = raw.length;
|
|
return "\n" + [
|
|
`free(${name}.data);`,
|
|
`${name}.length = ${len};`,
|
|
`${name}.data = malloc(${len} + 1);`,
|
|
`memcpy(${name}.data, "${escaped}", ${len});`,
|
|
`${name}.data[${len}] = 0;`,
|
|
].join("\n") + "\n";
|
|
}
|
|
|
|
_genStringCopy(dst, src) {
|
|
return "\n" + [
|
|
`free(${dst}.data);`,
|
|
`${dst}.length = ${src}.length;`,
|
|
`${dst}.data = malloc(${src}.length + 1);`,
|
|
`memcpy(${dst}.data, ${src}.data, ${src}.length);`,
|
|
`${dst}.data[${dst}.length] = 0;`,
|
|
].join("\n") + "\n";
|
|
}
|
|
|
|
saveData() { return { target: this._el.target.value, value: this._el.value.value, index: this._el.indexInput.value }; }
|
|
restoreData(d) {
|
|
const tgt = this.querySelector(".assign-target");
|
|
tgt.dataset.restoredValue = d.target || "";
|
|
tgt.value = d.target || "";
|
|
this.querySelector(".assign-value").value = d.value || "";
|
|
this.querySelector(".assign-index").value = d.index || "0";
|
|
this._syncType();
|
|
this._updateValueVisibility();
|
|
}
|
|
|
|
generateOwnCode() {
|
|
const targetId = this._el.target.value;
|
|
if (!targetId) return "";
|
|
const target = $id(targetId);
|
|
if (!target) return "";
|
|
const targetName = resolveSourceName(target) || "x";
|
|
const targetType = target.dataType || "scalar";
|
|
|
|
const source = findDataSource(this._el.dataIn);
|
|
|
|
if (targetType === "array") {
|
|
const idxSource = findDataSource(this._el.indexHook);
|
|
const idxExpr = idxSource ? this._getSourceVarName(idxSource) : (this._el.indexInput.value || "0");
|
|
if (!source) {
|
|
const raw = this._el.value.value || "0";
|
|
return `\n${targetName}[${idxExpr}] = ${raw};\n`;
|
|
}
|
|
const srcName = this._getSourceVarName(source);
|
|
return `\n${targetName}[${idxExpr}] = ${srcName};\n`;
|
|
}
|
|
|
|
if (!source) {
|
|
const raw = this._el.value.value || "0";
|
|
if (targetType === "string") return this._genStringLiteral(targetName, raw);
|
|
return `\n${targetName} = ${raw};\n`;
|
|
}
|
|
|
|
const srcName = this._getSourceVarName(source);
|
|
if (targetType === "string" && source.dataType === "string") {
|
|
return this._genStringCopy(targetName, srcName);
|
|
}
|
|
return `\n${targetName} = ${srcName};\n`;
|
|
}
|
|
}
|
|
|
|
|
|
// ── WCVarRef — read-only variable reference with data output hook ──────────
|
|
|
|
class WCVarRef extends WCBlock {
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this._el.select = this.querySelector(".varref-select");
|
|
this._el.select.addEventListener("change", () => {
|
|
this._syncType();
|
|
});
|
|
}
|
|
|
|
_syncType() {
|
|
const id = this._el.select.value;
|
|
const target = id ? $id(id) : null;
|
|
const hook = this.querySelector(".hook-out");
|
|
hook.dataset.type = target ? target.dataType : "scalar";
|
|
}
|
|
|
|
saveData() { return { selected: this._el.select.value }; }
|
|
restoreData(d) {
|
|
const sel = this.querySelector(".varref-select");
|
|
sel.dataset.restoredValue = d.selected || "";
|
|
sel.value = d.selected || "";
|
|
const id = sel.value;
|
|
const target = id ? $id(id) : null;
|
|
const hook = this.querySelector(".hook-out");
|
|
hook.dataset.type = target ? target.dataType : "scalar";
|
|
}
|
|
|
|
generateOwnCode() { return ""; }
|
|
}
|
|
|
|
|
|
// ── WCExpression — mathematical expression with MathJax rendering & data hooks ──
|
|
|
|
class WCExpression extends WCBlock {
|
|
get dataType() {
|
|
const el = this._el?.latex || this.querySelector(".expr-latex");
|
|
const latex = el?.value || "";
|
|
return /[<>]|==|!=|\\(?:ge|le|ne|neq|geq|leq|land|lor|wedge|vee|neg|lnot)\b/.test(latex) ? "boolean" : "scalar";
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this._el.name = this.querySelector(".expr-name");
|
|
this._el.latex = this.querySelector(".expr-latex");
|
|
this._el.rendered = this.querySelector(".expr-rendered");
|
|
this._el.varhooks = this.querySelector(".expr-varhooks");
|
|
|
|
this._el.latex.addEventListener("input", () => this._renderFormula());
|
|
setTimeout(() => this._renderFormula(), 0);
|
|
}
|
|
|
|
saveData() { return { name: this._el.name.value, latex: this._el.latex.value }; }
|
|
restoreData(d) {
|
|
this._el.name.value = d.name || "";
|
|
this._el.latex.value = d.latex || "";
|
|
this._renderFormula();
|
|
}
|
|
|
|
_parseVariables(latex) {
|
|
const tokens = latex
|
|
.replace(/\\[a-zA-Z]+/g, "")
|
|
.replace(/[{}\[\]()]/g, " ")
|
|
.replace(/[\d+*\/^_=,;:!?<>|~`@#\$%&'-]/g, " ")
|
|
.split(/\s+/)
|
|
.filter(t => t.length > 0 && t.length <= 8)
|
|
.filter(t => ![
|
|
"sin","cos","tan","cot","sec","csc","arcsin","arccos","arctan",
|
|
"log","ln","lg","exp","sqrt","lim","inf","sum","int","prod","det",
|
|
"gcd","lcm","max","min","arg","mod","Re","Im","deg","Pr","var",
|
|
"alpha","beta","gamma","delta","epsilon","zeta","eta","theta",
|
|
"iota","kappa","lambda","mu","nu","xi","omicron","rho","sigma",
|
|
"tau","upsilon","phi","chi","psi","omega","varepsilon","vartheta",
|
|
"varpi","varphi","varrho","varsigma","varUpsilon"
|
|
].includes(t.toLowerCase()));
|
|
return [...new Set(tokens)];
|
|
}
|
|
|
|
_rebuildVarHooks(vars) {
|
|
this._el.varhooks.innerHTML = "";
|
|
for (const v of vars) {
|
|
const hook = document.createElement("button");
|
|
hook.className = "hook";
|
|
hook.id = this.id + "-vh-" + v;
|
|
hook.title = v;
|
|
hook.textContent = v;
|
|
hook.dataset.direction = "in";
|
|
hook.dataset.linkType = "data";
|
|
hook.dataset.type = "scalar";
|
|
hook.dataset.exit = "left";
|
|
this._setupHook(hook);
|
|
this._el.varhooks.appendChild(hook);
|
|
}
|
|
}
|
|
|
|
_renderFormula() {
|
|
const latex = this._el.latex.value;
|
|
const outHook = this.querySelector(".hook-out");
|
|
if (outHook) outHook.dataset.type = this.dataType;
|
|
this._el.rendered.innerHTML = "";
|
|
const vars = latex.trim() ? this._parseVariables(latex) : [];
|
|
this._rebuildVarHooks(vars);
|
|
if (!latex.trim()) return;
|
|
if (window.MathJax && MathJax.typesetPromise) {
|
|
const wrapper = document.createElement("div");
|
|
wrapper.textContent = "$$" + latex + "$$";
|
|
const typeset = () => {
|
|
MathJax.typesetPromise([wrapper]).then(() => {
|
|
this._el.rendered.innerHTML = wrapper.innerHTML;
|
|
}).catch(() => {
|
|
this._el.rendered.textContent = "\u26A0 error";
|
|
});
|
|
};
|
|
if (MathJax.startup && MathJax.startup.promise && MathJax.startup.promise.then) {
|
|
MathJax.startup.promise.then(typeset);
|
|
} else {
|
|
typeset();
|
|
}
|
|
} else {
|
|
this._el.rendered.textContent = latex;
|
|
}
|
|
}
|
|
|
|
generateCode(visited = new Set()) {
|
|
if (visited.has(this)) return "";
|
|
visited.add(this);
|
|
return this.generateOwnCode();
|
|
}
|
|
|
|
generateOwnCode() {
|
|
const name = this._el.name.value || "res";
|
|
const formula = this._el.latex.value || "0";
|
|
const type = this.dataType === "boolean" ? "int" : "double";
|
|
return `${type} ${name} = ${formula};`;
|
|
}
|
|
}
|