// ── 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 /** Base class for all draggable, linkable flowchart and measurement blocks. */ class WCBlock extends HTMLElement { constructor() { super(); this.dataset.block = ""; this.addEventListener("mousedown", this.startDragging); /** @type {SVGPathElement[]} Incoming flow paths (from other blocks to this one). */ this.wcForeFlow = []; /** @type {SVGPathElement[]} Outgoing flow paths (from this block to others). */ 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(); }); this._isFlowBlock = !!this.closest("#draw-area"); const isAlgoBlock = this._isFlowBlock; if (!(this instanceof WCStart) && !(this instanceof WCEnd) && !this.dataset.readonly) { 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); if (isAlgoBlock) { 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); } } if (isAlgoBlock && !(this instanceof WCStart)) { const eb = document.createElement("button"); eb.className = "edit-btn"; eb.title = "edit"; eb.innerHTML = '' + ''; eb.addEventListener("click", ev => { ev.stopPropagation(); showBlockEditor(this); }); this.appendChild(eb); } if (isAlgoBlock) { const warn = document.createElement("span"); warn.className = "block-warn"; warn.textContent = "\u26A0"; this.appendChild(warn); setTimeout(() => this._checkLogicInput(), 0); } /* Replace inputs with value labels so blocks show compact info. The edit modal provides full editing. */ if (isAlgoBlock) { for (const field of this.querySelectorAll("input, select, textarea")) { const label = document.createElement("span"); label.className = "field-display"; const update = () => { if (field.tagName === "SELECT") { label.textContent = field.options[field.selectedIndex]?.text || ""; } else { label.textContent = field.value || ""; } }; update(); field.addEventListener("change", update); field.addEventListener("input", update); field.parentNode.insertBefore(label, field); field.style.display = "none"; } } } /** Checks if this block has an incoming logic flow; toggles the `.disconnected` class. */ _checkLogicInput() { const hook = this.querySelector('.hook[data-direction="in"][data-link-type="logic"]'); if (!hook) return; const has = this.wcForeFlow.some(p => p.toHook === hook); if (!has) this.classList.add("disconnected"); else this.classList.remove("disconnected"); } /** * Finds a required child element or throws an informative error. * @param {string} selector CSS selector * @returns {HTMLElement} */ _require(selector) { const el = this.querySelector(selector); if (!el) throw new Error(`${this.tagName}: missing required element "${selector}"`); return el; } /** Marks this block as selected (adds `.selected` class, removes it from all others). */ _select() { for (const el of document.querySelectorAll(".selected")) { el.classList.remove("selected"); } this.classList.add("selected"); } /** Removes this block and all connected flow paths, cleaning up arrays. */ 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(); } /** * Attaches two-click link behaviour to a single `.hook` button element. * @param {HTMLElement} hook */ _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; hook.dataset.connected = ""; path.dataset.linkType = linkType || "logic"; path.classList.add(linkType === "data" ? "flow-data" : "flow"); if (linkageFirst.hook.classList.contains("proccall-error-hook")) { path.classList.add("flow-error-source"); path.classList.add("flow-error"); } else if (linkType !== "data" && linkageFirst.block.classList.contains("error-path")) { path.classList.add("flow-error"); } this.wcForeFlow.push(path); linkageFirst.block.wcNextFlow.push(path); $id("connections").appendChild(path); drawpath(path); if (linkType === "data") { _dataSourceCache.set(hook, linkageFirst.block); } refreshBlockWarnings(); refreshErrorPaths(); resetLinkage(); } }); } /** @returns {string|null} The data type this block exposes (overridden by subclasses). */ get dataType() { return null; } /** @returns {Object} Serializable data for persistence (overridden by subclasses). */ saveData() { return {}; } /** * Restores block state from saved data (overridden by subclasses). * @param {Object} data */ restoreData(data) {} /** @returns {string} C code fragment for this single block (overridden by subclasses). */ generateOwnCode() { return ""; } /** * C flow-control: `goto` statements for every successor in wcNextFlow. * Override for decisions (if-goto) and error-handling proc calls. * @returns {string} */ generateFlowCode() { let code = ""; for (const path of this.wcNextFlow) { code += " goto " + path.toElement.id + ";\n"; } return code; } /** * Generates C code for variable declarations (non-flow blocks only). * For flow blocks, use CodeGen.emitFlat instead. * @returns {string} */ generateCode(visited) { if (!this._isFlowBlock) return this.generateOwnCode(); return ""; // flow blocks are handled by CodeGen.emitFlat } /** @returns {number} The CSS `--grid-size` value, cached after first read (default 25). */ _getGridSize() { if (this._gridSize === null) { this._gridSize = parseInt( window.getComputedStyle(this).getPropertyValue("--grid-size") ); } if (isNaN(this._gridSize)) this._gridSize = 25; return this._gridSize; } /** * Begins a drag operation; records start position and binds move/up listeners. * @param {MouseEvent} event */ startDragging(event) { if (event.target.closest(".resize-handle")) return; if (!this.closest("#draw-area")) return; this._dragMoved = false; this._getGridSize(); this.dragStartX = event.clientX; this.dragStartY = event.clientY; document.addEventListener("mousemove", this._dragBound); document.addEventListener("mouseup", this._stopBound); } /** * Handles mouse move during drag; grid-snaps position and enforces section border constraint. * @param {MouseEvent} event */ dragAround(event) { this._dragMoved = true; const snap = v => Math.round(v / this._gridSize) * this._gridSize; const prevTop = this.style.top, prevLeft = this.style.left; this.style.top = snap(this.offsetTop + event.clientY - this.dragStartY) + "px"; this.style.left = snap(this.offsetLeft + event.clientX - this.dragStartX) + "px"; if (typeof blockOnSectionBorder === "function" && blockOnSectionBorder(this)) { this.style.top = prevTop; this.style.left = prevLeft; } else { 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); } /** Ends the drag operation and removes move/up listeners. */ stopDragging() { document.removeEventListener("mousemove", this._dragBound); document.removeEventListener("mouseup", this._stopBound); } }