@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.
Files changed (161) hide show
  1. package/.eslintrc.json +6 -0
  2. package/CHANGELOG.md +45 -0
  3. package/dist/edges/collision.d.ts +8 -0
  4. package/dist/edges/collision.d.ts.map +1 -0
  5. package/dist/edges/collision.js +26 -0
  6. package/dist/edges/collision.js.map +1 -0
  7. package/dist/edges/geometry.d.ts +22 -0
  8. package/dist/edges/geometry.d.ts.map +1 -0
  9. package/dist/edges/geometry.js +75 -0
  10. package/dist/edges/geometry.js.map +1 -0
  11. package/dist/edges/index.d.ts +4 -0
  12. package/dist/edges/index.d.ts.map +1 -0
  13. package/dist/edges/index.js +4 -0
  14. package/dist/edges/index.js.map +1 -0
  15. package/dist/edges/pathBuilder.d.ts +11 -0
  16. package/dist/edges/pathBuilder.d.ts.map +1 -0
  17. package/dist/edges/pathBuilder.js +114 -0
  18. package/dist/edges/pathBuilder.js.map +1 -0
  19. package/dist/edges/pathCalculation.d.ts +14 -0
  20. package/dist/edges/pathCalculation.d.ts.map +1 -0
  21. package/dist/edges/pathCalculation.js +47 -0
  22. package/dist/edges/pathCalculation.js.map +1 -0
  23. package/dist/edges/routing.d.ts +6 -0
  24. package/dist/edges/routing.d.ts.map +1 -0
  25. package/dist/edges/routing.js +243 -0
  26. package/dist/edges/routing.js.map +1 -0
  27. package/dist/edges/sideSelection.d.ts +17 -0
  28. package/dist/edges/sideSelection.d.ts.map +1 -0
  29. package/dist/edges/sideSelection.js +45 -0
  30. package/dist/edges/sideSelection.js.map +1 -0
  31. package/dist/index.d.ts +7 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +6 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/layouts/centered.d.ts +6 -0
  36. package/dist/layouts/centered.d.ts.map +1 -0
  37. package/dist/layouts/centered.js +69 -0
  38. package/dist/layouts/centered.js.map +1 -0
  39. package/dist/layouts/compactBracket.d.ts +3 -0
  40. package/dist/layouts/compactBracket.d.ts.map +1 -0
  41. package/dist/layouts/compactBracket.js +7 -0
  42. package/dist/layouts/compactBracket.js.map +1 -0
  43. package/dist/layouts/dag.d.ts +3 -0
  44. package/dist/layouts/dag.d.ts.map +1 -0
  45. package/dist/layouts/dag.js +52 -0
  46. package/dist/layouts/dag.js.map +1 -0
  47. package/dist/layouts/forceDirected.d.ts +3 -0
  48. package/dist/layouts/forceDirected.d.ts.map +1 -0
  49. package/dist/layouts/forceDirected.js +176 -0
  50. package/dist/layouts/forceDirected.js.map +1 -0
  51. package/dist/layouts/grid.d.ts +6 -0
  52. package/dist/layouts/grid.d.ts.map +1 -0
  53. package/dist/layouts/grid.js +34 -0
  54. package/dist/layouts/grid.js.map +1 -0
  55. package/dist/layouts/index.d.ts +12 -0
  56. package/dist/layouts/index.d.ts.map +1 -0
  57. package/dist/layouts/index.js +88 -0
  58. package/dist/layouts/index.js.map +1 -0
  59. package/dist/layouts/orthogonalFlow.d.ts +3 -0
  60. package/dist/layouts/orthogonalFlow.d.ts.map +1 -0
  61. package/dist/layouts/orthogonalFlow.js +81 -0
  62. package/dist/layouts/orthogonalFlow.js.map +1 -0
  63. package/dist/layouts/radialTree.d.ts +3 -0
  64. package/dist/layouts/radialTree.d.ts.map +1 -0
  65. package/dist/layouts/radialTree.js +45 -0
  66. package/dist/layouts/radialTree.js.map +1 -0
  67. package/dist/layouts/tree.d.ts +6 -0
  68. package/dist/layouts/tree.d.ts.map +1 -0
  69. package/dist/layouts/tree.js +19 -0
  70. package/dist/layouts/tree.js.map +1 -0
  71. package/dist/layouts/treeAlignment.d.ts +10 -0
  72. package/dist/layouts/treeAlignment.d.ts.map +1 -0
  73. package/dist/layouts/treeAlignment.js +69 -0
  74. package/dist/layouts/treeAlignment.js.map +1 -0
  75. package/dist/layouts/treePositioning.d.ts +14 -0
  76. package/dist/layouts/treePositioning.d.ts.map +1 -0
  77. package/dist/layouts/treePositioning.js +30 -0
  78. package/dist/layouts/treePositioning.js.map +1 -0
  79. package/dist/layouts/treeTopology.d.ts +29 -0
  80. package/dist/layouts/treeTopology.d.ts.map +1 -0
  81. package/dist/layouts/treeTopology.js +137 -0
  82. package/dist/layouts/treeTopology.js.map +1 -0
  83. package/dist/rendering/defaultRenderers.d.ts +10 -0
  84. package/dist/rendering/defaultRenderers.d.ts.map +1 -0
  85. package/dist/rendering/defaultRenderers.js +85 -0
  86. package/dist/rendering/defaultRenderers.js.map +1 -0
  87. package/dist/rendering/index.d.ts +4 -0
  88. package/dist/rendering/index.d.ts.map +1 -0
  89. package/dist/rendering/index.js +4 -0
  90. package/dist/rendering/index.js.map +1 -0
  91. package/dist/rendering/svg.d.ts +7 -0
  92. package/dist/rendering/svg.d.ts.map +1 -0
  93. package/dist/rendering/svg.js +256 -0
  94. package/dist/rendering/svg.js.map +1 -0
  95. package/dist/rendering/utils.d.ts +5 -0
  96. package/dist/rendering/utils.d.ts.map +1 -0
  97. package/dist/rendering/utils.js +33 -0
  98. package/dist/rendering/utils.js.map +1 -0
  99. package/dist/utils/config.d.ts +36 -0
  100. package/dist/utils/config.d.ts.map +1 -0
  101. package/dist/utils/config.js +115 -0
  102. package/dist/utils/config.js.map +1 -0
  103. package/dist/utils/constants.d.ts +15 -0
  104. package/dist/utils/constants.d.ts.map +1 -0
  105. package/dist/utils/constants.js +19 -0
  106. package/dist/utils/constants.js.map +1 -0
  107. package/dist/utils/graphParser.d.ts +16 -0
  108. package/dist/utils/graphParser.d.ts.map +1 -0
  109. package/dist/utils/graphParser.js +277 -0
  110. package/dist/utils/graphParser.js.map +1 -0
  111. package/dist/utils/graphTraversal.d.ts +13 -0
  112. package/dist/utils/graphTraversal.d.ts.map +1 -0
  113. package/dist/utils/graphTraversal.js +28 -0
  114. package/dist/utils/graphTraversal.js.map +1 -0
  115. package/dist/utils/index.d.ts +8 -0
  116. package/dist/utils/index.d.ts.map +1 -0
  117. package/dist/utils/index.js +7 -0
  118. package/dist/utils/index.js.map +1 -0
  119. package/dist/utils/nodeMetrics.d.ts +8 -0
  120. package/dist/utils/nodeMetrics.d.ts.map +1 -0
  121. package/dist/utils/nodeMetrics.js +12 -0
  122. package/dist/utils/nodeMetrics.js.map +1 -0
  123. package/dist/utils/nodeSizing.d.ts +3 -0
  124. package/dist/utils/nodeSizing.d.ts.map +1 -0
  125. package/dist/utils/nodeSizing.js +77 -0
  126. package/dist/utils/nodeSizing.js.map +1 -0
  127. package/package.json +29 -0
  128. package/project.json +32 -0
  129. package/src/edges/collision.ts +31 -0
  130. package/src/edges/geometry.ts +85 -0
  131. package/src/edges/index.ts +3 -0
  132. package/src/edges/pathBuilder.ts +136 -0
  133. package/src/edges/pathCalculation.ts +69 -0
  134. package/src/edges/routing.ts +459 -0
  135. package/src/edges/sideSelection.ts +67 -0
  136. package/src/index.ts +50 -0
  137. package/src/layouts/centered.ts +114 -0
  138. package/src/layouts/compactBracket.ts +14 -0
  139. package/src/layouts/dag.ts +76 -0
  140. package/src/layouts/forceDirected.ts +224 -0
  141. package/src/layouts/grid.ts +50 -0
  142. package/src/layouts/index.ts +148 -0
  143. package/src/layouts/orthogonalFlow.ts +112 -0
  144. package/src/layouts/radialTree.ts +77 -0
  145. package/src/layouts/tree.ts +35 -0
  146. package/src/layouts/treeAlignment.ts +107 -0
  147. package/src/layouts/treePositioning.ts +55 -0
  148. package/src/layouts/treeTopology.ts +184 -0
  149. package/src/rendering/defaultRenderers.ts +110 -0
  150. package/src/rendering/index.ts +3 -0
  151. package/src/rendering/svg.ts +346 -0
  152. package/src/rendering/utils.ts +41 -0
  153. package/src/utils/config.ts +198 -0
  154. package/src/utils/constants.ts +24 -0
  155. package/src/utils/graphParser.ts +495 -0
  156. package/src/utils/graphTraversal.ts +32 -0
  157. package/src/utils/index.ts +19 -0
  158. package/src/utils/nodeMetrics.ts +23 -0
  159. package/src/utils/nodeSizing.ts +97 -0
  160. package/tsconfig.json +11 -0
  161. 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
+ };