UNPKG

mermaid

Version:

Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.

405 lines (402 loc) 13 kB
import { markerOffsets } from "./chunk-7XZOGNMF.mjs"; import { __name } from "./chunk-35ZOQMXA.mjs"; // src/rendering-util/rendering-elements/lineJump.ts var ROUNDED_CORNER_RADIUS = 5; var CORNER_EPSILON = 1e-5; var CORNER_JUMP_CLEARANCE = 2; var MIN_USEFUL_RADIUS_RATIO = 0.6; var ENDPOINT_EPSILON = 1e-6; function buildSegmentList(points) { const segments = []; for (let i = 0; i < points.length - 1; i++) { segments.push({ a: points[i], b: points[i + 1] }); } return segments; } __name(buildSegmentList, "buildSegmentList"); function segmentIntersection(a1, a2, b1, b2) { const dxA = a2.x - a1.x; const dyA = a2.y - a1.y; const dxB = b2.x - b1.x; const dyB = b2.y - b1.y; const denom = dxA * dyB - dyA * dxB; if (denom === 0) { return null; } const dx = b1.x - a1.x; const dy = b1.y - a1.y; const tA = (dx * dyB - dy * dxB) / denom; const tB = (dx * dyA - dy * dxA) / denom; if (tA <= ENDPOINT_EPSILON || tA >= 1 - ENDPOINT_EPSILON || tB <= ENDPOINT_EPSILON || tB >= 1 - ENDPOINT_EPSILON) { return null; } return { point: { x: a1.x + tA * dxA, y: a1.y + tA * dyA }, tA, tB }; } __name(segmentIntersection, "segmentIntersection"); function isHorizontalSeg(seg) { return Math.abs(seg.b.x - seg.a.x) >= Math.abs(seg.b.y - seg.a.y); } __name(isHorizontalSeg, "isHorizontalSeg"); function crossingSitsInRoundedCorner(edge, segIndex, t) { if (edge.curve !== "rounded") { return false; } const pts = edge.points; const a = pts[segIndex]; const b = pts[segIndex + 1]; if (!a || !b) { return false; } const segLen = Math.hypot(b.x - a.x, b.y - a.y); const d = t * segLen; const entering = segIndex > 0 ? computeRoundedCorner(pts[segIndex - 1], a, b, ROUNDED_CORNER_RADIUS) : null; if (entering && d < entering.cutLen) { return true; } const leaving = segIndex + 2 < pts.length ? computeRoundedCorner(a, b, pts[segIndex + 2], ROUNDED_CORNER_RADIUS) : null; return leaving !== null && segLen - d < leaving.cutLen; } __name(crossingSitsInRoundedCorner, "crossingSitsInRoundedCorner"); function findEdgeIntersections(edges) { const crossings = []; for (let i = 0; i < edges.length; i++) { const edgeA = edges[i]; const segmentsA = buildSegmentList(edgeA.points); for (let j = i + 1; j < edges.length; j++) { const edgeB = edges[j]; const segmentsB = buildSegmentList(edgeB.points); for (const [si, segA] of segmentsA.entries()) { for (const [sj, segB] of segmentsB.entries()) { const hit = segmentIntersection(segA.a, segA.b, segB.a, segB.b); if (!hit) { continue; } if (crossingSitsInRoundedCorner(edgeA, si, hit.tA) || crossingSitsInRoundedCorner(edgeB, sj, hit.tB)) { continue; } const aHoriz = isHorizontalSeg(segA); const bHoriz = isHorizontalSeg(segB); const orthogonalPair = aHoriz !== bHoriz; const jumpOnA = orthogonalPair ? aHoriz : false; if (jumpOnA) { crossings.push({ jumpEdgeId: edgeA.id, otherEdgeId: edgeB.id, segIndex: si, t: hit.tA, point: hit.point }); } else { crossings.push({ jumpEdgeId: edgeB.id, otherEdgeId: edgeA.id, segIndex: sj, t: hit.tB, point: hit.point }); } } } } } return crossings; } __name(findEdgeIntersections, "findEdgeIntersections"); function fmt(n) { const rounded = Math.round(n * 1e3) / 1e3; return Number.isInteger(rounded) ? `${rounded}` : `${rounded}`; } __name(fmt, "fmt"); function pointToString(p) { return `${fmt(p.x)},${fmt(p.y)}`; } __name(pointToString, "pointToString"); function getArcSweepFlag(seg) { const dx = seg.b.x - seg.a.x; const dy = seg.b.y - seg.a.y; if (Math.abs(dx) >= Math.abs(dy)) { return dx >= 0 ? 1 : 0; } return dy >= 0 ? 1 : 0; } __name(getArcSweepFlag, "getArcSweepFlag"); function applyMarkerOffsets(points, edge) { if (points.length < 2) { return points.map((p) => ({ ...p })); } const out = points.map((p) => ({ ...p })); const startOff = edge.arrowTypeStart && markerOffsets[edge.arrowTypeStart]; if (startOff) { const a = points[0]; const b = points[1]; const ang = Math.atan2(b.y - a.y, b.x - a.x); out[0].x = a.x + startOff * Math.cos(ang); out[0].y = a.y + startOff * Math.sin(ang); } const endOff = edge.arrowTypeEnd && markerOffsets[edge.arrowTypeEnd]; if (endOff) { const n = points.length; const a = points[n - 2]; const b = points[n - 1]; const ang = Math.atan2(b.y - a.y, b.x - a.x); out[n - 1].x = b.x - endOff * Math.cos(ang); out[n - 1].y = b.y - endOff * Math.sin(ang); } return out; } __name(applyMarkerOffsets, "applyMarkerOffsets"); function emitJump(jump, ux, uy, sweep, style) { const cx = jump.point.x; const cy = jump.point.y; const pre = { x: cx - ux * jump.r, y: cy - uy * jump.r }; const post = { x: cx + ux * jump.r, y: cy + uy * jump.r }; const out = [`L${pointToString(pre)}`]; if (style === "arc") { out.push(`A${fmt(jump.r)},${fmt(jump.r)} 0 0 ${sweep} ${pointToString(post)}`); } else { out.push(`M${pointToString(post)}`); } return out; } __name(emitJump, "emitJump"); function computeRoundedCorner(prev, curr, next, radius) { 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.hypot(dx1, dy1); const len2 = Math.hypot(dx2, dy2); if (len1 < CORNER_EPSILON || len2 < CORNER_EPSILON) { return null; } const nx1 = dx1 / len1; const ny1 = dy1 / len1; const nx2 = dx2 / len2; const ny2 = dy2 / len2; const dot = nx1 * nx2 + ny1 * ny2; const clamped = Math.max(-1, Math.min(1, dot)); const angle = Math.acos(clamped); if (angle < CORNER_EPSILON || Math.abs(Math.PI - angle) < CORNER_EPSILON) { return null; } const cutLen = Math.min(radius / Math.sin(angle / 2), len1 / 2, len2 / 2); return { startX: curr.x - nx1 * cutLen, startY: curr.y - ny1 * cutLen, endX: curr.x + nx2 * cutLen, endY: curr.y + ny2 * cutLen, ctrlX: curr.x, ctrlY: curr.y, cutLen }; } __name(computeRoundedCorner, "computeRoundedCorner"); function rewriteEdgePath(edge, jumps, config) { const rawPoints = edge.points; if (rawPoints.length < 2) { return ""; } const points = applyMarkerOffsets(rawPoints, edge); const rounded = edge.curve === "rounded"; const segments = buildSegmentList(points); const bySeg = /* @__PURE__ */ new Map(); for (const j of jumps) { const seg = segments[j.segIndex]; if (!seg) { continue; } const segLen = Math.hypot(seg.b.x - seg.a.x, seg.b.y - seg.a.y); const list = bySeg.get(j.segIndex) ?? []; list.push({ t: j.t, point: j.point, d: j.t * segLen, r: config.jumpRadius }); bySeg.set(j.segIndex, list); } const parts = [`M${pointToString(points[0])}`]; for (let i = 0; i < segments.length; i++) { const seg = segments[i]; const segLen = Math.hypot(seg.b.x - seg.a.x, seg.b.y - seg.a.y); const ux = segLen === 0 ? 0 : (seg.b.x - seg.a.x) / segLen; const uy = segLen === 0 ? 0 : (seg.b.y - seg.a.y) / segLen; const sweep = getArcSweepFlag(seg); let segStartConsumed = 0; if (rounded && i > 0) { const corner = computeRoundedCorner( points[i - 1], points[i], points[i + 1] ?? points[i], ROUNDED_CORNER_RADIUS ); if (corner) { segStartConsumed = corner.cutLen; } } let segEndStop = segLen; let upcomingCorner = null; if (rounded && i < segments.length - 1) { upcomingCorner = computeRoundedCorner( points[i], points[i + 1], points[i + 2] ?? points[i + 1], ROUNDED_CORNER_RADIUS ); if (upcomingCorner) { segEndStop = segLen - upcomingCorner.cutLen; } } const minUsefulRadius = config.jumpRadius * MIN_USEFUL_RADIUS_RATIO; const segJumps = [...bySeg.get(i) ?? []].sort((a, b) => a.t - b.t).filter((j) => { const room = Math.min(j.d - segStartConsumed, segEndStop - j.d) - CORNER_JUMP_CLEARANCE; j.r = Math.min(j.r, room); return j.r >= minUsefulRadius; }); for (let k = 0; k < segJumps.length - 1; k++) { const gap = segJumps[k + 1].d - segJumps[k].d; if (segJumps[k].r + segJumps[k + 1].r > gap) { const half = gap / 2; segJumps[k].r = Math.min(segJumps[k].r, half); segJumps[k + 1].r = Math.min(segJumps[k + 1].r, half); } } for (const j of segJumps) { if (j.r < minUsefulRadius) { continue; } parts.push(...emitJump(j, ux, uy, sweep, config.jumpStyle)); } if (rounded && upcomingCorner) { parts.push(`L${fmt(upcomingCorner.startX)},${fmt(upcomingCorner.startY)}`); parts.push( `Q${fmt(upcomingCorner.ctrlX)},${fmt(upcomingCorner.ctrlY)} ${fmt(upcomingCorner.endX)},${fmt(upcomingCorner.endY)}` ); } else { parts.push(`L${pointToString(seg.b)}`); } } return parts.join(" "); } __name(rewriteEdgePath, "rewriteEdgePath"); function isStraightPath(d) { return /^[\d\s+,.LMelm-]*$/.test(d); } __name(isStraightPath, "isStraightPath"); function curveSupportsLineHops(curve) { if (!curve) { return true; } return curve === "linear" || curve === "rounded" || curve === "step" || curve === "stepBefore" || curve === "stepAfter"; } __name(curveSupportsLineHops, "curveSupportsLineHops"); function decodeDataPoints(raw) { if (!raw) { return null; } try { const json = typeof atob === "function" ? atob(raw) : Buffer.from(raw, "base64").toString(); const parsed = JSON.parse(json); if (!Array.isArray(parsed)) { return null; } const pts = []; for (const p of parsed) { if (p && typeof p.x === "number" && typeof p.y === "number") { pts.push({ x: p.x, y: p.y }); } } return pts.length >= 2 ? pts : null; } catch { return null; } } __name(decodeDataPoints, "decodeDataPoints"); function applyLineJumpsToSvg(edgePathsGroup, edges, config) { if (!config.enabled) { return; } const groupNode = edgePathsGroup.node(); if (!groupNode) { return; } const edgeMeta = /* @__PURE__ */ new Map(); for (const e of edges) { edgeMeta.set(e.id, e); } const pathByDataId = /* @__PURE__ */ new Map(); for (const el of groupNode.querySelectorAll("path[data-id]")) { const id = el.getAttribute("data-id"); if (id !== null && !pathByDataId.has(id)) { pathByDataId.set(id, el); } } const renderedEdges = []; for (const e of edges) { const pathEl = pathByDataId.get(e.id); if (!pathEl) { continue; } const decoded = decodeDataPoints(pathEl.getAttribute("data-points")); const points = decoded ?? e.points; renderedEdges.push({ ...e, points }); } const crossings = findEdgeIntersections(renderedEdges); if (crossings.length === 0) { return; } const jumpsByEdge = /* @__PURE__ */ new Map(); for (const c of crossings) { const list = jumpsByEdge.get(c.jumpEdgeId) ?? []; list.push(c); jumpsByEdge.set(c.jumpEdgeId, list); } for (const renderedEdge of renderedEdges) { const jumps = jumpsByEdge.get(renderedEdge.id); if (!jumps || jumps.length === 0) { continue; } const meta = edgeMeta.get(renderedEdge.id); const curveHint = meta?.curve; if (curveHint !== void 0 && !curveSupportsLineHops(curveHint)) { continue; } const pathEl = pathByDataId.get(renderedEdge.id); if (!pathEl) { continue; } if (curveHint === void 0) { const currentD = pathEl.getAttribute("d") ?? ""; if (!isStraightPath(currentD)) { continue; } } const originalStyle = pathEl.getAttribute("style") ?? ""; const dasharrayMatch = /stroke-dasharray\s*:\s*0\s+([\d.]+)\s+[\d.]+\s+([\d.]+)/.exec( originalStyle ); const preservedOValueS = dasharrayMatch ? Number.parseFloat(dasharrayMatch[1]) : null; const preservedOValueE = dasharrayMatch ? Number.parseFloat(dasharrayMatch[2]) : null; const newD = rewriteEdgePath(renderedEdge, jumps, config); pathEl.setAttribute("d", newD); if (preservedOValueS !== null && preservedOValueE !== null && typeof pathEl.getTotalLength === "function") { const newLen = pathEl.getTotalLength(); const onLen = Math.max(0, newLen - preservedOValueS - preservedOValueE); const newDasharray = `0 ${preservedOValueS} ${onLen} ${preservedOValueE}`; const cleaned = originalStyle.replace(/stroke-dasharray\s*:[^;]*;?/g, `stroke-dasharray: ${newDasharray};`).replace(/;\s*;+/g, ";"); pathEl.setAttribute("style", cleaned); } } } __name(applyLineJumpsToSvg, "applyLineJumpsToSvg"); export { applyLineJumpsToSvg };