// ── 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; } }