Files
avcf/wcblock.js
T

200 lines
6.5 KiB
JavaScript

// ── WCBlock — draggable/linkable block base class ──────────────────────────
//
// Base class for all draggable, linkable flowchart and measurement blocks.
// Provides grid-snapped drag-and-drop and a two-click link system between .hook buttons.
// Each block tracks incoming (wcForeFlow) and outgoing (wcNextFlow) SVG path references.
//
// Subclasses override generateCode() / generateOwnCode() to emit C code fragments.
//
// Load order dependencies:
// dom.js → svgNamespace, $id
// error.js → showError
// draw.js → linkageFirst, resetLinkage, startShadowLink, drawpath, deleteLink
//
// This file is loaded first among the block files.
// → blocks-algo.js, blocks-data.js, blocks-proc.js, wcprogram.js
class WCBlock extends HTMLElement {
constructor() {
super();
this.dataset.block = "";
this.addEventListener("mousedown", this.startDragging);
this.wcForeFlow = [];
this.wcNextFlow = [];
this._gridSize = null;
this._el = {};
this._dragBound = ev => this.dragAround(ev);
this._stopBound = () => this.stopDragging();
}
connectedCallback() {
if (this._hooksSetup) return;
this._hooksSetup = true;
for (const hook of this.querySelectorAll(".hook")) {
this._setupHook(hook);
}
this.addEventListener("mousedown", () => { this._dragMoved = false; });
this.addEventListener("click", ev => {
if (ev.target.closest(".hook") || ev.target.closest(".delete-btn") || ev.target.closest(".resize-handle")) return;
if (this._dragMoved) return;
ev.stopPropagation();
this._select();
});
if (!(this instanceof WCStart) && !(this instanceof WCEnd)) {
const btn = document.createElement("button");
btn.className = "delete-btn";
btn.textContent = "\u00D7";
btn.title = "delete";
btn.addEventListener("click", ev => {
ev.stopPropagation();
this.delete();
});
this.appendChild(btn);
const rh = document.createElement("div");
rh.className = "resize-handle";
rh.addEventListener("mousedown", ev => {
ev.stopPropagation();
ev.preventDefault();
const startX = ev.clientX, startY = ev.clientY;
const startW = this.offsetWidth, startH = this.offsetHeight;
const move = e => {
this.style.width = Math.max(60, startW + e.clientX - startX) + "px";
this.style.height = Math.max(30, startH + e.clientY - startY) + "px";
for (const p of this.wcNextFlow) drawpath(p);
for (const p of this.wcForeFlow) drawpath(p);
};
const up = () => {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", up);
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", up);
});
this.appendChild(rh);
}
}
_select() {
for (const el of document.querySelectorAll(".selected")) {
el.classList.remove("selected");
}
this.classList.add("selected");
}
delete() {
resetLinkage();
for (const path of [...this.wcForeFlow]) deleteLink(path);
for (const path of [...this.wcNextFlow]) deleteLink(path);
this.wcForeFlow = [];
this.wcNextFlow = [];
this.remove();
refreshAll();
}
_setupHook(hook) {
hook.addEventListener("click", ev => {
ev.stopPropagation();
const linkType = hook.dataset.linkType;
if (!linkageFirst) {
if (hook.dataset.direction !== "out") return;
linkageFirst = { block: this, hook };
startShadowLink(hook);
} else if (this === linkageFirst.block) {
showError("Cannot link a block to itself.");
resetLinkage();
} else {
if (hook.dataset.direction !== "in") {
showError("Must link to an input hook.");
resetLinkage();
return;
}
const firstType = linkageFirst.hook.dataset.linkType;
if (firstType !== linkType) {
showError("Cannot link " + (firstType || "logic") + " to " + (linkType || "logic") + " hook.");
resetLinkage();
return;
}
if (linkType === "data") {
if (linkageFirst.hook.dataset.type !== hook.dataset.type) {
showError("Data type mismatch: " + (linkageFirst.hook.dataset.type || "?") + " \u2260 " + (hook.dataset.type || "?"));
resetLinkage();
return;
}
}
const path = document.createElementNS(svgNamespace, "path");
path.toElement = this;
path.fromElement = linkageFirst.block;
path.fromHook = linkageFirst.hook;
path.toHook = hook;
path.dataset.linkType = linkType || "logic";
path.classList.add(linkType === "data" ? "flow-data" : "flow");
this.wcForeFlow.push(path);
linkageFirst.block.wcNextFlow.push(path);
$id("connections").appendChild(path);
drawpath(path);
resetLinkage();
}
});
}
get dataType() { return null; }
saveData() { return {}; }
restoreData(data) {}
generateOwnCode() { return ""; }
generateCode(visited = new Set()) {
if (visited.has(this)) return "";
visited.add(this);
return this.generateOwnCode() + this.generateNextCode(visited);
}
generateNextCode(visited) {
let code = "";
for (const path of this.wcNextFlow) {
code += path.toElement.generateCode(visited);
}
return code;
}
_getGridSize() {
if (this._gridSize === null) {
this._gridSize = parseInt(
window.getComputedStyle(this).getPropertyValue("--grid-size")
);
}
return this._gridSize;
}
startDragging(event) {
if (event.target.closest(".resize-handle")) return;
this._dragMoved = false;
this._getGridSize();
this.dragStartX = event.clientX;
this.dragStartY = event.clientY;
document.onmousemove = this._dragBound;
document.onmouseup = this._stopBound;
}
dragAround(event) {
this._dragMoved = true;
const snap = v => Math.round(v / this._gridSize) * this._gridSize;
this.style.top = snap(this.offsetTop + event.clientY - this.dragStartY) + "px";
this.style.left = snap(this.offsetLeft + event.clientX - this.dragStartX) + "px";
this.dragStartX = snap(event.clientX);
this.dragStartY = snap(event.clientY);
for (const p of this.wcNextFlow) drawpath(p);
for (const p of this.wcForeFlow) drawpath(p);
}
stopDragging() {
document.onmousemove = null;
document.onmouseup = null;
}
}