Files
avcf/draw.js
T

208 lines
6.7 KiB
JavaScript

// ── SVG connection drawing, direction routing, shadow link & deletion ──────────
//
// Manages the two-click link system between block hooks, draws polyline SVG
// paths constrained to {0°, 45°, 90°} angles, renders a live shadow path
// from the selected output hook to the cursor while linking, and handles
// link deletion with full block array cleanup.
//
// Depends on: dom.js (svgNamespace, $, $id)
//
const EXT = 25; // pixels the path extends from a hook in its exit direction
let linkageFirst = null;
let shadowPath = null;
/**
* Routes a 45°-constrained polyline between two points.
* Returns the SVG path data (everything after the initial "M") as a string
* suitable for appending to an existing moveto.
* @param {number} x1 - start x
* @param {number} y1 - start y
* @param {number} x2 - end x
* @param {number} y2 - end y
* @returns {string} SVG path data segment (e.g. "L 100 50 L 150 50")
*/
function routeBetween(x1, y1, x2, y2) {
const dx = x2 - x1, dy = y2 - y1;
const adx = Math.abs(dx), ady = Math.abs(dy);
if (adx === 0 && ady === 0) return `L ${x2} ${y2}`;
const sx = dx >= 0 ? 1 : -1;
const sy = dy >= 0 ? 1 : -1;
const R45 = Math.PI / 4, R90 = Math.PI / 2, TOL = 5 * Math.PI / 180;
const normAngle = (() => {
const raw = Math.abs(Math.atan2(dy, dx));
return raw > R90 ? Math.PI - raw : raw;
})();
if (
Math.abs(normAngle) < TOL ||
Math.abs(normAngle - R45) < TOL ||
Math.abs(normAngle - R90) < TOL
) {
return `L ${x2} ${y2}`;
}
if (adx >= ady) {
const x1m = x1 + sx * ((adx - ady) / 2);
const x2m = x1m + sx * ady;
return `L ${x1m} ${y1} L ${x2m} ${y2} L ${x2} ${y2}`;
} else {
const y1m = y1 + sy * ((ady - adx) / 2);
const y2m = y1m + sy * adx;
return `L ${x1} ${y1m} L ${x2} ${y2m} L ${x2} ${y2}`;
}
}
/**
* Resolves a hook DOM element, preferring the explicitly stored reference
* and falling back to the block's first .hook child.
* @param {SVGPathElement} path
* @param {string} which - "from" or "to"
* @returns {HTMLElement} the hook button
*/
function resolveHook(path, which) {
const key = which === "from" ? "fromHook" : "toHook";
const elKey = which === "from" ? "fromElement" : "toElement";
return path[key] || path[elKey].querySelector(".hook");
}
/**
* Computes the extended control point for a hook: the hook centre pushed
* `EXT` pixels in the hook's declared exit direction.
* @param {HTMLElement} hook - the hook button element
* @param {DOMRect} svgRect - bounding rect of the SVG viewport
* @returns {{ x: number, y: number }}
*/
function getHookExit(hook, svgRect) {
const r = hook.getBoundingClientRect();
const x = (r.left + r.right) / 2 - svgRect.left;
const y = (r.top + r.bottom) / 2 - svgRect.top;
const dir = hook.dataset.exit;
if (dir === "right") return { x: x + EXT, y };
if (dir === "left") return { x: x - EXT, y };
if (dir === "down") return { x, y: y + EXT };
if (dir === "up") return { x, y: y - EXT };
return { x, y };
}
/**
* Draws (or redraws) an SVG connection path between two linked block hooks.
* Each segment starts by extending EXT pixels in the source hook's exit
* direction and ends by approaching from the target hook's exit direction.
* The middle portion uses 45°-constrained routing.
* @param {SVGPathElement} path - path element with fromHook / toHook references
*/
function drawpath(path) {
const svg = path.closest("svg");
const svgRect = svg.getBoundingClientRect();
const fromHook = resolveHook(path, "from");
const toHook = resolveHook(path, "to");
const rFrom = fromHook.getBoundingClientRect();
const rTo = toHook.getBoundingClientRect();
const fx = (rFrom.left + rFrom.right) / 2 - svgRect.left;
const fy = (rFrom.top + rFrom.bottom) / 2 - svgRect.top;
const tx = (rTo.left + rTo.right) / 2 - svgRect.left;
const ty = (rTo.top + rTo.bottom) / 2 - svgRect.top;
const srcExit = getHookExit(fromHook, svgRect);
const tgtExit = getHookExit(toHook, svgRect);
path.setAttribute("d",
`M ${fx} ${fy} L ${srcExit.x} ${srcExit.y}` +
routeBetween(srcExit.x, srcExit.y, tgtExit.x, tgtExit.y) +
` L ${tx} ${ty}`
);
}
// ── Shadow (preview) link ─────────────────────────────────────────────────────
/**
* Begins showing a dashed shadow path from the given hook to the cursor.
* @param {HTMLElement} hook - the source hook button
*/
function startShadowLink(hook) {
if (shadowPath) endShadowLink();
const svg = $id("connections");
shadowPath = document.createElementNS(svgNamespace, "path");
shadowPath.classList.add(hook.dataset.linkType === "data" ? "flow-shadow-data" : "flow-shadow");
shadowPath._hook = hook;
svg.appendChild(shadowPath);
document.addEventListener("mousemove", onShadowMove);
}
/** Updates the shadow path endpoint to follow the mouse. */
function onShadowMove(ev) {
if (!shadowPath) return;
const svg = shadowPath.closest("svg");
const svgRect = svg.getBoundingClientRect();
const mx = ev.clientX - svgRect.left;
const my = ev.clientY - svgRect.top;
const hook = shadowPath._hook;
const rHook = hook.getBoundingClientRect();
const fx = (rHook.left + rHook.right) / 2 - svgRect.left;
const fy = (rHook.top + rHook.bottom) / 2 - svgRect.top;
const exit = getHookExit(hook, svgRect);
shadowPath.setAttribute("d",
`M ${fx} ${fy} L ${exit.x} ${exit.y}` +
routeBetween(exit.x, exit.y, mx, my)
);
}
/** Removes the shadow path and stops tracking the mouse. */
function endShadowLink() {
if (shadowPath) {
shadowPath.remove();
shadowPath = null;
}
document.removeEventListener("mousemove", onShadowMove);
}
/** Cancels any pending link operation and removes the shadow. */
function resetLinkage() {
linkageFirst = null;
endShadowLink();
}
// ── Link deletion ────────────────────────────────────────────────────────────
/**
* Removes an SVG connection path and cleans up both blocks' flow arrays.
* @param {SVGPathElement} path - the flow path element to delete
*/
function deleteLink(path) {
if (!path.classList.contains("flow") && !path.classList.contains("flow-data")) return;
const src = path.fromElement;
const tgt = path.toElement;
if (src && src.wcNextFlow) {
const i = src.wcNextFlow.indexOf(path);
if (i !== -1) src.wcNextFlow.splice(i, 1);
}
if (tgt && tgt.wcForeFlow) {
const i = tgt.wcForeFlow.indexOf(path);
if (i !== -1) tgt.wcForeFlow.splice(i, 1);
}
path.remove();
path.fromElement = null;
path.toElement = null;
path.fromHook = null;
path.toHook = null;
}