208 lines
6.7 KiB
JavaScript
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;
|
|
}
|