| import * as d3 from 'd3'; |
|
|
| |
| export const LINEAR_ARC_ADJACENT_GAP_DEFAULT = 0; |
| export const LINEAR_ARC_ADJACENT_GAP_MIN = 0; |
| export const LINEAR_ARC_ADJACENT_GAP_MAX = 400; |
|
|
| |
| const LINEAR_ARC_PROMPT_GEN_EXTRA_GAP_PX = 12; |
|
|
| |
| |
| |
| |
| export const LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION = 0.25; |
|
|
| |
| const LINEAR_ARC_FIRST_CENTER_X = 20; |
| const LINEAR_ARC_BASELINE_Y = 0; |
|
|
| export function clampLinearArcAdjacentGap(px: number): number { |
| return Math.max( |
| LINEAR_ARC_ADJACENT_GAP_MIN, |
| Math.min(LINEAR_ARC_ADJACENT_GAP_MAX, Math.round(px)) |
| ); |
| } |
|
|
| type LinearArcNodeLike = { nodeW: number }; |
|
|
| |
| type LinearArcSteppedNode = LinearArcNodeLike & { step: number }; |
|
|
| function computeNodeCenterXs(nodes: LinearArcSteppedNode[], adjacentGapPx: number): number[] { |
| const xs: number[] = []; |
| if (nodes.length === 0) return xs; |
| xs.push(LINEAR_ARC_FIRST_CENTER_X); |
| for (let i = 1; i < nodes.length; i++) { |
| const prev = nodes[i - 1]!; |
| const curr = nodes[i]!; |
| const gap = |
| adjacentGapPx + |
| (prev.step === -1 && curr.step !== -1 ? LINEAR_ARC_PROMPT_GEN_EXTRA_GAP_PX : 0); |
| xs.push(xs[i - 1]! + prev.nodeW / 2 + gap + curr.nodeW / 2); |
| } |
| return xs; |
| } |
|
|
| |
| |
| |
| |
| |
| export function paintLinearArcLayout< |
| LinkDatum, |
| NodeDatum extends LinearArcSteppedNode, |
| >(params: { |
| linkSel: d3.Selection<SVGGElement, LinkDatum, SVGGElement, unknown>; |
| nodeSel: d3.Selection<SVGGElement, NodeDatum, SVGGElement, unknown>; |
| nodes: NodeDatum[]; |
| adjacentGapPx: number; |
| getLinkNodes: (link: LinkDatum) => { src: NodeDatum; tgt: NodeDatum }; |
| }): void { |
| const { linkSel, nodeSel, nodes, adjacentGapPx, getLinkNodes } = params; |
|
|
| const centerXs = computeNodeCenterXs(nodes, adjacentGapPx); |
|
|
| |
| const centerXByNode = new Map<NodeDatum, number>(); |
| for (let i = 0; i < nodes.length; i++) { |
| centerXByNode.set(nodes[i]!, centerXs[i]!); |
| } |
|
|
| const arcPathBetweenNodes = (src: NodeDatum, tgt: NodeDatum): string => { |
| const srcCx = centerXByNode.get(src); |
| const tgtCx = centerXByNode.get(tgt); |
| if (srcCx === undefined || tgtCx === undefined) { |
| throw new Error('paintLinearArcLayout: link endpoint not in linear node list'); |
| } |
| const y = LINEAR_ARC_BASELINE_Y; |
| const dx = Math.abs(tgtCx - srcCx); |
| const arcH = dx * 0.4; |
| const upY = y - arcH; |
| const t = Math.max(0, Math.min(1, LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION)); |
| const inset = t * (dx / 2); |
| const dir = tgtCx >= srcCx ? 1 : -1; |
| const p1x = srcCx + dir * inset; |
| const p2x = tgtCx - dir * inset; |
| return `M ${srcCx} ${y} C ${p1x} ${upY}, ${p2x} ${upY}, ${tgtCx} ${y}`; |
| }; |
|
|
| linkSel.each(function(d) { |
| const { src, tgt } = getLinkNodes(d); |
| d3.select(this) |
| .selectAll('path.gen-attr-dag-link-visible') |
| .attr('d', arcPathBetweenNodes(src, tgt)); |
| }); |
|
|
| nodeSel.attr('transform', (d) => { |
| const cx = centerXByNode.get(d); |
| if (cx === undefined) return null; |
| return `translate(${cx - d.nodeW / 2},${LINEAR_ARC_BASELINE_Y})`; |
| }); |
| } |
|
|