/**
* Edge path generators for different edge types.
*/
import { calculateStepPoints, clearStepCache } from './stepRouting.mjs'
export { clearStepCache }
/**
* Normalize conn.points (forced waypoints) into [{x,y},...].
* Accepts [[x,y],...] or [{x,y},...]; returns null when absent/invalid so callers
* can fall back to automatic routing.
*/
function normalizeConnPoints(points) {
if (!Array.isArray(points) || points.length === 0) return null
const pts = []
for (const p of points) {
let x = null
let y = null
if (Array.isArray(p) && p.length >= 2) {
x = Number(p[0]); y = Number(p[1])
}
else if (p && typeof p === 'object') {
x = Number(p.x); y = Number(p.y)
}
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
pts.push({ x, y })
}
return pts
}
/**
* Build an orthogonal point list passing through forced waypoints.
* - Leaves the source along its anchor axis (perpendicular to the node edge).
* - Turns AT each waypoint (leaves it along the other axis than it arrived on).
* - Arrives at the target along its anchor axis.
* Inserts at most one corner between consecutive points; skips zero-length segments.
*/
function orthogonalizeThroughPoints(
sourceX, sourceY, sourcePosition,
targetX, targetY, targetPosition,
waypoints
) {
const pts = [{ x: sourceX, y: sourceY }]
const push = (p) => {
const last = pts[pts.length - 1]
if (Math.abs(p.x - last.x) > 0.5 || Math.abs(p.y - last.y) > 0.5) pts.push({ x: p.x, y: p.y })
}
// Axis of the first sub-segment when leaving the previous point
let leaveHoriz = sourcePosition === 'left' || sourcePosition === 'right'
for (let i = 0; i < waypoints.length; i++) {
const w = waypoints[i]
const prev = pts[pts.length - 1]
const dx = Math.abs(w.x - prev.x)
const dy = Math.abs(w.y - prev.y)
let arriveHoriz
if (dx > 0.5 && dy > 0.5) {
push(leaveHoriz ? { x: w.x, y: prev.y } : { x: prev.x, y: w.y })
arriveHoriz = !leaveHoriz
}
else {
arriveHoriz = dx > 0.5
}
push(w)
leaveHoriz = !arriveHoriz //bend point: turn at the waypoint
}
// Final hop: approach the target along its anchor axis
const prev = pts[pts.length - 1]
const targetHoriz = targetPosition === 'left' || targetPosition === 'right'
if (Math.abs(targetX - prev.x) > 0.5 && Math.abs(targetY - prev.y) > 0.5) {
push(targetHoriz ? { x: prev.x, y: targetY } : { x: targetX, y: prev.y })
}
push({ x: targetX, y: targetY })
return pts
}
/**
* Build a rounded (smoothstep-style) svg path from an orthogonal point list.
* @returns {{ path: string, labelX: number, labelY: number }}
*/
function buildRoundedPath(points, borderRadius) {
if (points.length <= 2) {
const path = `M ${points[0].x},${points[0].y} L ${points[1].x},${points[1].y}`
const labelX = (points[0].x + points[1].x) / 2
const labelY = (points[0].y + points[1].y) / 2
return { path, labelX, labelY }
}
let path = `M ${points[0].x},${points[0].y}`
for (let i = 1; i < points.length - 1; i++) {
const prev = points[i - 1]
const curr = points[i]
const next = points[i + 1]
const dx1 = curr.x - prev.x
const dy1 = curr.y - prev.y
const dx2 = next.x - curr.x
const dy2 = next.y - curr.y
const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1)
const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2)
if (len1 === 0 || len2 === 0) {
// Zero-length segment — skip rounding, just draw straight line
path += ` L ${curr.x},${curr.y}`
continue
}
const r = Math.min(borderRadius, len1 / 2, len2 / 2)
const beforeX = curr.x - (dx1 / len1) * r
const beforeY = curr.y - (dy1 / len1) * r
const afterX = curr.x + (dx2 / len2) * r
const afterY = curr.y + (dy2 / len2) * r
path += ` L ${beforeX},${beforeY} Q ${curr.x},${curr.y} ${afterX},${afterY}`
}
path += ` L ${points[points.length - 1].x},${points[points.length - 1].y}`
const label = labelAtHalfLength(points)
return { path, labelX: label.x, labelY: label.y }
}
/**
* Calculate the control point offset for bezier curves based on handle position.
*/
function getControlOffset(distance, position) {
switch (position) {
case 'top': return { x: 0, y: -distance }
case 'bottom': return { x: 0, y: distance }
case 'left': return { x: -distance, y: 0 }
case 'right': return { x: distance, y: 0 }
default: return { x: 0, y: 0 }
}
}
/**
* Get bezier curve path.
* @returns {{ path: string, labelX: number, labelY: number }}
*/
export function getBezierPath({
sourceX, sourceY, sourcePosition = 'bottom',
targetX, targetY, targetPosition = 'top',
curvature = 0.25,
points,
}) {
// Forced waypoints: smooth curve passing exactly through each point (Catmull-Rom → cubic)
const wps = normalizeConnPoints(points)
if (wps) {
const pts = [{ x: sourceX, y: sourceY }, ...wps, { x: targetX, y: targetY }]
let path = `M ${pts[0].x},${pts[0].y}`
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i - 1] || pts[i]
const p1 = pts[i]
const p2 = pts[i + 1]
const p3 = pts[i + 2] || p2
const c1x = p1.x + (p2.x - p0.x) / 6
const c1y = p1.y + (p2.y - p0.y) / 6
const c2x = p2.x - (p3.x - p1.x) / 6
const c2y = p2.y - (p3.y - p1.y) / 6
path += ` C ${c1x},${c1y} ${c2x},${c2y} ${p2.x},${p2.y}`
}
const label = labelAtHalfLength(pts)
return { path, labelX: label.x, labelY: label.y }
}
const dist = Math.sqrt(Math.pow(targetX - sourceX, 2) + Math.pow(targetY - sourceY, 2))
const offset = Math.max(dist * curvature, 25)
const s = getControlOffset(offset, sourcePosition)
const t = getControlOffset(offset, targetPosition)
const controlX1 = sourceX + s.x
const controlY1 = sourceY + s.y
const controlX2 = targetX + t.x
const controlY2 = targetY + t.y
const path = `M ${sourceX},${sourceY} C ${controlX1},${controlY1} ${controlX2},${controlY2} ${targetX},${targetY}`
const labelX = (sourceX + controlX1 + controlX2 + targetX) / 4
const labelY = (sourceY + controlY1 + controlY2 + targetY) / 4
return { path, labelX, labelY }
}
/**
* Get straight line path.
* @returns {{ path: string, labelX: number, labelY: number }}
*/
export function getStraightPath({ sourceX, sourceY, targetX, targetY, points }) {
// Forced waypoints: polyline through each point
const wps = normalizeConnPoints(points)
if (wps) {
const pts = [{ x: sourceX, y: sourceY }, ...wps, { x: targetX, y: targetY }]
const path = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' ')
const label = labelAtHalfLength(pts)
return { path, labelX: label.x, labelY: label.y }
}
const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`
const labelX = (sourceX + targetX) / 2
const labelY = (sourceY + targetY) / 2
return { path, labelX, labelY }
}
/**
* Get step (right-angle) path.
* @returns {{ path: string, labelX: number, labelY: number }}
*/
export function getStepPath({
sourceX, sourceY, sourcePosition = 'bottom',
targetX, targetY, targetPosition = 'top',
offset = 20,
allNodes, nodeInternals, connFromId, connToId,
points,
}) {
// Forced waypoints bypass automatic routing (anchors still honored at both ends)
const wps = normalizeConnPoints(points)
const pts = wps
? orthogonalizeThroughPoints(sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, wps)
: calculateStepPoints(
sourceX, sourceY, sourcePosition,
targetX, targetY, targetPosition,
offset, allNodes, nodeInternals, connFromId, connToId
)
const path = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' ')
const label = labelAtHalfLength(pts)
return { path, labelX: label.x, labelY: label.y }
}
/**
* Get smooth step (rounded right-angle) path.
* @returns {{ path: string, labelX: number, labelY: number }}
*/
export function getSmoothStepPath({
sourceX, sourceY, sourcePosition = 'bottom',
targetX, targetY, targetPosition = 'top',
borderRadius = 5,
offset = 20,
allNodes, nodeInternals, connFromId, connToId,
points,
}) {
// Forced waypoints bypass automatic routing (anchors still honored at both ends)
const wps = normalizeConnPoints(points)
const pts = wps
? orthogonalizeThroughPoints(sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, wps)
: calculateStepPoints(
sourceX, sourceY, sourcePosition,
targetX, targetY, targetPosition,
offset, allNodes, nodeInternals, connFromId, connToId
)
return buildRoundedPath(pts, borderRadius)
}
/**
* Find the point at exactly half the total Manhattan path length.
*/
function labelAtHalfLength(pts) {
if (pts.length < 2) return { x: pts[0].x, y: pts[0].y }
let totalLen = 0
for (let i = 0; i < pts.length - 1; i++) {
totalLen += Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y)
}
let half = totalLen / 2
let acc = 0
for (let i = 0; i < pts.length - 1; i++) {
let segLen = Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y)
if (acc + segLen >= half) {
let ratio = segLen > 0 ? (half - acc) / segLen : 0
return {
x: pts[i].x + (pts[i + 1].x - pts[i].x) * ratio,
y: pts[i].y + (pts[i + 1].y - pts[i].y) * ratio,
}
}
acc += segLen
}
return { x: pts[pts.length - 1].x, y: pts[pts.length - 1].y }
}