@truedat/core 8.11.5 → 8.11.7

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.
@@ -1,12 +1,40 @@
1
- import { memo, useState } from "react";
2
- import { BaseEdge, EdgeLabelRenderer } from "@xyflow/react";
1
+ import {
2
+ createContext,
3
+ memo,
4
+ useContext,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import { BaseEdge, EdgeLabelRenderer, getBezierPath } from "@xyflow/react";
3
10
  import { getTargetSlotOffset } from "./edgeLayout";
4
11
 
12
+ export const EdgeHoverContext = createContext(null);
13
+
5
14
  const MIN_CURVE_OFFSET = 14;
6
15
  const MAX_CURVE_OFFSET = 38;
16
+ const ORTHOGONAL_EDGE_CLEARANCE = 24;
17
+ const ORTHOGONAL_EDGE_RADIUS = 8;
18
+ const ORTHOGONAL_LANE_GAP = 10;
19
+ const PORT_VERTICAL_INSET = 16;
7
20
  const EDGE_ARROW_LENGTH = 10;
8
21
  const EDGE_ARROW_WIDTH = 8;
9
22
  const EDGE_START_DOT_RADIUS = 3.5;
23
+ const EDGE_FLOW_SPEED_PX_PER_SECOND = 40;
24
+ const clampNumber = (value, min, max) => Math.max(min, Math.min(max, value));
25
+
26
+ export const edgeAnimationDuration = (
27
+ pathLength,
28
+ pixelsPerSecond = EDGE_FLOW_SPEED_PX_PER_SECOND,
29
+ ) => {
30
+ const length = Number(pathLength);
31
+ const speed = Number(pixelsPerSecond);
32
+
33
+ if (!Number.isFinite(length) || length <= 0) return null;
34
+ if (!Number.isFinite(speed) || speed <= 0) return null;
35
+
36
+ return length / speed;
37
+ };
10
38
 
11
39
  const getCurveOffset = ({ sourceX, sourceY, targetX, targetY }) => {
12
40
  const horizontalDistance = Math.abs(targetX - sourceX);
@@ -52,120 +80,728 @@ const getBezierDerivative = (t, p0, p1, p2, p3) => {
52
80
  );
53
81
  };
54
82
 
83
+ const portVector = (position) => {
84
+ switch (position) {
85
+ case "left":
86
+ return { x: -1, y: 0 };
87
+ case "right":
88
+ return { x: 1, y: 0 };
89
+ case "top":
90
+ return { x: 0, y: -1 };
91
+ case "bottom":
92
+ return { x: 0, y: 1 };
93
+ default:
94
+ return { x: 1, y: 0 };
95
+ }
96
+ };
97
+
55
98
  const normalizeVector = (x, y) => {
56
99
  const length = Math.hypot(x, y) || 1;
57
100
 
58
101
  return { x: x / length, y: y / length };
59
102
  };
60
103
 
104
+ const point = (x, y) => ({ x, y });
105
+
106
+ const dedupePoints = (points) =>
107
+ points.filter(
108
+ (currentPoint, index) =>
109
+ index === 0 ||
110
+ currentPoint.x !== points[index - 1].x ||
111
+ currentPoint.y !== points[index - 1].y,
112
+ );
113
+
114
+ const distance = (pointA, pointB) =>
115
+ Math.hypot(pointB.x - pointA.x, pointB.y - pointA.y);
116
+
117
+ const pointToward = (from, to, amount) => {
118
+ const segmentLength = distance(from, to);
119
+
120
+ if (!segmentLength) return from;
121
+
122
+ const ratio = amount / segmentLength;
123
+
124
+ return {
125
+ x: from.x + (to.x - from.x) * ratio,
126
+ y: from.y + (to.y - from.y) * ratio,
127
+ };
128
+ };
129
+
130
+ const roundedOrthogonalPath = (points, radius) => {
131
+ if (points.length < 2) return "";
132
+
133
+ const start = `M ${points[0].x},${points[0].y}`;
134
+ const segments = points.slice(1, -1).map((corner, idx) => {
135
+ const previous = points[idx];
136
+ const next = points[idx + 2];
137
+ const cornerRadius = Math.min(
138
+ radius,
139
+ distance(previous, corner) / 2,
140
+ distance(corner, next) / 2,
141
+ );
142
+
143
+ if (!cornerRadius) {
144
+ return ` L ${corner.x},${corner.y}`;
145
+ }
146
+
147
+ const cornerStart = pointToward(corner, previous, cornerRadius);
148
+ const cornerEnd = pointToward(corner, next, cornerRadius);
149
+
150
+ return ` L ${cornerStart.x},${cornerStart.y} Q ${corner.x},${corner.y} ${cornerEnd.x},${cornerEnd.y}`;
151
+ });
152
+
153
+ const lastPoint = points[points.length - 1];
154
+ return [start, ...segments, ` L ${lastPoint.x},${lastPoint.y}`].join("");
155
+ };
156
+
157
+ const firstSegmentTangent = (points, fallback) => {
158
+ const firstValidIndex = points.findIndex((point, index) => {
159
+ if (index === 0) return false;
160
+ const previous = points[index - 1];
161
+ return previous.x !== point.x || previous.y !== point.y;
162
+ });
163
+
164
+ if (firstValidIndex === -1) return fallback;
165
+
166
+ const previous = points[firstValidIndex - 1];
167
+ const current = points[firstValidIndex];
168
+ return normalizeVector(current.x - previous.x, current.y - previous.y);
169
+ };
170
+
171
+ const lastSegmentTangent = (points, fallback) => {
172
+ const lastValidIndex = points.reduce((acc, point, index) => {
173
+ if (index === 0) return acc;
174
+ const previous = points[index - 1];
175
+ if (previous.x !== point.x || previous.y !== point.y) return index;
176
+ return acc;
177
+ }, -1);
178
+
179
+ if (lastValidIndex === -1) return fallback;
180
+
181
+ const previous = points[lastValidIndex - 1];
182
+ const current = points[lastValidIndex];
183
+ return normalizeVector(current.x - previous.x, current.y - previous.y);
184
+ };
185
+
186
+ export const getOrthogonalEdgePath = ({
187
+ sourceX,
188
+ sourceY,
189
+ targetX,
190
+ targetY,
191
+ sourcePosition = "right",
192
+ targetPosition = "left",
193
+ clearance = ORTHOGONAL_EDGE_CLEARANCE,
194
+ borderRadius = ORTHOGONAL_EDGE_RADIUS,
195
+ laneOffset = 0,
196
+ routeIndex,
197
+ routeCount,
198
+ sourceNodeY,
199
+ targetNodeY,
200
+ routeGuide,
201
+ }) => {
202
+ const sourceVector = portVector(sourcePosition);
203
+ const targetVector = portVector(targetPosition);
204
+ const sourcePoint = point(sourceX, sourceY);
205
+ const targetPoint = point(targetX, targetY);
206
+ const sourceStub = point(
207
+ sourceX + sourceVector.x * clearance,
208
+ sourceY + sourceVector.y * clearance,
209
+ );
210
+ const targetStub = point(
211
+ targetX + targetVector.x * clearance,
212
+ targetY + targetVector.y * clearance,
213
+ );
214
+ const forwardRoute =
215
+ (sourcePosition === "right" &&
216
+ targetPosition === "left" &&
217
+ targetX > sourceX + clearance * 2) ||
218
+ (sourcePosition === "left" &&
219
+ targetPosition === "right" &&
220
+ targetX < sourceX - clearance * 2);
221
+ const routeCenterX = (sourceStub.x + targetStub.x) / 2;
222
+ const maxLaneOffset = Math.max(
223
+ 0,
224
+ Math.abs(targetStub.x - sourceStub.x) / 2 - borderRadius * 2,
225
+ );
226
+ const routeSpan =
227
+ Number.isFinite(routeIndex) && routeCount > 1
228
+ ? Math.min((routeCount - 1) * ORTHOGONAL_LANE_GAP, maxLaneOffset * 2)
229
+ : 0;
230
+ const distributedLaneOffset = routeSpan
231
+ ? -routeSpan / 2 + (routeIndex * routeSpan) / (routeCount - 1)
232
+ : laneOffset;
233
+ const routeX =
234
+ routeGuide?.laneX ??
235
+ routeCenterX +
236
+ clampNumber(distributedLaneOffset, -maxLaneOffset, maxLaneOffset);
237
+ const routeY =
238
+ routeGuide?.laneY ??
239
+ Math.min(sourceNodeY ?? sourceY, targetNodeY ?? targetY, sourceY, targetY) -
240
+ clearance -
241
+ Math.abs(distributedLaneOffset);
242
+ const guidedBypass = routeGuide?.kind === "bypass";
243
+ const sourceLaneX = routeGuide?.sourceLaneX ?? sourceStub.x;
244
+ const targetLaneX = routeGuide?.targetLaneX ?? targetStub.x;
245
+ const points = dedupePoints(
246
+ guidedBypass
247
+ ? [
248
+ sourcePoint,
249
+ sourceStub,
250
+ point(sourceLaneX, sourceStub.y),
251
+ point(sourceLaneX, routeY),
252
+ point(targetLaneX, routeY),
253
+ point(targetLaneX, targetStub.y),
254
+ targetStub,
255
+ targetPoint,
256
+ ]
257
+ : forwardRoute || routeGuide?.kind === "corridor"
258
+ ? [
259
+ sourcePoint,
260
+ sourceStub,
261
+ point(routeX, sourceStub.y),
262
+ point(routeX, targetStub.y),
263
+ targetStub,
264
+ targetPoint,
265
+ ]
266
+ : [
267
+ sourcePoint,
268
+ sourceStub,
269
+ point(sourceStub.x, routeY),
270
+ point(targetStub.x, routeY),
271
+ targetStub,
272
+ targetPoint,
273
+ ],
274
+ );
275
+ const path = roundedOrthogonalPath(points, borderRadius);
276
+ const labelPoint = routeGuide?.labelPoint
277
+ ? routeGuide.labelPoint
278
+ : forwardRoute || routeGuide?.kind === "corridor"
279
+ ? point(routeX, (sourceStub.y + targetStub.y) / 2)
280
+ : point((sourceStub.x + targetStub.x) / 2, routeY);
281
+
282
+ return {
283
+ path,
284
+ labelX: labelPoint.x,
285
+ labelY: labelPoint.y,
286
+ sourceTangent: firstSegmentTangent(points, sourceVector),
287
+ targetTangent: lastSegmentTangent(
288
+ points,
289
+ normalizeVector(-targetVector.x, -targetVector.y),
290
+ ),
291
+ };
292
+ };
293
+
294
+ export const getClassicArrowPoints = ({ targetX, targetY, targetTangent }) => {
295
+ const tangent = normalizeVector(targetTangent.x, targetTangent.y);
296
+ const normal = { x: -tangent.y, y: tangent.x };
297
+ const lineTargetX = targetX - tangent.x * EDGE_ARROW_LENGTH;
298
+ const lineTargetY = targetY - tangent.y * EDGE_ARROW_LENGTH;
299
+ const arrowLeftX = lineTargetX + normal.x * (EDGE_ARROW_WIDTH / 2);
300
+ const arrowLeftY = lineTargetY + normal.y * (EDGE_ARROW_WIDTH / 2);
301
+ const arrowRightX = lineTargetX - normal.x * (EDGE_ARROW_WIDTH / 2);
302
+ const arrowRightY = lineTargetY - normal.y * (EDGE_ARROW_WIDTH / 2);
303
+
304
+ return `${targetX},${targetY} ${arrowLeftX},${arrowLeftY} ${arrowRightX},${arrowRightY}`;
305
+ };
306
+
307
+ const offsetFromPort = ({ x, y, position, distance }) => {
308
+ const vector = portVector(position);
309
+
310
+ return {
311
+ x: x + vector.x * distance,
312
+ y: y + vector.y * distance,
313
+ };
314
+ };
315
+
316
+ export const getFlexibleEdgePath = ({
317
+ sourceX,
318
+ sourceY,
319
+ targetX,
320
+ targetY,
321
+ sourcePosition = "right",
322
+ targetPosition = "left",
323
+ curvature = 0.32,
324
+ bundleSource = false,
325
+ bundleTarget = false,
326
+ bundleOffset = 40,
327
+ }) => {
328
+ const sourceJunction = bundleSource
329
+ ? offsetFromPort({
330
+ x: sourceX,
331
+ y: sourceY,
332
+ position: sourcePosition,
333
+ distance: bundleOffset,
334
+ })
335
+ : point(sourceX, sourceY);
336
+ const targetJunction = bundleTarget
337
+ ? offsetFromPort({
338
+ x: targetX,
339
+ y: targetY,
340
+ position: targetPosition,
341
+ distance: bundleOffset,
342
+ })
343
+ : point(targetX, targetY);
344
+ const [branchPath, labelX, labelY] = getBezierPath({
345
+ sourceX: sourceJunction.x,
346
+ sourceY: sourceJunction.y,
347
+ sourcePosition,
348
+ targetX: targetJunction.x,
349
+ targetY: targetJunction.y,
350
+ targetPosition,
351
+ curvature,
352
+ });
353
+ const pathParts = [];
354
+
355
+ if (bundleSource) {
356
+ pathParts.push(
357
+ `M ${sourceX},${sourceY} L ${sourceJunction.x},${sourceJunction.y}`,
358
+ );
359
+ }
360
+
361
+ pathParts.push(branchPath);
362
+
363
+ if (bundleTarget) {
364
+ pathParts.push(
365
+ `M ${targetJunction.x},${targetJunction.y} L ${targetX},${targetY}`,
366
+ );
367
+ }
368
+
369
+ const targetVector = portVector(targetPosition);
370
+
371
+ return {
372
+ path: pathParts.join(" "),
373
+ labelX,
374
+ labelY,
375
+ targetTangent: normalizeVector(-targetVector.x, -targetVector.y),
376
+ };
377
+ };
378
+
379
+ export const getEdgeSlotOffsets = (data = {}) => ({
380
+ sourceSlotOffset: data.lockSourcePort
381
+ ? 0
382
+ : data.sourceFieldIndex === undefined
383
+ ? getTargetSlotOffset(data.sourceSlotIndex, data.sourceSlotCount)
384
+ : 0,
385
+ targetSlotOffset:
386
+ data.targetFieldIndex === undefined
387
+ ? getTargetSlotOffset(
388
+ data.targetSlotIndex,
389
+ data.targetSlotCount,
390
+ Number.isFinite(data.targetNodeHeight)
391
+ ? Math.max(0, data.targetNodeHeight - PORT_VERTICAL_INSET)
392
+ : Number.POSITIVE_INFINITY,
393
+ )
394
+ : 0,
395
+ });
396
+
397
+ export const getLineageEdgeVisualState = (data = {}, hovered = false) => {
398
+ const aggregatedCount = Number(data.aggregatedCount);
399
+ const aggregated = Number.isFinite(aggregatedCount) && aggregatedCount > 1;
400
+ const dimmed = Boolean(data.focusDimmed);
401
+
402
+ return {
403
+ aggregated,
404
+ highlighted:
405
+ !data.containment && !dimmed && (hovered || Boolean(data.focusFocused)),
406
+ showDecorations: !dimmed,
407
+ showHitArea: true,
408
+ showLabel:
409
+ !dimmed &&
410
+ (aggregated
411
+ ? Boolean(data.selectedEndpoint)
412
+ : hovered && Boolean(data.label)),
413
+ };
414
+ };
415
+
61
416
  const ColoredEdge = ({
62
417
  id,
418
+ source,
419
+ target,
63
420
  sourceX,
64
421
  sourceY,
65
422
  targetX,
66
423
  targetY,
67
424
  sourcePosition,
68
425
  targetPosition,
426
+ markerEnd,
69
427
  style = {},
70
428
  data = {},
71
429
  }) => {
430
+ const hoverCtx = useContext(EdgeHoverContext);
431
+ const hoveredNodeId = hoverCtx?.hoveredNodeId;
432
+ const hoveredEdgeEndpoints = hoverCtx?.hoveredEdgeEndpoints;
72
433
  const [hovered, setHovered] = useState(false);
73
- const targetSlotOffset = getTargetSlotOffset(
74
- data.targetSlotIndex,
75
- data.targetSlotCount,
434
+ const [motionTiming, setMotionTiming] = useState(null);
435
+ const gRef = useRef(null);
436
+ const hasMetadata =
437
+ data.metadata && typeof data.metadata === "object"
438
+ ? Object.keys(data.metadata).length > 0
439
+ : false;
440
+ const interactive = Boolean(data.onClick);
441
+ const { sourceSlotOffset, targetSlotOffset } = getEdgeSlotOffsets(data);
442
+ const useOrthogonalPath = data.edgePath === "orthogonal";
443
+ const useFlexibleBezierPath = data.edgePath === "bezier";
444
+ const useClassicEdgePath = !useOrthogonalPath && !useFlexibleBezierPath;
445
+ const adjustedSourceY = sourceY + (data.bundleSource ? 0 : sourceSlotOffset);
446
+ const adjustedTargetY = targetY + (data.bundleTarget ? 0 : targetSlotOffset);
447
+ const { aggregated, highlighted, showDecorations, showHitArea, showLabel } =
448
+ getLineageEdgeVisualState(data, hovered);
449
+ const dependenciesOnLeft =
450
+ data.selectedEndpoint === "source" ? targetX < sourceX : sourceX < targetX;
451
+ const setHoveredEdgeEndpoints = hoverCtx?.setHoveredEdgeEndpoints;
452
+ useEffect(() => {
453
+ if (!showHitArea) setHovered(false);
454
+ return () => {
455
+ setHoveredEdgeEndpoints?.((current) =>
456
+ current?.edgeId === id ? null : current,
457
+ );
458
+ };
459
+ }, [id, showHitArea, setHoveredEdgeEndpoints]);
460
+ const forceHorizontalPorts = Boolean(
461
+ useOrthogonalPath && data.forceHorizontalPorts,
76
462
  );
77
- const adjustedTargetY = targetY + targetSlotOffset;
78
- const curveOffset = getCurveOffset({
79
- sourceX,
80
- sourceY,
81
- targetX,
82
- targetY: adjustedTargetY,
83
- });
84
463
  const stroke = style.stroke || "var(--td-graph-edge-stroke, #b0b8c8)";
85
- const sourceControl = getControlPoint(
86
- sourceX,
87
- sourceY,
88
- sourcePosition,
89
- curveOffset,
90
- );
91
- const targetControl = getControlPoint(
92
- targetX,
93
- adjustedTargetY,
94
- targetPosition,
95
- curveOffset,
96
- );
97
- const targetTangent = normalizeVector(
98
- getBezierDerivative(1, sourceX, sourceControl.x, targetControl.x, targetX),
99
- getBezierDerivative(
100
- 1,
101
- sourceY,
102
- sourceControl.y,
103
- targetControl.y,
104
- adjustedTargetY,
105
- ),
106
- );
107
- const lineTargetX = targetX - targetTangent.x * EDGE_ARROW_LENGTH;
108
- const lineTargetY = adjustedTargetY - targetTangent.y * EDGE_ARROW_LENGTH;
109
- const edgePath = `M ${sourceX},${sourceY} C ${sourceControl.x},${sourceControl.y} ${targetControl.x},${targetControl.y} ${lineTargetX},${lineTargetY}`;
110
- const labelX = getBezierPoint(
111
- 0.5,
112
- sourceX,
113
- sourceControl.x,
114
- targetControl.x,
115
- lineTargetX,
116
- );
117
- const labelY = getBezierPoint(
118
- 0.5,
119
- sourceY,
120
- sourceControl.y,
121
- targetControl.y,
122
- lineTargetY,
464
+ const markerPosition = data.markerPosition;
465
+ const showTargetMarker =
466
+ showDecorations &&
467
+ (useOrthogonalPath
468
+ ? markerPosition === "target" ||
469
+ markerPosition === "both" ||
470
+ (markerPosition === undefined && data.showTargetArrow !== false)
471
+ : data.showTargetArrow !== false);
472
+ const showSourceMarker =
473
+ showDecorations &&
474
+ (markerPosition === "source" || markerPosition === "both") &&
475
+ data.showTargetArrow !== false;
476
+ const effectiveSourcePosition = forceHorizontalPorts
477
+ ? data.sourcePortPosition || "right"
478
+ : sourcePosition || "right";
479
+ const effectiveTargetPosition = forceHorizontalPorts
480
+ ? data.targetPortPosition || "left"
481
+ : targetPosition || "left";
482
+ const sourcePortX = sourceX;
483
+ const sourcePortY = adjustedSourceY;
484
+ const targetPortX = targetX;
485
+ const targetPortY = adjustedTargetY;
486
+ const { edgePath, labelX, labelY, targetTangent, sourceTangent } = (() => {
487
+ if (useOrthogonalPath) {
488
+ const orthogonalEdge = getOrthogonalEdgePath({
489
+ sourceX: sourcePortX,
490
+ sourceY: sourcePortY,
491
+ targetX: targetPortX,
492
+ targetY: targetPortY,
493
+ sourcePosition: effectiveSourcePosition,
494
+ targetPosition: effectiveTargetPosition,
495
+ borderRadius: data.edgeBorderRadius ?? ORTHOGONAL_EDGE_RADIUS,
496
+ clearance: data.edgeOffset ?? ORTHOGONAL_EDGE_CLEARANCE,
497
+ routeIndex: data.routeIndex,
498
+ routeCount: data.routeCount,
499
+ sourceNodeY: data.sourceNodeY,
500
+ targetNodeY: data.targetNodeY,
501
+ routeGuide: data.routeGuide,
502
+ });
503
+
504
+ const sourceVector = portVector(effectiveSourcePosition);
505
+ return {
506
+ edgePath: orthogonalEdge.path,
507
+ labelX: orthogonalEdge.labelX,
508
+ labelY: orthogonalEdge.labelY,
509
+ targetTangent: orthogonalEdge.targetTangent,
510
+ sourceTangent: normalizeVector(sourceVector.x, sourceVector.y),
511
+ };
512
+ } else if (useFlexibleBezierPath) {
513
+ const flexibleEdge = getFlexibleEdgePath({
514
+ sourceX: sourcePortX,
515
+ sourceY: sourcePortY,
516
+ sourcePosition: effectiveSourcePosition,
517
+ targetX: targetPortX,
518
+ targetY: targetPortY,
519
+ targetPosition: effectiveTargetPosition,
520
+ curvature: data.edgeCurvature ?? 0.32,
521
+ bundleSource: data.bundleSource,
522
+ bundleTarget: data.bundleTarget,
523
+ bundleOffset: data.bundleOffset,
524
+ });
525
+
526
+ const bezierSourceVector = portVector(effectiveSourcePosition);
527
+ return {
528
+ edgePath: flexibleEdge.path,
529
+ labelX: flexibleEdge.labelX,
530
+ labelY: flexibleEdge.labelY,
531
+ targetTangent: flexibleEdge.targetTangent,
532
+ sourceTangent: normalizeVector(
533
+ bezierSourceVector.x,
534
+ bezierSourceVector.y,
535
+ ),
536
+ };
537
+ } else {
538
+ const curveOffset = getCurveOffset({
539
+ sourceX,
540
+ sourceY: adjustedSourceY,
541
+ targetX,
542
+ targetY: adjustedTargetY,
543
+ });
544
+ const sourceControl = getControlPoint(
545
+ sourceX,
546
+ adjustedSourceY,
547
+ sourcePosition,
548
+ curveOffset,
549
+ );
550
+ const targetControl = getControlPoint(
551
+ targetX,
552
+ adjustedTargetY,
553
+ targetPosition,
554
+ curveOffset,
555
+ );
556
+
557
+ const targetTangent = normalizeVector(
558
+ getBezierDerivative(
559
+ 1,
560
+ sourceX,
561
+ sourceControl.x,
562
+ targetControl.x,
563
+ targetX,
564
+ ),
565
+ getBezierDerivative(
566
+ 1,
567
+ adjustedSourceY,
568
+ sourceControl.y,
569
+ targetControl.y,
570
+ adjustedTargetY,
571
+ ),
572
+ );
573
+ const sourceTangent = normalizeVector(
574
+ getBezierDerivative(
575
+ 0,
576
+ sourceX,
577
+ sourceControl.x,
578
+ targetControl.x,
579
+ targetX,
580
+ ),
581
+ getBezierDerivative(
582
+ 0,
583
+ adjustedSourceY,
584
+ sourceControl.y,
585
+ targetControl.y,
586
+ adjustedTargetY,
587
+ ),
588
+ );
589
+ const lineTargetX = targetX - targetTangent.x * EDGE_ARROW_LENGTH;
590
+ const lineTargetY = adjustedTargetY - targetTangent.y * EDGE_ARROW_LENGTH;
591
+ const edgePath = `M ${sourceX},${adjustedSourceY} C ${sourceControl.x},${sourceControl.y} ${targetControl.x},${targetControl.y} ${lineTargetX},${lineTargetY}`;
592
+ const labelX = getBezierPoint(
593
+ 0.5,
594
+ sourceX,
595
+ sourceControl.x,
596
+ targetControl.x,
597
+ lineTargetX,
598
+ );
599
+ const labelY = getBezierPoint(
600
+ 0.5,
601
+ adjustedSourceY,
602
+ sourceControl.y,
603
+ targetControl.y,
604
+ lineTargetY,
605
+ );
606
+
607
+ return { edgePath, labelX, labelY, targetTangent, sourceTangent };
608
+ }
609
+ })();
610
+ useEffect(() => {
611
+ if (!data.animated || !gRef.current) return;
612
+
613
+ const pathElement = gRef.current.querySelector("path.td-graph-edge");
614
+
615
+ if (!pathElement?.getTotalLength) return;
616
+
617
+ const duration = edgeAnimationDuration(pathElement.getTotalLength());
618
+
619
+ if (!duration) return;
620
+
621
+ setMotionTiming((current) =>
622
+ current?.path === edgePath && current.duration === duration
623
+ ? current
624
+ : { duration, path: edgePath },
625
+ );
626
+ }, [data.animated, edgePath]);
627
+ const animationDuration =
628
+ motionTiming?.path === edgePath ? motionTiming.duration : null;
629
+ const nodeHovered = hoveredNodeId
630
+ ? (hoveredNodeId === source || hoveredNodeId === target) &&
631
+ !data.containment
632
+ : false;
633
+ const sourceBundleHovered = Boolean(
634
+ data.bundleSource &&
635
+ !data.containment &&
636
+ !data.focusDimmed &&
637
+ hoveredEdgeEndpoints?.source === source &&
638
+ hoveredEdgeEndpoints.sourceFieldIndex === data.sourceFieldIndex,
123
639
  );
124
- const arrowNormalX = -targetTangent.y;
125
- const arrowNormalY = targetTangent.x;
126
- const arrowLeftX = lineTargetX + arrowNormalX * (EDGE_ARROW_WIDTH / 2);
127
- const arrowLeftY = lineTargetY + arrowNormalY * (EDGE_ARROW_WIDTH / 2);
128
- const arrowRightX = lineTargetX - arrowNormalX * (EDGE_ARROW_WIDTH / 2);
129
- const arrowRightY = lineTargetY - arrowNormalY * (EDGE_ARROW_WIDTH / 2);
640
+ const effectiveStroke = data.connectionSelected
641
+ ? "var(--td-graph-edge-metadata, #7c3aed)"
642
+ : highlighted || nodeHovered || sourceBundleHovered
643
+ ? "var(--td-graph-edge-active, var(--td-graph-accent, #ed5c17))"
644
+ : stroke;
645
+ const handleClick = (event) => {
646
+ if (!interactive) return;
647
+ event.stopPropagation();
648
+ data.onClick(data.metadata);
649
+ };
650
+ const edgeClassName = [
651
+ "td-graph-edge",
652
+ data.connectionSelected ? "td-graph-edge--connection-selected" : "",
653
+ data.animated ? "td-graph-edge--animated" : "",
654
+ data.flowDirection ? `td-graph-edge--flow-${data.flowDirection}` : "",
655
+ data.containment ? "td-graph-edge--containment" : "",
656
+ hovered ? "td-graph-edge--hovered" : "",
657
+ data.restrictive ? "td-graph-edge--restrictive" : "",
658
+ data.focusFocused ? "td-graph-edge--focus-focused" : "",
659
+ data.focusDimmed ? "td-graph-edge--focus-dimmed" : "",
660
+ nodeHovered ? "td-graph-edge--node-hovered" : "",
661
+ sourceBundleHovered ? "td-graph-edge--source-bundle-hovered" : "",
662
+ ]
663
+ .filter(Boolean)
664
+ .join(" ");
130
665
 
131
666
  return (
132
- <>
133
- <BaseEdge id={id} path={edgePath} style={style} />
134
- <circle
135
- cx={sourceX}
136
- cy={sourceY}
137
- r={EDGE_START_DOT_RADIUS}
138
- fill={stroke}
139
- pointerEvents="none"
667
+ <g ref={gRef}>
668
+ <BaseEdge
669
+ className={edgeClassName}
670
+ id={id}
671
+ path={edgePath}
672
+ style={{
673
+ ...style,
674
+ stroke: effectiveStroke,
675
+ }}
140
676
  />
141
- {data.showTargetArrow !== false ? (
677
+ {data.animated && animationDuration ? (
678
+ <circle
679
+ aria-hidden="true"
680
+ className="td-graph-edge-flow-dot"
681
+ r={4}
682
+ fill={effectiveStroke}
683
+ opacity={1}
684
+ pointerEvents="none"
685
+ >
686
+ <animateMotion
687
+ calcMode="paced"
688
+ dur={`${animationDuration}s`}
689
+ repeatCount="indefinite"
690
+ begin="0s"
691
+ fill="freeze"
692
+ path={edgePath}
693
+ />
694
+ </circle>
695
+ ) : null}
696
+ {data.showSourceDot !== false ? (
697
+ <circle
698
+ cx={sourceX}
699
+ cy={adjustedSourceY}
700
+ r={EDGE_START_DOT_RADIUS}
701
+ fill={effectiveStroke}
702
+ pointerEvents="none"
703
+ />
704
+ ) : null}
705
+ {!useClassicEdgePath && showTargetMarker && markerEnd && targetTangent ? (
142
706
  <polygon
143
- points={`${targetX},${adjustedTargetY} ${arrowLeftX},${arrowLeftY} ${arrowRightX},${arrowRightY}`}
144
- fill={stroke}
707
+ className="td-graph-edge-arrow"
708
+ points={getClassicArrowPoints({
709
+ targetX: targetPortX,
710
+ targetY: targetPortY,
711
+ targetTangent,
712
+ })}
713
+ fill={effectiveStroke}
145
714
  pointerEvents="none"
146
715
  />
147
716
  ) : null}
148
- <path
149
- d={edgePath}
150
- fill="none"
151
- stroke="transparent"
152
- strokeWidth={16}
153
- onMouseEnter={() => setHovered(true)}
154
- onMouseLeave={() => setHovered(false)}
155
- />
156
- {hovered && data.label && (
717
+ {!useClassicEdgePath && showSourceMarker && markerEnd && sourceTangent ? (
718
+ <polygon
719
+ className="td-graph-edge-arrow"
720
+ points={getClassicArrowPoints({
721
+ targetX: sourcePortX,
722
+ targetY: sourcePortY,
723
+ targetTangent: { x: -sourceTangent.x, y: -sourceTangent.y },
724
+ })}
725
+ fill={effectiveStroke}
726
+ pointerEvents="none"
727
+ />
728
+ ) : null}
729
+ {useClassicEdgePath &&
730
+ showDecorations &&
731
+ data.showTargetArrow !== false &&
732
+ targetTangent ? (
733
+ <polygon
734
+ points={getClassicArrowPoints({
735
+ targetX: targetPortX,
736
+ targetY: targetPortY,
737
+ targetTangent,
738
+ })}
739
+ fill={effectiveStroke}
740
+ pointerEvents="none"
741
+ />
742
+ ) : null}
743
+ {showHitArea ? (
744
+ <path
745
+ className="td-graph-edge-hit-area"
746
+ d={edgePath}
747
+ fill="none"
748
+ stroke="transparent"
749
+ strokeWidth={16}
750
+ onMouseEnter={() => {
751
+ setHovered(true);
752
+ hoverCtx?.setHoveredEdgeEndpoints({
753
+ edgeId: id,
754
+ source,
755
+ sourceFieldIndex: data.sourceFieldIndex,
756
+ target,
757
+ targetFieldIndex: data.targetFieldIndex,
758
+ });
759
+ }}
760
+ onMouseLeave={() => {
761
+ setHovered(false);
762
+ setHoveredEdgeEndpoints?.((current) =>
763
+ current?.edgeId === id ? null : current,
764
+ );
765
+ }}
766
+ onClick={handleClick}
767
+ cursor={interactive ? "pointer" : "default"}
768
+ />
769
+ ) : null}
770
+ {showLabel && (
771
+ <EdgeLabelRenderer>
772
+ <div
773
+ className={`td-graph-edge-label nodrag nopan${aggregated ? " td-graph-edge-label--aggregated" : ""}`}
774
+ style={{
775
+ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
776
+ }}
777
+ >
778
+ {aggregated ? (
779
+ <>
780
+ {dependenciesOnLeft ? <span aria-hidden="true">→</span> : null}
781
+ <span>{data.aggregatedCount}</span>
782
+ {!dependenciesOnLeft ? <span aria-hidden="true">→</span> : null}
783
+ </>
784
+ ) : (
785
+ data.label
786
+ )}
787
+ </div>
788
+ </EdgeLabelRenderer>
789
+ )}
790
+ {showDecorations && hasMetadata && (
157
791
  <EdgeLabelRenderer>
158
792
  <div
159
- className="td-graph-edge-label nodrag nopan"
793
+ className="td-graph-edge-metadata-badge nodrag nopan"
160
794
  style={{
161
795
  transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
162
796
  }}
797
+ title={interactive ? "Click for details" : undefined}
798
+ onClick={handleClick}
163
799
  >
164
- {data.label}
800
+ <span className="td-graph-edge-metadata-badge__dot" />
165
801
  </div>
166
802
  </EdgeLabelRenderer>
167
803
  )}
168
- </>
804
+ </g>
169
805
  );
170
806
  };
171
807