js/edge-path.mjs

/**
 * Edge path generators for different edge types.
 */

import { calculateStepPoints, clearStepCache } from './step-routing.mjs'

export { clearStepCache }

/**
 * 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,
}) {
    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 }) {
    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,
}) {
    const points = calculateStepPoints(
        sourceX, sourceY, sourcePosition,
        targetX, targetY, targetPosition,
        offset, allNodes, nodeInternals, connFromId, connToId
    )
    const path = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' ')
    const label = labelAtHalfLength(points)
    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,
}) {
    const points = calculateStepPoints(
        sourceX, sourceY, sourcePosition,
        targetX, targetY, targetPosition,
        offset, allNodes, nodeInternals, connFromId, connToId
    )

    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 }
}

/**
 * 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 }
}