@graph-render/core 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +6 -0
- package/CHANGELOG.md +45 -0
- package/dist/edges/collision.d.ts +8 -0
- package/dist/edges/collision.d.ts.map +1 -0
- package/dist/edges/collision.js +26 -0
- package/dist/edges/collision.js.map +1 -0
- package/dist/edges/geometry.d.ts +22 -0
- package/dist/edges/geometry.d.ts.map +1 -0
- package/dist/edges/geometry.js +75 -0
- package/dist/edges/geometry.js.map +1 -0
- package/dist/edges/index.d.ts +4 -0
- package/dist/edges/index.d.ts.map +1 -0
- package/dist/edges/index.js +4 -0
- package/dist/edges/index.js.map +1 -0
- package/dist/edges/pathBuilder.d.ts +11 -0
- package/dist/edges/pathBuilder.d.ts.map +1 -0
- package/dist/edges/pathBuilder.js +114 -0
- package/dist/edges/pathBuilder.js.map +1 -0
- package/dist/edges/pathCalculation.d.ts +14 -0
- package/dist/edges/pathCalculation.d.ts.map +1 -0
- package/dist/edges/pathCalculation.js +47 -0
- package/dist/edges/pathCalculation.js.map +1 -0
- package/dist/edges/routing.d.ts +6 -0
- package/dist/edges/routing.d.ts.map +1 -0
- package/dist/edges/routing.js +243 -0
- package/dist/edges/routing.js.map +1 -0
- package/dist/edges/sideSelection.d.ts +17 -0
- package/dist/edges/sideSelection.d.ts.map +1 -0
- package/dist/edges/sideSelection.js +45 -0
- package/dist/edges/sideSelection.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/layouts/centered.d.ts +6 -0
- package/dist/layouts/centered.d.ts.map +1 -0
- package/dist/layouts/centered.js +69 -0
- package/dist/layouts/centered.js.map +1 -0
- package/dist/layouts/compactBracket.d.ts +3 -0
- package/dist/layouts/compactBracket.d.ts.map +1 -0
- package/dist/layouts/compactBracket.js +7 -0
- package/dist/layouts/compactBracket.js.map +1 -0
- package/dist/layouts/dag.d.ts +3 -0
- package/dist/layouts/dag.d.ts.map +1 -0
- package/dist/layouts/dag.js +52 -0
- package/dist/layouts/dag.js.map +1 -0
- package/dist/layouts/forceDirected.d.ts +3 -0
- package/dist/layouts/forceDirected.d.ts.map +1 -0
- package/dist/layouts/forceDirected.js +176 -0
- package/dist/layouts/forceDirected.js.map +1 -0
- package/dist/layouts/grid.d.ts +6 -0
- package/dist/layouts/grid.d.ts.map +1 -0
- package/dist/layouts/grid.js +34 -0
- package/dist/layouts/grid.js.map +1 -0
- package/dist/layouts/index.d.ts +12 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/layouts/index.js +88 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/layouts/orthogonalFlow.d.ts +3 -0
- package/dist/layouts/orthogonalFlow.d.ts.map +1 -0
- package/dist/layouts/orthogonalFlow.js +81 -0
- package/dist/layouts/orthogonalFlow.js.map +1 -0
- package/dist/layouts/radialTree.d.ts +3 -0
- package/dist/layouts/radialTree.d.ts.map +1 -0
- package/dist/layouts/radialTree.js +45 -0
- package/dist/layouts/radialTree.js.map +1 -0
- package/dist/layouts/tree.d.ts +6 -0
- package/dist/layouts/tree.d.ts.map +1 -0
- package/dist/layouts/tree.js +19 -0
- package/dist/layouts/tree.js.map +1 -0
- package/dist/layouts/treeAlignment.d.ts +10 -0
- package/dist/layouts/treeAlignment.d.ts.map +1 -0
- package/dist/layouts/treeAlignment.js +69 -0
- package/dist/layouts/treeAlignment.js.map +1 -0
- package/dist/layouts/treePositioning.d.ts +14 -0
- package/dist/layouts/treePositioning.d.ts.map +1 -0
- package/dist/layouts/treePositioning.js +30 -0
- package/dist/layouts/treePositioning.js.map +1 -0
- package/dist/layouts/treeTopology.d.ts +29 -0
- package/dist/layouts/treeTopology.d.ts.map +1 -0
- package/dist/layouts/treeTopology.js +137 -0
- package/dist/layouts/treeTopology.js.map +1 -0
- package/dist/rendering/defaultRenderers.d.ts +10 -0
- package/dist/rendering/defaultRenderers.d.ts.map +1 -0
- package/dist/rendering/defaultRenderers.js +85 -0
- package/dist/rendering/defaultRenderers.js.map +1 -0
- package/dist/rendering/index.d.ts +4 -0
- package/dist/rendering/index.d.ts.map +1 -0
- package/dist/rendering/index.js +4 -0
- package/dist/rendering/index.js.map +1 -0
- package/dist/rendering/svg.d.ts +7 -0
- package/dist/rendering/svg.d.ts.map +1 -0
- package/dist/rendering/svg.js +256 -0
- package/dist/rendering/svg.js.map +1 -0
- package/dist/rendering/utils.d.ts +5 -0
- package/dist/rendering/utils.d.ts.map +1 -0
- package/dist/rendering/utils.js +33 -0
- package/dist/rendering/utils.js.map +1 -0
- package/dist/utils/config.d.ts +36 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +115 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/constants.d.ts +15 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +19 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/graphParser.d.ts +16 -0
- package/dist/utils/graphParser.d.ts.map +1 -0
- package/dist/utils/graphParser.js +277 -0
- package/dist/utils/graphParser.js.map +1 -0
- package/dist/utils/graphTraversal.d.ts +13 -0
- package/dist/utils/graphTraversal.d.ts.map +1 -0
- package/dist/utils/graphTraversal.js +28 -0
- package/dist/utils/graphTraversal.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/nodeMetrics.d.ts +8 -0
- package/dist/utils/nodeMetrics.d.ts.map +1 -0
- package/dist/utils/nodeMetrics.js +12 -0
- package/dist/utils/nodeMetrics.js.map +1 -0
- package/dist/utils/nodeSizing.d.ts +3 -0
- package/dist/utils/nodeSizing.d.ts.map +1 -0
- package/dist/utils/nodeSizing.js +77 -0
- package/dist/utils/nodeSizing.js.map +1 -0
- package/package.json +29 -0
- package/project.json +32 -0
- package/src/edges/collision.ts +31 -0
- package/src/edges/geometry.ts +85 -0
- package/src/edges/index.ts +3 -0
- package/src/edges/pathBuilder.ts +136 -0
- package/src/edges/pathCalculation.ts +69 -0
- package/src/edges/routing.ts +459 -0
- package/src/edges/sideSelection.ts +67 -0
- package/src/index.ts +50 -0
- package/src/layouts/centered.ts +114 -0
- package/src/layouts/compactBracket.ts +14 -0
- package/src/layouts/dag.ts +76 -0
- package/src/layouts/forceDirected.ts +224 -0
- package/src/layouts/grid.ts +50 -0
- package/src/layouts/index.ts +148 -0
- package/src/layouts/orthogonalFlow.ts +112 -0
- package/src/layouts/radialTree.ts +77 -0
- package/src/layouts/tree.ts +35 -0
- package/src/layouts/treeAlignment.ts +107 -0
- package/src/layouts/treePositioning.ts +55 -0
- package/src/layouts/treeTopology.ts +184 -0
- package/src/rendering/defaultRenderers.ts +110 -0
- package/src/rendering/index.ts +3 -0
- package/src/rendering/svg.ts +346 -0
- package/src/rendering/utils.ts +41 -0
- package/src/utils/config.ts +198 -0
- package/src/utils/constants.ts +24 -0
- package/src/utils/graphParser.ts +495 -0
- package/src/utils/graphTraversal.ts +32 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/nodeMetrics.ts +23 -0
- package/src/utils/nodeSizing.ts +97 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PositionedNode,
|
|
3
|
+
EdgeData,
|
|
4
|
+
PositionedEdge,
|
|
5
|
+
RouteEdgesOptions,
|
|
6
|
+
EdgeRoutingContext,
|
|
7
|
+
NodeSide,
|
|
8
|
+
Point,
|
|
9
|
+
Size,
|
|
10
|
+
EdgeType,
|
|
11
|
+
LayoutDirection,
|
|
12
|
+
} from '@graph-render/types';
|
|
13
|
+
import { DEFAULT_NODE_SIZE } from '../utils';
|
|
14
|
+
import { getNodeCenter, getAnchorPoint, getSideNormal, getSideInwardNormal } from './geometry';
|
|
15
|
+
import {
|
|
16
|
+
sortSidesByDistance,
|
|
17
|
+
applyDirectionalPreference,
|
|
18
|
+
findNonIntersectingSides,
|
|
19
|
+
} from './sideSelection';
|
|
20
|
+
import {
|
|
21
|
+
getLeadOutDistance,
|
|
22
|
+
calculateControlPoints,
|
|
23
|
+
calculateStraightPoints,
|
|
24
|
+
} from './pathCalculation';
|
|
25
|
+
|
|
26
|
+
type ParallelEdgeMeta = {
|
|
27
|
+
index: number;
|
|
28
|
+
total: number;
|
|
29
|
+
centeredOffset: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ORTHOGONAL_TERMINAL_SEGMENT = 20;
|
|
33
|
+
const MAX_COLLISION_SCAN_WORK = 20_000;
|
|
34
|
+
|
|
35
|
+
const getParallelGroupKey = (edge: EdgeData): string => {
|
|
36
|
+
const pair = [edge.source, edge.target].sort().join('|');
|
|
37
|
+
return `${pair}|${edge.type ?? EdgeType.Directed}`;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const buildParallelEdgeIndex = (edges: EdgeData[]): Map<string, ParallelEdgeMeta> => {
|
|
41
|
+
const groups = new Map<string, EdgeData[]>();
|
|
42
|
+
|
|
43
|
+
edges.forEach((edge) => {
|
|
44
|
+
const key = getParallelGroupKey(edge);
|
|
45
|
+
groups.set(key, [...(groups.get(key) ?? []), edge]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const meta = new Map<string, ParallelEdgeMeta>();
|
|
49
|
+
groups.forEach((group) => {
|
|
50
|
+
const total = group.length;
|
|
51
|
+
group.forEach((edge, index) => {
|
|
52
|
+
meta.set(edge.id, {
|
|
53
|
+
index,
|
|
54
|
+
total,
|
|
55
|
+
centeredOffset: index - (total - 1) / 2,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return meta;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const calculateLabelPosition = (points: Point[]): Point | undefined => {
|
|
64
|
+
if (points.length < 2) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const segmentLengths = points.slice(1).map((point, index) => {
|
|
69
|
+
const previous = points[index];
|
|
70
|
+
return Math.hypot(point.x - previous.x, point.y - previous.y);
|
|
71
|
+
});
|
|
72
|
+
const totalLength = segmentLengths.reduce((sum, length) => sum + length, 0);
|
|
73
|
+
const halfway = totalLength / 2;
|
|
74
|
+
let traversed = 0;
|
|
75
|
+
|
|
76
|
+
for (let index = 0; index < segmentLengths.length; index += 1) {
|
|
77
|
+
const length = segmentLengths[index];
|
|
78
|
+
if (traversed + length >= halfway) {
|
|
79
|
+
const start = points[index];
|
|
80
|
+
const end = points[index + 1];
|
|
81
|
+
const ratio = length === 0 ? 0 : (halfway - traversed) / length;
|
|
82
|
+
return {
|
|
83
|
+
x: start.x + (end.x - start.x) * ratio,
|
|
84
|
+
y: start.y + (end.y - start.y) * ratio,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
traversed += length;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return points[Math.floor(points.length / 2)];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const createSelfLoopPoints = (
|
|
94
|
+
node: PositionedNode,
|
|
95
|
+
size: Size,
|
|
96
|
+
loopRadius: number,
|
|
97
|
+
offset: number
|
|
98
|
+
): Point[] => {
|
|
99
|
+
const right = node.position.x + size.width;
|
|
100
|
+
const top = node.position.y;
|
|
101
|
+
const anchorX = right - Math.min(size.width * 0.2, 16);
|
|
102
|
+
const anchorY = top + Math.min(size.height * 0.3, 20);
|
|
103
|
+
const loopX = right + loopRadius + offset;
|
|
104
|
+
const loopTop = top - loopRadius - Math.abs(offset) * 0.4;
|
|
105
|
+
const loopBottom = top + size.height * 0.75 + Math.abs(offset) * 0.3;
|
|
106
|
+
|
|
107
|
+
return [
|
|
108
|
+
{ x: anchorX, y: anchorY },
|
|
109
|
+
{ x: loopX * 0.92, y: loopTop },
|
|
110
|
+
{ x: loopX, y: loopTop },
|
|
111
|
+
{ x: loopX, y: loopBottom },
|
|
112
|
+
{ x: anchorX, y: top + size.height * 0.82 },
|
|
113
|
+
];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const applyParallelOffset = (
|
|
117
|
+
points: Point[],
|
|
118
|
+
sourceCenter: Point,
|
|
119
|
+
targetCenter: Point,
|
|
120
|
+
offset: number
|
|
121
|
+
): Point[] => {
|
|
122
|
+
if (Math.abs(offset) < 0.01) {
|
|
123
|
+
return points;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const dx = targetCenter.x - sourceCenter.x;
|
|
127
|
+
const dy = targetCenter.y - sourceCenter.y;
|
|
128
|
+
const distance = Math.max(1, Math.hypot(dx, dy));
|
|
129
|
+
const normal = { x: -dy / distance, y: dx / distance };
|
|
130
|
+
|
|
131
|
+
return points.map((point) => ({
|
|
132
|
+
x: point.x + normal.x * offset,
|
|
133
|
+
y: point.y + normal.y * offset,
|
|
134
|
+
}));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const calculateOrthogonalPoints = (
|
|
138
|
+
startPoint: Point,
|
|
139
|
+
endPoint: Point,
|
|
140
|
+
sourceCenter: Point,
|
|
141
|
+
targetCenter: Point,
|
|
142
|
+
routingStyle: 'orthogonal' | 'bundled',
|
|
143
|
+
parallelOffset: number,
|
|
144
|
+
sourceSide: NodeSide,
|
|
145
|
+
targetSide: NodeSide
|
|
146
|
+
): Point[] => {
|
|
147
|
+
const sourceNormal = getSideNormal(sourceSide);
|
|
148
|
+
const targetNormal = getSideNormal(targetSide);
|
|
149
|
+
const startLead = {
|
|
150
|
+
x: startPoint.x + sourceNormal.x * ORTHOGONAL_TERMINAL_SEGMENT,
|
|
151
|
+
y: startPoint.y + sourceNormal.y * ORTHOGONAL_TERMINAL_SEGMENT,
|
|
152
|
+
};
|
|
153
|
+
const endLead = {
|
|
154
|
+
x: endPoint.x + targetNormal.x * ORTHOGONAL_TERMINAL_SEGMENT,
|
|
155
|
+
y: endPoint.y + targetNormal.y * ORTHOGONAL_TERMINAL_SEGMENT,
|
|
156
|
+
};
|
|
157
|
+
const dx = endLead.x - startLead.x;
|
|
158
|
+
const dy = endLead.y - startLead.y;
|
|
159
|
+
const sourceIsHorizontal = sourceNormal.x !== 0;
|
|
160
|
+
const targetIsHorizontal = targetNormal.x !== 0;
|
|
161
|
+
const sourceIsVertical = sourceNormal.y !== 0;
|
|
162
|
+
const targetIsVertical = targetNormal.y !== 0;
|
|
163
|
+
const preferHorizontalRun =
|
|
164
|
+
(sourceIsHorizontal && targetIsHorizontal) ||
|
|
165
|
+
(!(sourceIsVertical && targetIsVertical) && Math.abs(dx) >= Math.abs(dy));
|
|
166
|
+
|
|
167
|
+
if (preferHorizontalRun) {
|
|
168
|
+
const midX =
|
|
169
|
+
routingStyle === 'bundled'
|
|
170
|
+
? (sourceCenter.x + targetCenter.x) / 2 + parallelOffset * 0.5
|
|
171
|
+
: startLead.x + dx / 2;
|
|
172
|
+
|
|
173
|
+
return [
|
|
174
|
+
startPoint,
|
|
175
|
+
startLead,
|
|
176
|
+
{ x: midX, y: startLead.y },
|
|
177
|
+
{ x: midX, y: endLead.y },
|
|
178
|
+
endLead,
|
|
179
|
+
endPoint,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const midY =
|
|
184
|
+
routingStyle === 'bundled'
|
|
185
|
+
? (sourceCenter.y + targetCenter.y) / 2 + parallelOffset * 0.5
|
|
186
|
+
: startLead.y + dy / 2;
|
|
187
|
+
|
|
188
|
+
return [
|
|
189
|
+
startPoint,
|
|
190
|
+
startLead,
|
|
191
|
+
{ x: startLead.x, y: midY },
|
|
192
|
+
{ x: endLead.x, y: midY },
|
|
193
|
+
endLead,
|
|
194
|
+
endPoint,
|
|
195
|
+
];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create routing context for an edge
|
|
200
|
+
*/
|
|
201
|
+
const createRoutingContext = (
|
|
202
|
+
source: PositionedNode,
|
|
203
|
+
target: PositionedNode,
|
|
204
|
+
sourceSize: Size,
|
|
205
|
+
targetSize: Size,
|
|
206
|
+
nodes: PositionedNode[],
|
|
207
|
+
useObstacleAvoidance: boolean,
|
|
208
|
+
isUndirected: boolean,
|
|
209
|
+
arrowPadding: number,
|
|
210
|
+
straight: boolean,
|
|
211
|
+
forceRightToLeft: boolean,
|
|
212
|
+
layoutDirection: LayoutDirection,
|
|
213
|
+
routingStyle: 'smart' | 'orthogonal' | 'bundled',
|
|
214
|
+
edgeSeparation: number,
|
|
215
|
+
selfLoopRadius: number
|
|
216
|
+
): EdgeRoutingContext => {
|
|
217
|
+
return {
|
|
218
|
+
source,
|
|
219
|
+
target,
|
|
220
|
+
sourceSize,
|
|
221
|
+
targetSize,
|
|
222
|
+
isUndirected,
|
|
223
|
+
arrowPadding,
|
|
224
|
+
straight,
|
|
225
|
+
forceRightToLeft,
|
|
226
|
+
layoutDirection,
|
|
227
|
+
routingStyle,
|
|
228
|
+
edgeSeparation,
|
|
229
|
+
selfLoopRadius,
|
|
230
|
+
otherRects: useObstacleAvoidance
|
|
231
|
+
? nodes
|
|
232
|
+
.filter((n) => n.id !== source.id && n.id !== target.id)
|
|
233
|
+
.map((n) => ({
|
|
234
|
+
x: n.position.x,
|
|
235
|
+
y: n.position.y,
|
|
236
|
+
w: n.size?.width ?? DEFAULT_NODE_SIZE.width,
|
|
237
|
+
h: n.size?.height ?? DEFAULT_NODE_SIZE.height,
|
|
238
|
+
}))
|
|
239
|
+
: [],
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Find the best connection sides between source and target nodes
|
|
245
|
+
*/
|
|
246
|
+
const findConnectionSides = (
|
|
247
|
+
source: PositionedNode,
|
|
248
|
+
target: PositionedNode,
|
|
249
|
+
sourceSize: Size,
|
|
250
|
+
targetSize: Size,
|
|
251
|
+
context: EdgeRoutingContext,
|
|
252
|
+
isDirected: boolean
|
|
253
|
+
): { sourceSide: NodeSide; targetSide: NodeSide } => {
|
|
254
|
+
// If forceRightToLeft is enabled, always use right side for source and left side for target
|
|
255
|
+
if (context.forceRightToLeft) {
|
|
256
|
+
return { sourceSide: NodeSide.Right, targetSide: NodeSide.Left };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const srcCenter = getNodeCenter(source, sourceSize);
|
|
260
|
+
const tgtCenter = getNodeCenter(target, targetSize);
|
|
261
|
+
|
|
262
|
+
const sortedTargetSides = sortSidesByDistance(target, targetSize, srcCenter);
|
|
263
|
+
const sortedSourceSidesBase = sortSidesByDistance(source, sourceSize, tgtCenter);
|
|
264
|
+
const sortedSourceSides = applyDirectionalPreference(
|
|
265
|
+
sortedSourceSidesBase,
|
|
266
|
+
isDirected,
|
|
267
|
+
context.layoutDirection
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return findNonIntersectingSides(context, sortedSourceSides, sortedTargetSides);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Calculate edge path points based on connection sides
|
|
275
|
+
*/
|
|
276
|
+
const calculateEdgePoints = (
|
|
277
|
+
source: PositionedNode,
|
|
278
|
+
target: PositionedNode,
|
|
279
|
+
sourceSize: Size,
|
|
280
|
+
targetSize: Size,
|
|
281
|
+
sourceSide: NodeSide,
|
|
282
|
+
targetSide: NodeSide,
|
|
283
|
+
isUndirected: boolean,
|
|
284
|
+
arrowPadding: number,
|
|
285
|
+
straight: boolean,
|
|
286
|
+
routingStyle: 'smart' | 'orthogonal' | 'bundled',
|
|
287
|
+
parallelOffset: number
|
|
288
|
+
): Point[] => {
|
|
289
|
+
const targetInset = isUndirected ? 0 : arrowPadding;
|
|
290
|
+
const startPoint = getAnchorPoint(source, sourceSize, sourceSide, 0, 0);
|
|
291
|
+
const endPoint = getAnchorPoint(target, targetSize, targetSide, 0, targetInset);
|
|
292
|
+
const sourceCenter = getNodeCenter(source, sourceSize);
|
|
293
|
+
const targetCenter = getNodeCenter(target, targetSize);
|
|
294
|
+
|
|
295
|
+
if (routingStyle === 'orthogonal' || routingStyle === 'bundled') {
|
|
296
|
+
return calculateOrthogonalPoints(
|
|
297
|
+
startPoint,
|
|
298
|
+
endPoint,
|
|
299
|
+
sourceCenter,
|
|
300
|
+
targetCenter,
|
|
301
|
+
routingStyle,
|
|
302
|
+
parallelOffset,
|
|
303
|
+
sourceSide,
|
|
304
|
+
targetSide
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const sourceNormal = getSideNormal(sourceSide);
|
|
309
|
+
const targetNormal = getSideInwardNormal(targetSide);
|
|
310
|
+
const leadOut = getLeadOutDistance(straight, isUndirected);
|
|
311
|
+
|
|
312
|
+
const points = straight
|
|
313
|
+
? calculateStraightPoints(
|
|
314
|
+
startPoint,
|
|
315
|
+
endPoint,
|
|
316
|
+
sourceNormal,
|
|
317
|
+
targetNormal,
|
|
318
|
+
leadOut,
|
|
319
|
+
isUndirected
|
|
320
|
+
)
|
|
321
|
+
: calculateControlPoints(
|
|
322
|
+
startPoint,
|
|
323
|
+
endPoint,
|
|
324
|
+
sourceNormal,
|
|
325
|
+
targetNormal,
|
|
326
|
+
leadOut,
|
|
327
|
+
isUndirected
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return applyParallelOffset(points, sourceCenter, targetCenter, parallelOffset);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Route a single edge between two nodes
|
|
335
|
+
*/
|
|
336
|
+
const routeSingleEdge = (
|
|
337
|
+
edge: EdgeData,
|
|
338
|
+
nodeMap: Map<string, PositionedNode>,
|
|
339
|
+
nodes: PositionedNode[],
|
|
340
|
+
useObstacleAvoidance: boolean,
|
|
341
|
+
arrowPadding: number,
|
|
342
|
+
straight: boolean,
|
|
343
|
+
forceRightToLeft: boolean,
|
|
344
|
+
layoutDirection: LayoutDirection,
|
|
345
|
+
routingStyle: 'smart' | 'orthogonal' | 'bundled',
|
|
346
|
+
edgeSeparation: number,
|
|
347
|
+
selfLoopRadius: number,
|
|
348
|
+
parallelMeta: ParallelEdgeMeta
|
|
349
|
+
): PositionedEdge => {
|
|
350
|
+
const source = nodeMap.get(edge.source);
|
|
351
|
+
const target = nodeMap.get(edge.target);
|
|
352
|
+
|
|
353
|
+
if (!source || !target) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Cannot route edge "${edge.id}" because endpoint nodes are missing. Source: "${edge.source}", target: "${edge.target}".`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const isUndirected = edge.type === EdgeType.Undirected;
|
|
360
|
+
const isDirected = edge.type === EdgeType.Directed;
|
|
361
|
+
const sourceSize = source.size ?? DEFAULT_NODE_SIZE;
|
|
362
|
+
const targetSize = target.size ?? DEFAULT_NODE_SIZE;
|
|
363
|
+
const parallelOffset = parallelMeta.centeredOffset * edgeSeparation;
|
|
364
|
+
|
|
365
|
+
if (source.id === target.id) {
|
|
366
|
+
const points = createSelfLoopPoints(source, sourceSize, selfLoopRadius, parallelOffset);
|
|
367
|
+
return {
|
|
368
|
+
...edge,
|
|
369
|
+
points: edge.points ?? points,
|
|
370
|
+
labelPosition: calculateLabelPosition(edge.points ?? points),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const context = createRoutingContext(
|
|
375
|
+
source,
|
|
376
|
+
target,
|
|
377
|
+
sourceSize,
|
|
378
|
+
targetSize,
|
|
379
|
+
nodes,
|
|
380
|
+
useObstacleAvoidance,
|
|
381
|
+
isUndirected,
|
|
382
|
+
arrowPadding,
|
|
383
|
+
straight,
|
|
384
|
+
forceRightToLeft,
|
|
385
|
+
layoutDirection,
|
|
386
|
+
routingStyle,
|
|
387
|
+
edgeSeparation,
|
|
388
|
+
selfLoopRadius
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const { sourceSide, targetSide } = findConnectionSides(
|
|
392
|
+
source,
|
|
393
|
+
target,
|
|
394
|
+
sourceSize,
|
|
395
|
+
targetSize,
|
|
396
|
+
context,
|
|
397
|
+
isDirected
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const defaultPoints = calculateEdgePoints(
|
|
401
|
+
source,
|
|
402
|
+
target,
|
|
403
|
+
sourceSize,
|
|
404
|
+
targetSize,
|
|
405
|
+
sourceSide,
|
|
406
|
+
targetSide,
|
|
407
|
+
isUndirected,
|
|
408
|
+
arrowPadding,
|
|
409
|
+
straight,
|
|
410
|
+
routingStyle,
|
|
411
|
+
parallelOffset
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const points = edge.points ?? defaultPoints;
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
...edge,
|
|
418
|
+
points,
|
|
419
|
+
labelPosition: calculateLabelPosition(points),
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Route edges between nodes, calculating the path points for each edge
|
|
425
|
+
*/
|
|
426
|
+
export const routeEdges = (
|
|
427
|
+
nodes: PositionedNode[],
|
|
428
|
+
edges: EdgeData[],
|
|
429
|
+
opts?: RouteEdgesOptions
|
|
430
|
+
): PositionedEdge[] => {
|
|
431
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
432
|
+
const arrowPadding = Math.max(2, opts?.arrowPadding ?? 6);
|
|
433
|
+
const straight = opts?.straight ?? false;
|
|
434
|
+
const forceRightToLeft = opts?.forceRightToLeft ?? false;
|
|
435
|
+
const layoutDirection = opts?.layoutDirection ?? LayoutDirection.LTR;
|
|
436
|
+
const routingStyle = opts?.routingStyle ?? 'smart';
|
|
437
|
+
const edgeSeparation = Math.max(6, opts?.edgeSeparation ?? 18);
|
|
438
|
+
const selfLoopRadius = Math.max(12, opts?.selfLoopRadius ?? 32);
|
|
439
|
+
const parallelIndex = buildParallelEdgeIndex(edges);
|
|
440
|
+
const useObstacleAvoidance = nodes.length * edges.length <= MAX_COLLISION_SCAN_WORK;
|
|
441
|
+
|
|
442
|
+
return edges
|
|
443
|
+
.map((edge) =>
|
|
444
|
+
routeSingleEdge(
|
|
445
|
+
edge,
|
|
446
|
+
nodeMap,
|
|
447
|
+
nodes,
|
|
448
|
+
useObstacleAvoidance,
|
|
449
|
+
arrowPadding,
|
|
450
|
+
straight,
|
|
451
|
+
forceRightToLeft,
|
|
452
|
+
layoutDirection,
|
|
453
|
+
routingStyle,
|
|
454
|
+
edgeSeparation,
|
|
455
|
+
selfLoopRadius,
|
|
456
|
+
parallelIndex.get(edge.id) ?? { index: 0, total: 1, centeredOffset: 0 }
|
|
457
|
+
)
|
|
458
|
+
);
|
|
459
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PositionedNode,
|
|
3
|
+
Point,
|
|
4
|
+
Size,
|
|
5
|
+
NodeSide,
|
|
6
|
+
EdgeRoutingContext,
|
|
7
|
+
LayoutDirection,
|
|
8
|
+
} from '@graph-render/types';
|
|
9
|
+
import { getSideCenter } from './geometry';
|
|
10
|
+
import { segmentIntersectsRect } from './collision';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sort node sides by distance to a target point
|
|
14
|
+
*/
|
|
15
|
+
export const sortSidesByDistance = (
|
|
16
|
+
node: PositionedNode,
|
|
17
|
+
size: Size,
|
|
18
|
+
targetPoint: Point
|
|
19
|
+
): NodeSide[] => {
|
|
20
|
+
const sides: NodeSide[] = [NodeSide.Left, NodeSide.Right, NodeSide.Top, NodeSide.Bottom];
|
|
21
|
+
return [...sides].sort((a, b) => {
|
|
22
|
+
const ca = getSideCenter(node, size, a);
|
|
23
|
+
const cb = getSideCenter(node, size, b);
|
|
24
|
+
const da = Math.hypot(ca.x - targetPoint.x, ca.y - targetPoint.y);
|
|
25
|
+
const db = Math.hypot(cb.x - targetPoint.x, cb.y - targetPoint.y);
|
|
26
|
+
return da - db;
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Apply directional preference for source sides based on layout flow.
|
|
32
|
+
*/
|
|
33
|
+
export const applyDirectionalPreference = (
|
|
34
|
+
sides: NodeSide[],
|
|
35
|
+
isDirected: boolean,
|
|
36
|
+
layoutDirection: LayoutDirection = LayoutDirection.LTR
|
|
37
|
+
): NodeSide[] => {
|
|
38
|
+
if (!isDirected) return sides;
|
|
39
|
+
|
|
40
|
+
const preferredSide = layoutDirection === LayoutDirection.RTL ? NodeSide.Left : NodeSide.Right;
|
|
41
|
+
|
|
42
|
+
return [...sides].sort((a, b) => {
|
|
43
|
+
const weight = (side: NodeSide) => (side === preferredSide ? 0 : 1);
|
|
44
|
+
return weight(a) - weight(b);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find the best non-intersecting pair of sides between source and target
|
|
50
|
+
*/
|
|
51
|
+
export const findNonIntersectingSides = (
|
|
52
|
+
context: EdgeRoutingContext,
|
|
53
|
+
sortedSourceSides: NodeSide[],
|
|
54
|
+
sortedTargetSides: NodeSide[]
|
|
55
|
+
): { sourceSide: NodeSide; targetSide: NodeSide } => {
|
|
56
|
+
for (const s of sortedSourceSides) {
|
|
57
|
+
for (const t of sortedTargetSides) {
|
|
58
|
+
const start = getSideCenter(context.source, context.sourceSize, s);
|
|
59
|
+
const end = getSideCenter(context.target, context.targetSize, t);
|
|
60
|
+
if (!context.otherRects.some((r) => segmentIntersectsRect(start, end, r))) {
|
|
61
|
+
return { sourceSide: s, targetSide: t };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Return first options if no non-intersecting pair found
|
|
66
|
+
return { sourceSide: sortedSourceSides[0], targetSide: sortedTargetSides[0] };
|
|
67
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export { EdgeType, LayoutType, LayoutDirection, NodeSide } from '@graph-render/types';
|
|
2
|
+
export type {
|
|
3
|
+
DragState,
|
|
4
|
+
EdgeComponent,
|
|
5
|
+
EdgeData,
|
|
6
|
+
EdgeId,
|
|
7
|
+
EdgePathProps,
|
|
8
|
+
EdgeRenderer,
|
|
9
|
+
EdgeRoutingContext,
|
|
10
|
+
GraphConfig,
|
|
11
|
+
GraphControlsPosition,
|
|
12
|
+
GraphHandle,
|
|
13
|
+
GraphHoverMeta,
|
|
14
|
+
GraphProps,
|
|
15
|
+
GraphRenderContext,
|
|
16
|
+
GraphSearchResults,
|
|
17
|
+
GraphSelection,
|
|
18
|
+
GraphTheme,
|
|
19
|
+
GraphTopology,
|
|
20
|
+
GraphViewport,
|
|
21
|
+
LayoutOptions,
|
|
22
|
+
NodeData,
|
|
23
|
+
NodeId,
|
|
24
|
+
NodeMeasurementHints,
|
|
25
|
+
NodeRenderer,
|
|
26
|
+
NodeSizingMode,
|
|
27
|
+
NxEdgeAttrs,
|
|
28
|
+
NxGraphInput,
|
|
29
|
+
NxNodeAttrs,
|
|
30
|
+
PathHoverOptions,
|
|
31
|
+
PathTraversalOptions,
|
|
32
|
+
PathTraversalResult,
|
|
33
|
+
Point,
|
|
34
|
+
PositionedEdge,
|
|
35
|
+
PositionedNode,
|
|
36
|
+
RenderConfig,
|
|
37
|
+
RenderGraphToSvgOptions,
|
|
38
|
+
RenderGraphToSvgResult,
|
|
39
|
+
RenderTheme,
|
|
40
|
+
RouteEdgesOptions,
|
|
41
|
+
Size,
|
|
42
|
+
TreeMetrics,
|
|
43
|
+
VertexComponent,
|
|
44
|
+
VertexComponentProps,
|
|
45
|
+
} from '@graph-render/types';
|
|
46
|
+
|
|
47
|
+
export * from './rendering';
|
|
48
|
+
export * from './layouts';
|
|
49
|
+
export * from './edges';
|
|
50
|
+
export * from './utils';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { NodeData, Point, PositionedNode } from '@graph-render/types';
|
|
2
|
+
import { DEFAULT_NODE_SIZE, DEFAULT_PADDING, getMaxNodeDimensions } from '../utils';
|
|
3
|
+
import { gridLayout } from './grid';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Calculate the center point of the container
|
|
7
|
+
*/
|
|
8
|
+
const getContainerCenter = (width: number, height: number): Point => {
|
|
9
|
+
return {
|
|
10
|
+
x: width / 2,
|
|
11
|
+
y: height / 2,
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Calculate radius for circular layout
|
|
17
|
+
*/
|
|
18
|
+
const calculateCircleRadius = (
|
|
19
|
+
width: number,
|
|
20
|
+
height: number,
|
|
21
|
+
padding: number,
|
|
22
|
+
maxNodeWidth: number,
|
|
23
|
+
maxNodeHeight: number
|
|
24
|
+
): number => {
|
|
25
|
+
return Math.max(
|
|
26
|
+
0,
|
|
27
|
+
Math.min(width, height) / 2 - padding - Math.max(maxNodeWidth, maxNodeHeight) / 2
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Position a single node in the center
|
|
33
|
+
*/
|
|
34
|
+
const positionSingleNode = (node: NodeData, centerX: number, centerY: number): PositionedNode => {
|
|
35
|
+
const nodeWidth = node.size?.width ?? DEFAULT_NODE_SIZE.width;
|
|
36
|
+
const nodeHeight = node.size?.height ?? DEFAULT_NODE_SIZE.height;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
...node,
|
|
40
|
+
position: {
|
|
41
|
+
x: centerX - nodeWidth / 2,
|
|
42
|
+
y: centerY - nodeHeight / 2,
|
|
43
|
+
},
|
|
44
|
+
} as PositionedNode;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Calculate position on a circle for a node at given index
|
|
49
|
+
*/
|
|
50
|
+
const calculateCircularPosition = (
|
|
51
|
+
index: number,
|
|
52
|
+
total: number,
|
|
53
|
+
centerX: number,
|
|
54
|
+
centerY: number,
|
|
55
|
+
radius: number,
|
|
56
|
+
nodeWidth: number,
|
|
57
|
+
nodeHeight: number
|
|
58
|
+
): Point => {
|
|
59
|
+
const angle = (2 * Math.PI * index) / total - Math.PI / 2;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
x: centerX + radius * Math.cos(angle) - nodeWidth / 2,
|
|
63
|
+
y: centerY + radius * Math.sin(angle) - nodeHeight / 2,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Layout nodes in a circular pattern around the center
|
|
69
|
+
*/
|
|
70
|
+
export const centeredLayout = (
|
|
71
|
+
nodes: NodeData[],
|
|
72
|
+
pad: number = DEFAULT_PADDING,
|
|
73
|
+
width: number = 960,
|
|
74
|
+
height: number = 720
|
|
75
|
+
): PositionedNode[] => {
|
|
76
|
+
const count = nodes.length;
|
|
77
|
+
|
|
78
|
+
if (count === 0) {
|
|
79
|
+
return [] as PositionedNode[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { x: centerX, y: centerY } = getContainerCenter(width, height);
|
|
83
|
+
|
|
84
|
+
if (count === 1) {
|
|
85
|
+
return [positionSingleNode(nodes[0], centerX, centerY)];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { maxWidth, maxHeight } = getMaxNodeDimensions(nodes);
|
|
89
|
+
const radius = calculateCircleRadius(width, height, pad, maxWidth, maxHeight);
|
|
90
|
+
|
|
91
|
+
if (radius === 0) {
|
|
92
|
+
return gridLayout(nodes, pad, Math.max(24, Math.min(maxWidth, maxHeight) * 0.25));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return nodes.map((node, index) => {
|
|
96
|
+
if (node.position) {
|
|
97
|
+
return node as PositionedNode;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nodeWidth = node.size?.width ?? DEFAULT_NODE_SIZE.width;
|
|
101
|
+
const nodeHeight = node.size?.height ?? DEFAULT_NODE_SIZE.height;
|
|
102
|
+
const position = calculateCircularPosition(
|
|
103
|
+
index,
|
|
104
|
+
count,
|
|
105
|
+
centerX,
|
|
106
|
+
centerY,
|
|
107
|
+
radius,
|
|
108
|
+
nodeWidth,
|
|
109
|
+
nodeHeight
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return { ...node, position } as PositionedNode;
|
|
113
|
+
});
|
|
114
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EdgeData, LayoutDirection, NodeData, PositionedNode } from '@graph-render/types';
|
|
2
|
+
import { DEFAULT_NODE_GAP, DEFAULT_PADDING } from '../utils';
|
|
3
|
+
import { treeLayout } from './tree';
|
|
4
|
+
|
|
5
|
+
export const compactBracketLayout = (
|
|
6
|
+
nodes: NodeData[],
|
|
7
|
+
edges: EdgeData[],
|
|
8
|
+
pad: number = DEFAULT_PADDING,
|
|
9
|
+
gap: number = DEFAULT_NODE_GAP,
|
|
10
|
+
direction: LayoutDirection = LayoutDirection.LTR,
|
|
11
|
+
height?: number
|
|
12
|
+
): PositionedNode[] => {
|
|
13
|
+
return treeLayout(nodes, edges, pad, Math.max(28, gap * 0.55), direction, height);
|
|
14
|
+
};
|