@falkordb/canvas 0.0.41 → 0.0.43
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/dist/canvas-types.d.ts +2 -2
- package/dist/canvas-types.d.ts.map +1 -1
- package/dist/canvas-utils.d.ts +1 -0
- package/dist/canvas-utils.d.ts.map +1 -1
- package/dist/canvas-utils.js +1 -1
- package/dist/canvas-utils.js.map +1 -1
- package/dist/canvas.d.ts +2 -0
- package/dist/canvas.d.ts.map +1 -1
- package/dist/canvas.js +247 -94
- package/dist/canvas.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/canvas-types.ts +2 -2
- package/src/canvas-utils.ts +1 -1
- package/src/canvas.ts +260 -106
- package/src/index.ts +1 -0
package/dist/canvas.js
CHANGED
|
@@ -8,6 +8,11 @@ const ARROW_WH_RATIO = 1.6;
|
|
|
8
8
|
const ARROW_VLEN_RATIO = 0.2;
|
|
9
9
|
// Multiplier to convert node size → cubic bezier control-point distance for self-loops
|
|
10
10
|
const SELF_LOOP_CURVE_FACTOR = 11.67;
|
|
11
|
+
// Base font size used for the initial measurement and for two-line text.
|
|
12
|
+
const NODE_FONT_SIZE_BASE = 2;
|
|
13
|
+
// Fraction of the chord width that single-line text should fill (0–1).
|
|
14
|
+
// Leaves (1 - ratio)/2 of the radius as horizontal padding on each side.
|
|
15
|
+
const NODE_TEXT_FILL_RATIO = 0.85;
|
|
11
16
|
// Force constants
|
|
12
17
|
const CHARGE_STRENGTH = -400;
|
|
13
18
|
const CENTER_STRENGTH = 0.03;
|
|
@@ -62,7 +67,14 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
62
67
|
this.nodeMode = 'replace';
|
|
63
68
|
this.linkMode = 'replace';
|
|
64
69
|
this.nodeDegreeMap = new Map();
|
|
70
|
+
// Per-node font size cache: computed once per node, read every frame.
|
|
71
|
+
this.nodeDisplayFontSize = new Map();
|
|
65
72
|
this.relationshipsTextCache = new Map();
|
|
73
|
+
this.onFontsLoadingDone = () => {
|
|
74
|
+
this.relationshipsTextCache.clear();
|
|
75
|
+
this.nodeDisplayFontSize.clear();
|
|
76
|
+
this.triggerRender();
|
|
77
|
+
};
|
|
66
78
|
this.attachShadow({ mode: "open" });
|
|
67
79
|
}
|
|
68
80
|
/**
|
|
@@ -97,9 +109,14 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
97
109
|
}
|
|
98
110
|
this.log('Component connected to DOM');
|
|
99
111
|
this.render();
|
|
112
|
+
// Text measurements taken before the custom font finishes loading use the
|
|
113
|
+
// fallback system font and produce wrong widths that get locked in the cache.
|
|
114
|
+
// Re-measure on every font-load batch (including the initial one).
|
|
115
|
+
document.fonts.addEventListener("loadingdone", this.onFontsLoadingDone);
|
|
100
116
|
}
|
|
101
117
|
disconnectedCallback() {
|
|
102
118
|
this.log('Component disconnected from DOM');
|
|
119
|
+
document.fonts.removeEventListener("loadingdone", this.onFontsLoadingDone);
|
|
103
120
|
if (this.resizeObserver) {
|
|
104
121
|
this.resizeObserver.disconnect();
|
|
105
122
|
this.resizeObserver = null;
|
|
@@ -115,7 +132,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
115
132
|
// Update event handlers if they were provided
|
|
116
133
|
if (config.onNodeClick || config.onLinkClick || config.onNodeRightClick || config.onLinkRightClick ||
|
|
117
134
|
config.onNodeHover || config.onLinkHover || config.onBackgroundClick || config.onBackgroundRightClick || config.onZoom ||
|
|
118
|
-
config.onEngineStop || config.isNodeSelected || config.isLinkSelected || config.
|
|
135
|
+
config.onEngineStop || config.isNodeSelected || config.isLinkSelected || config.node || config.link) {
|
|
119
136
|
this.log('Updating event handlers');
|
|
120
137
|
this.updateEventHandlers();
|
|
121
138
|
}
|
|
@@ -466,7 +483,6 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
466
483
|
this.config.onEngineStop();
|
|
467
484
|
}
|
|
468
485
|
})
|
|
469
|
-
.linkLineDash((link) => this.config.linkLineDash?.(link) ?? null)
|
|
470
486
|
.nodeCanvasObject((node, ctx) => {
|
|
471
487
|
if (this.config.node) {
|
|
472
488
|
this.config.node.nodeCanvasObject(node, ctx);
|
|
@@ -477,7 +493,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
477
493
|
})
|
|
478
494
|
.linkCanvasObject((link, ctx, globalScale) => {
|
|
479
495
|
if (this.config.link) {
|
|
480
|
-
this.config.link.linkCanvasObject(link, ctx);
|
|
496
|
+
this.config.link.linkCanvasObject(link, ctx, globalScale);
|
|
481
497
|
}
|
|
482
498
|
else {
|
|
483
499
|
this.drawLink(link, ctx, globalScale);
|
|
@@ -541,8 +557,10 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
541
557
|
this.log('Force simulation setup complete');
|
|
542
558
|
}
|
|
543
559
|
drawNode(node, ctx) {
|
|
544
|
-
if (node.x
|
|
545
|
-
|
|
560
|
+
if (node.x === undefined || node.y === undefined) {
|
|
561
|
+
node.x = 0;
|
|
562
|
+
node.y = 0;
|
|
563
|
+
}
|
|
546
564
|
ctx.lineWidth = this.config.isNodeSelected?.(node) ? 1 : 0.5;
|
|
547
565
|
ctx.strokeStyle = this.config.foregroundColor;
|
|
548
566
|
ctx.fillStyle = node.color;
|
|
@@ -557,13 +575,30 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
557
575
|
ctx.fillStyle = getContrastTextColor(node.color);
|
|
558
576
|
ctx.textAlign = "center";
|
|
559
577
|
ctx.textBaseline = "middle";
|
|
560
|
-
ctx.font = "400 2px SofiaSans";
|
|
561
578
|
let [line1, line2] = node.displayName;
|
|
579
|
+
const textRadius = node.size - PADDING / 2;
|
|
562
580
|
if (!line1 && !line2) {
|
|
563
581
|
const text = getNodeDisplayText(node, this.config.captionsKeys, this.config.showPropertyKeyPrefix);
|
|
564
|
-
|
|
582
|
+
// Measure at the base (smallest) size — one cheap measurement.
|
|
583
|
+
ctx.font = `400 ${NODE_FONT_SIZE_BASE}px SofiaSans`;
|
|
565
584
|
[line1, line2] = wrapTextForCircularNode(ctx, text, textRadius);
|
|
585
|
+
let chosenSize = NODE_FONT_SIZE_BASE;
|
|
586
|
+
if (!line2) {
|
|
587
|
+
// Single-line: scale up so the text fills NODE_TEXT_FILL_RATIO of the
|
|
588
|
+
// available chord. Font metrics scale linearly so this is exact.
|
|
589
|
+
const measuredWidth = ctx.measureText(line1).width;
|
|
590
|
+
if (measuredWidth > 0) {
|
|
591
|
+
chosenSize = NODE_FONT_SIZE_BASE * (NODE_TEXT_FILL_RATIO * 2 * textRadius / measuredWidth);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
ctx.font = `400 ${chosenSize}px SofiaSans`;
|
|
566
595
|
node.displayName = [line1, line2];
|
|
596
|
+
this.nodeDisplayFontSize.set(node.id, chosenSize);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
// Cache hit: the font size was stored when displayName was first computed.
|
|
600
|
+
const chosenSize = this.nodeDisplayFontSize.get(node.id) ?? NODE_FONT_SIZE_BASE;
|
|
601
|
+
ctx.font = `400 ${chosenSize}px SofiaSans`;
|
|
567
602
|
}
|
|
568
603
|
const textMetrics = ctx.measureText(line1);
|
|
569
604
|
const textHeight = textMetrics.actualBoundingBoxAscent +
|
|
@@ -577,8 +612,11 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
577
612
|
}
|
|
578
613
|
}
|
|
579
614
|
pointerNode(node, color, ctx) {
|
|
580
|
-
if (node.x
|
|
581
|
-
|
|
615
|
+
if (node.x === undefined || node.y === undefined) {
|
|
616
|
+
node.x = 0;
|
|
617
|
+
node.y = 0;
|
|
618
|
+
}
|
|
619
|
+
;
|
|
582
620
|
const radius = node.size + PADDING;
|
|
583
621
|
ctx.fillStyle = color;
|
|
584
622
|
ctx.beginPath();
|
|
@@ -588,25 +626,35 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
588
626
|
drawLink(link, ctx, globalScale) {
|
|
589
627
|
const start = link.source;
|
|
590
628
|
const end = link.target;
|
|
591
|
-
if (start.x
|
|
592
|
-
|
|
629
|
+
if (start.x === undefined || start.y === undefined || end.x === undefined || end.y === undefined) {
|
|
630
|
+
start.x = 0;
|
|
631
|
+
start.y = 0;
|
|
632
|
+
end.x = 0;
|
|
633
|
+
end.y = 0;
|
|
634
|
+
}
|
|
593
635
|
let textX;
|
|
594
636
|
let textY;
|
|
595
637
|
let angle;
|
|
638
|
+
const isLinkSelected = this.config.isLinkSelected?.(link) ?? false;
|
|
639
|
+
const arrowLen = isLinkSelected ? 4 : 2;
|
|
640
|
+
// Deferred arrowhead — drawn after the label so it is never covered by
|
|
641
|
+
// the label background rect (which happens for short links where the
|
|
642
|
+
// bezier midpoint and the arrow tip are at almost the same position).
|
|
643
|
+
let pendingArrow = null;
|
|
596
644
|
if (start.id === end.id) {
|
|
597
645
|
const nodeSize = start.size || 6;
|
|
598
646
|
const d = (link.curve || 0) * nodeSize * SELF_LOOP_CURVE_FACTOR;
|
|
599
|
-
ctx.lineWidth = (
|
|
600
|
-
|
|
647
|
+
ctx.lineWidth = (isLinkSelected ? 2 : 1) / globalScale;
|
|
648
|
+
if (this.config.linkLineDash)
|
|
649
|
+
ctx.setLineDash(this.config.linkLineDash(link));
|
|
601
650
|
// The visible outer edge of the node border is nodeSize + strokeWidth
|
|
602
651
|
// (stroke is centered on nodeSize + strokeWidth/2, so outer edge = nodeSize + strokeWidth).
|
|
603
652
|
const nodeStrokeWidth = this.config.isNodeSelected?.(start) ? 1 : 0.5;
|
|
604
|
-
const borderRadius = nodeSize + nodeStrokeWidth;
|
|
653
|
+
const borderRadius = nodeSize + nodeStrokeWidth + PADDING;
|
|
605
654
|
// Binary search for tArrow near 1.0 where the curve is at distance borderRadius
|
|
606
655
|
// from the node center (i.e. on the outer edge of the node border stroke).
|
|
607
656
|
// Bezier parametric form: Bx(t)=sx+3(1-t)t²d, By(t)=sy-3(1-t)²td
|
|
608
657
|
// dist(t) = 3*(1-t)*t*|d|*sqrt(t² + (1-t)²)
|
|
609
|
-
const arrowLen = (this.config.isLinkSelected?.(link) ? 4 : 2) / globalScale;
|
|
610
658
|
const arrowHalfWidth = arrowLen / ARROW_WH_RATIO / 2;
|
|
611
659
|
let lo = 0.5, hi = 1.0;
|
|
612
660
|
const absD = Math.abs(d);
|
|
@@ -659,13 +707,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
659
707
|
if (tLen !== 0 && canReachBorder) {
|
|
660
708
|
const nx = tdx / tLen;
|
|
661
709
|
const ny = tdy / tLen;
|
|
662
|
-
|
|
663
|
-
ctx.beginPath();
|
|
664
|
-
ctx.moveTo(tipX, tipY);
|
|
665
|
-
ctx.lineTo(tipX - nx * arrowLen + ny * arrowHalfWidth, tipY - ny * arrowLen - nx * arrowHalfWidth);
|
|
666
|
-
ctx.lineTo(tipX - nx * arrowLen * (1 - ARROW_VLEN_RATIO), tipY - ny * arrowLen * (1 - ARROW_VLEN_RATIO));
|
|
667
|
-
ctx.lineTo(tipX - nx * arrowLen - ny * arrowHalfWidth, tipY - ny * arrowLen + nx * arrowHalfWidth);
|
|
668
|
-
ctx.fill();
|
|
710
|
+
pendingArrow = { tipX, tipY, nx, ny, arrowLen, arrowHalfWidth };
|
|
669
711
|
}
|
|
670
712
|
// Midpoint of cubic bezier: P0=(sx,sy), P1=(sx,sy-d), P2=(sx+d,sy), P3=(sx,sy)
|
|
671
713
|
textX = start.x + 0.375 * d;
|
|
@@ -708,22 +750,12 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
708
750
|
angle = -(Math.PI - angle);
|
|
709
751
|
if (angle < -Math.PI / 2)
|
|
710
752
|
angle = -(-Math.PI - angle);
|
|
711
|
-
//
|
|
712
|
-
// Scale arrow geometry by 1/globalScale so arrowheads remain visually
|
|
713
|
-
// consistent across zoom levels, matching ctx.lineWidth / globalScale.
|
|
714
|
-
const baseArrowLen = this.config.isLinkSelected?.(link) ? 4 : 2;
|
|
715
|
-
const arrowLen = baseArrowLen / globalScale;
|
|
753
|
+
// Draw regular link line and arrowhead
|
|
716
754
|
const arrowHalfWidth = arrowLen / ARROW_WH_RATIO / 2;
|
|
717
|
-
//
|
|
718
|
-
// Q(t) = (1-t)²·start + 2(1-t)t·control + t²·end
|
|
719
|
-
// is at distance borderRadius from the target node centre.
|
|
755
|
+
// Target-side clip: find t where bezier enters target node border + PADDING
|
|
720
756
|
const endNodeSize = end.size || 6;
|
|
721
|
-
const
|
|
722
|
-
const borderRadius = endNodeSize + endNodeStrokeWidth;
|
|
757
|
+
const borderRadius = endNodeSize + (this.config.isNodeSelected?.(end) ? 1 : 0.5) + PADDING;
|
|
723
758
|
const borderRadiusSq = borderRadius * borderRadius;
|
|
724
|
-
// When borderRadius is small relative to the chord length, the bezier and
|
|
725
|
-
// chord diverge only near t=1, so a linear approximation is accurate and
|
|
726
|
-
// avoids the per-frame search cost on large graphs.
|
|
727
759
|
let tArrow;
|
|
728
760
|
if (borderRadius / distance < 0.02) {
|
|
729
761
|
tArrow = Math.min(1, Math.max(0, 1 - borderRadius / distance));
|
|
@@ -747,55 +779,89 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
747
779
|
tArrow = (lo + hi) / 2;
|
|
748
780
|
}
|
|
749
781
|
const uArrow = 1 - tArrow;
|
|
750
|
-
// Tip = Q(tArrow)
|
|
751
782
|
const tipX = uArrow * uArrow * start.x + 2 * uArrow * tArrow * controlX + tArrow * tArrow * end.x;
|
|
752
783
|
const tipY = uArrow * uArrow * start.y + 2 * uArrow * tArrow * controlY + tArrow * tArrow * end.y;
|
|
753
|
-
//
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
const
|
|
784
|
+
// Source-side clip: find t where bezier exits source node border + PADDING
|
|
785
|
+
const startNodeSize = start.size || 6;
|
|
786
|
+
const srcBorderRadius = startNodeSize + (this.config.isNodeSelected?.(start) ? 1 : 0.5) + PADDING;
|
|
787
|
+
const srcBorderRadiusSq = srcBorderRadius * srcBorderRadius;
|
|
788
|
+
let tStart = 0;
|
|
789
|
+
if (srcBorderRadius / distance < 0.02) {
|
|
790
|
+
tStart = Math.min(0.5, srcBorderRadius / distance);
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
let lo = 0.0, hi = 0.5;
|
|
794
|
+
for (let i = 0; i < 10; i++) {
|
|
795
|
+
const mid = (lo + hi) / 2;
|
|
796
|
+
const um = 1 - mid;
|
|
797
|
+
const qx = um * um * start.x + 2 * um * mid * controlX + mid * mid * end.x;
|
|
798
|
+
const qy = um * um * start.y + 2 * um * mid * controlY + mid * mid * end.y;
|
|
799
|
+
const dxSrc = qx - start.x;
|
|
800
|
+
const dySrc = qy - start.y;
|
|
801
|
+
if (dxSrc * dxSrc + dySrc * dySrc < srcBorderRadiusSq)
|
|
802
|
+
lo = mid;
|
|
803
|
+
else
|
|
804
|
+
hi = mid;
|
|
805
|
+
if (hi - lo < 1e-3)
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
tStart = (lo + hi) / 2;
|
|
809
|
+
}
|
|
810
|
+
// Gap start point: Q(tStart)
|
|
811
|
+
const uS = 1 - tStart;
|
|
812
|
+
const gapStartX = uS * uS * start.x + 2 * uS * tStart * controlX + tStart * tStart * end.x;
|
|
813
|
+
const gapStartY = uS * uS * start.y + 2 * uS * tStart * controlY + tStart * tStart * end.y;
|
|
814
|
+
// Sub-bezier [tStart, tArrow] control point via De Casteljau:
|
|
815
|
+
// Right sub-bezier at tStart → NewP1 = lerp(control, end, tStart)
|
|
816
|
+
// Left sub-curve at tArrow' = (tArrow-tStart)/(1-tStart) → ctrl = lerp(gapStart, NewP1, tArrow')
|
|
817
|
+
const tArrowPrime = tStart < tArrow ? (tArrow - tStart) / (1 - tStart) : 0;
|
|
818
|
+
const newP1X = (1 - tStart) * controlX + tStart * end.x;
|
|
819
|
+
const newP1Y = (1 - tStart) * controlY + tStart * end.y;
|
|
820
|
+
const subCtrlX = (1 - tArrowPrime) * gapStartX + tArrowPrime * newP1X;
|
|
821
|
+
const subCtrlY = (1 - tArrowPrime) * gapStartY + tArrowPrime * newP1Y;
|
|
757
822
|
ctx.strokeStyle = link.color;
|
|
758
|
-
ctx.lineWidth = (
|
|
823
|
+
ctx.lineWidth = (isLinkSelected ? 2 : 1) / globalScale;
|
|
759
824
|
ctx.setLineDash(this.config.linkLineDash?.(link) ?? []);
|
|
760
825
|
ctx.beginPath();
|
|
761
|
-
ctx.moveTo(
|
|
762
|
-
ctx.quadraticCurveTo(
|
|
826
|
+
ctx.moveTo(gapStartX, gapStartY);
|
|
827
|
+
ctx.quadraticCurveTo(subCtrlX, subCtrlY, tipX, tipY);
|
|
763
828
|
ctx.stroke();
|
|
764
829
|
ctx.setLineDash([]);
|
|
765
|
-
// Arrowhead tangent at tArrow: Q'(t) = 2(1-t)(control-start) + 2t(end-control)
|
|
766
830
|
const atx = 2 * uArrow * (controlX - start.x) + 2 * tArrow * (end.x - controlX);
|
|
767
831
|
const aty = 2 * uArrow * (controlY - start.y) + 2 * tArrow * (end.y - controlY);
|
|
768
832
|
const atLen = Math.sqrt(atx * atx + aty * aty);
|
|
769
833
|
if (atLen !== 0) {
|
|
770
834
|
const nx = atx / atLen;
|
|
771
835
|
const ny = aty / atLen;
|
|
772
|
-
|
|
773
|
-
ctx.beginPath();
|
|
774
|
-
ctx.moveTo(tipX, tipY);
|
|
775
|
-
ctx.lineTo(tipX - nx * arrowLen + ny * arrowHalfWidth, tipY - ny * arrowLen - nx * arrowHalfWidth);
|
|
776
|
-
ctx.lineTo(tipX - nx * arrowLen * (1 - ARROW_VLEN_RATIO), tipY - ny * arrowLen * (1 - ARROW_VLEN_RATIO));
|
|
777
|
-
ctx.lineTo(tipX - nx * arrowLen - ny * arrowHalfWidth, tipY - ny * arrowLen + nx * arrowHalfWidth);
|
|
778
|
-
ctx.fill();
|
|
836
|
+
pendingArrow = { tipX, tipY, nx, ny, arrowLen, arrowHalfWidth };
|
|
779
837
|
}
|
|
780
838
|
}
|
|
781
|
-
ctx.font = "400 2px SofiaSans";
|
|
839
|
+
ctx.font = isLinkSelected ? "700 2px SofiaSans" : "400 2px SofiaSans";
|
|
782
840
|
ctx.textAlign = "center";
|
|
783
841
|
// Draw text with alphabetic baseline, positioned so visual center is at y=0
|
|
784
842
|
ctx.textBaseline = "alphabetic";
|
|
785
|
-
|
|
843
|
+
// Separate cache entries per weight so each state is measured with its own
|
|
844
|
+
// font, giving equal visual padding regardless of selection state.
|
|
845
|
+
const cacheKey = `${link.relationship}_${isLinkSelected ? "700" : "400"}`;
|
|
846
|
+
let cached = this.relationshipsTextCache.get(cacheKey);
|
|
786
847
|
if (!cached) {
|
|
848
|
+
// ctx.font is already set to the correct weight above; measure it directly.
|
|
787
849
|
const metrics = ctx.measureText(link.relationship);
|
|
788
|
-
// Use
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
//
|
|
792
|
-
const
|
|
850
|
+
// Use actual ink bounds for vertical metrics; fontBoundingBox* is the full
|
|
851
|
+
// line-box and adds excessive space for lighter weights.
|
|
852
|
+
// Use metrics.width for horizontal extent: actualBoundingBoxLeft/Right are
|
|
853
|
+
// unreliable with textAlign="center" and can double the value on some engines.
|
|
854
|
+
const inkAscent = metrics.actualBoundingBoxAscent ?? metrics.fontBoundingBoxAscent;
|
|
855
|
+
const inkDescent = metrics.actualBoundingBoxDescent ?? metrics.fontBoundingBoxDescent;
|
|
856
|
+
const inkWidth = metrics.width;
|
|
857
|
+
const bgPadding = 0.3;
|
|
793
858
|
cached = {
|
|
794
|
-
textWidth:
|
|
795
|
-
textHeight:
|
|
796
|
-
|
|
859
|
+
textWidth: inkWidth + bgPadding * 2,
|
|
860
|
+
textHeight: inkAscent + inkDescent + bgPadding * 2,
|
|
861
|
+
// Shift baseline up so the ink block is centred inside the bg rect.
|
|
862
|
+
textYOffset: (inkAscent - inkDescent) / 2,
|
|
797
863
|
};
|
|
798
|
-
this.relationshipsTextCache.set(
|
|
864
|
+
this.relationshipsTextCache.set(cacheKey, cached);
|
|
799
865
|
}
|
|
800
866
|
const { textWidth, textHeight, textYOffset } = cached;
|
|
801
867
|
ctx.save();
|
|
@@ -808,6 +874,17 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
808
874
|
ctx.fillStyle = getContrastTextColor(this.config.backgroundColor);
|
|
809
875
|
ctx.fillText(link.relationship, 0, textYOffset);
|
|
810
876
|
ctx.restore();
|
|
877
|
+
// Draw arrowhead last so it always appears on top of the label background.
|
|
878
|
+
if (pendingArrow) {
|
|
879
|
+
const { tipX, tipY, nx, ny, arrowLen: aLen, arrowHalfWidth: aHW } = pendingArrow;
|
|
880
|
+
ctx.fillStyle = link.color;
|
|
881
|
+
ctx.beginPath();
|
|
882
|
+
ctx.moveTo(tipX, tipY);
|
|
883
|
+
ctx.lineTo(tipX - nx * aLen + ny * aHW, tipY - ny * aLen - nx * aHW);
|
|
884
|
+
ctx.lineTo(tipX - nx * aLen * (1 - ARROW_VLEN_RATIO), tipY - ny * aLen * (1 - ARROW_VLEN_RATIO));
|
|
885
|
+
ctx.lineTo(tipX - nx * aLen - ny * aHW, tipY - ny * aLen + nx * aHW);
|
|
886
|
+
ctx.fill();
|
|
887
|
+
}
|
|
811
888
|
}
|
|
812
889
|
pointerLink(link, color, ctx) {
|
|
813
890
|
const start = link.source;
|
|
@@ -828,14 +905,38 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
828
905
|
}
|
|
829
906
|
ctx.beginPath();
|
|
830
907
|
if (start.id === end.id) {
|
|
831
|
-
// Self-loop: replicate
|
|
908
|
+
// Self-loop: replicate exact cubic bezier clip from drawLink
|
|
832
909
|
const nodeSize = start.size || 6;
|
|
833
910
|
const d = (link.curve || 0) * nodeSize * SELF_LOOP_CURVE_FACTOR;
|
|
911
|
+
const nodeStrokeWidth = this.config.isNodeSelected?.(start) ? 1 : 0.5;
|
|
912
|
+
const borderRadius = nodeSize + nodeStrokeWidth + PADDING;
|
|
913
|
+
const absD = Math.abs(d);
|
|
914
|
+
const maxReachableDist = 3 * 0.5 * 0.5 * absD * Math.sqrt(0.5);
|
|
915
|
+
const canReachBorder = absD > 0 && maxReachableDist >= borderRadius;
|
|
834
916
|
ctx.moveTo(start.x, start.y);
|
|
835
|
-
|
|
917
|
+
if (canReachBorder) {
|
|
918
|
+
let lo = 0.5, hi = 1.0;
|
|
919
|
+
for (let i = 0; i < 20; i++) {
|
|
920
|
+
const mid = (lo + hi) / 2;
|
|
921
|
+
const um = 1 - mid;
|
|
922
|
+
const dist = 3 * um * mid * absD * Math.sqrt(mid * mid + um * um);
|
|
923
|
+
if (dist > borderRadius)
|
|
924
|
+
lo = mid;
|
|
925
|
+
else
|
|
926
|
+
hi = mid;
|
|
927
|
+
}
|
|
928
|
+
const tArrow = (lo + hi) / 2;
|
|
929
|
+
const uArrow = 1 - tArrow;
|
|
930
|
+
const tipX = start.x + 3 * uArrow * tArrow * tArrow * d;
|
|
931
|
+
const tipY = start.y - 3 * uArrow * uArrow * tArrow * d;
|
|
932
|
+
ctx.bezierCurveTo(start.x, start.y - tArrow * d, start.x + tArrow * tArrow * d, start.y - 2 * tArrow * uArrow * d, tipX, tipY);
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
ctx.bezierCurveTo(start.x, start.y - d, start.x + d, start.y, start.x, start.y);
|
|
936
|
+
}
|
|
836
937
|
}
|
|
837
938
|
else {
|
|
838
|
-
// Regular link: replicate
|
|
939
|
+
// Regular link: replicate exact quadratic bezier clip from drawLink
|
|
839
940
|
const dx = end.x - start.x;
|
|
840
941
|
const dy = end.y - start.y;
|
|
841
942
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
@@ -849,18 +950,71 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
849
950
|
const perpY = -dx / distance;
|
|
850
951
|
const controlX = (start.x + end.x) / 2 + perpX * curvature * distance;
|
|
851
952
|
const controlY = (start.y + end.y) / 2 + perpY * curvature * distance;
|
|
852
|
-
//
|
|
853
|
-
|
|
854
|
-
const
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
953
|
+
// Use the same borderRadius and binary-search clip as drawLink
|
|
954
|
+
const endNodeSize = end.size || 6;
|
|
955
|
+
const borderRadius = endNodeSize + (this.config.isNodeSelected?.(end) ? 1 : 0.5) + PADDING;
|
|
956
|
+
const borderRadiusSq = borderRadius * borderRadius;
|
|
957
|
+
let tArrow;
|
|
958
|
+
if (borderRadius / distance < 0.02) {
|
|
959
|
+
tArrow = Math.min(1, Math.max(0, 1 - borderRadius / distance));
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
let lo = 0.5, hi = 1.0;
|
|
963
|
+
for (let i = 0; i < 10; i++) {
|
|
964
|
+
const mid = (lo + hi) / 2;
|
|
965
|
+
const um = 1 - mid;
|
|
966
|
+
const qx = um * um * start.x + 2 * um * mid * controlX + mid * mid * end.x;
|
|
967
|
+
const qy = um * um * start.y + 2 * um * mid * controlY + mid * mid * end.y;
|
|
968
|
+
const dxEnd = qx - end.x;
|
|
969
|
+
const dyEnd = qy - end.y;
|
|
970
|
+
if (dxEnd * dxEnd + dyEnd * dyEnd > borderRadiusSq)
|
|
971
|
+
lo = mid;
|
|
972
|
+
else
|
|
973
|
+
hi = mid;
|
|
974
|
+
if (hi - lo < 1e-3)
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
tArrow = (lo + hi) / 2;
|
|
978
|
+
}
|
|
979
|
+
const uArrow = 1 - tArrow;
|
|
980
|
+
const tipX = uArrow * uArrow * start.x + 2 * uArrow * tArrow * controlX + tArrow * tArrow * end.x;
|
|
981
|
+
const tipY = uArrow * uArrow * start.y + 2 * uArrow * tArrow * controlY + tArrow * tArrow * end.y;
|
|
982
|
+
// Source-side clip: mirror of drawLink source gap
|
|
983
|
+
const startNodeSize = start.size || 6;
|
|
984
|
+
const srcBorderRadius = startNodeSize + (this.config.isNodeSelected?.(start) ? 1 : 0.5) + PADDING;
|
|
985
|
+
const srcBorderRadiusSq = srcBorderRadius * srcBorderRadius;
|
|
986
|
+
let tStart = 0;
|
|
987
|
+
if (srcBorderRadius / distance < 0.02) {
|
|
988
|
+
tStart = Math.min(0.5, srcBorderRadius / distance);
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
let lo = 0.0, hi = 0.5;
|
|
992
|
+
for (let i = 0; i < 10; i++) {
|
|
993
|
+
const mid = (lo + hi) / 2;
|
|
994
|
+
const um = 1 - mid;
|
|
995
|
+
const qx = um * um * start.x + 2 * um * mid * controlX + mid * mid * end.x;
|
|
996
|
+
const qy = um * um * start.y + 2 * um * mid * controlY + mid * mid * end.y;
|
|
997
|
+
const dxSrc = qx - start.x;
|
|
998
|
+
const dySrc = qy - start.y;
|
|
999
|
+
if (dxSrc * dxSrc + dySrc * dySrc < srcBorderRadiusSq)
|
|
1000
|
+
lo = mid;
|
|
1001
|
+
else
|
|
1002
|
+
hi = mid;
|
|
1003
|
+
if (hi - lo < 1e-3)
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
tStart = (lo + hi) / 2;
|
|
1007
|
+
}
|
|
1008
|
+
const uS = 1 - tStart;
|
|
1009
|
+
const gapStartX = uS * uS * start.x + 2 * uS * tStart * controlX + tStart * tStart * end.x;
|
|
1010
|
+
const gapStartY = uS * uS * start.y + 2 * uS * tStart * controlY + tStart * tStart * end.y;
|
|
1011
|
+
const tArrowPrime = tStart < tArrow ? (tArrow - tStart) / (1 - tStart) : 0;
|
|
1012
|
+
const newP1X = (1 - tStart) * controlX + tStart * end.x;
|
|
1013
|
+
const newP1Y = (1 - tStart) * controlY + tStart * end.y;
|
|
1014
|
+
const subCtrlX = (1 - tArrowPrime) * gapStartX + tArrowPrime * newP1X;
|
|
1015
|
+
const subCtrlY = (1 - tArrowPrime) * gapStartY + tArrowPrime * newP1Y;
|
|
1016
|
+
ctx.moveTo(gapStartX, gapStartY);
|
|
1017
|
+
ctx.quadraticCurveTo(subCtrlX, subCtrlY, tipX, tipY);
|
|
864
1018
|
}
|
|
865
1019
|
}
|
|
866
1020
|
ctx.stroke();
|
|
@@ -976,7 +1130,6 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
976
1130
|
this.config.onEngineStop();
|
|
977
1131
|
}
|
|
978
1132
|
})
|
|
979
|
-
.linkLineDash((link) => this.config.linkLineDash?.(link) ?? null)
|
|
980
1133
|
.nodeCanvasObject((node, ctx) => {
|
|
981
1134
|
if (this.config.node) {
|
|
982
1135
|
this.config.node.nodeCanvasObject(node, ctx);
|
|
@@ -987,28 +1140,28 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
987
1140
|
})
|
|
988
1141
|
.linkCanvasObject((link, ctx, globalScale) => {
|
|
989
1142
|
if (this.config.link) {
|
|
990
|
-
this.config.link.linkCanvasObject(link, ctx);
|
|
1143
|
+
this.config.link.linkCanvasObject(link, ctx, globalScale);
|
|
991
1144
|
}
|
|
992
1145
|
else {
|
|
993
1146
|
this.drawLink(link, ctx, globalScale);
|
|
994
1147
|
}
|
|
995
|
-
})
|
|
996
|
-
|
|
997
|
-
|
|
1148
|
+
});
|
|
1149
|
+
if (this.config.node) {
|
|
1150
|
+
this.graph.nodePointerAreaPaint((node, color, ctx) => {
|
|
998
1151
|
this.config.node.nodePointerAreaPaint(node, color, ctx);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
this.graph.nodePointerAreaPaint();
|
|
1156
|
+
}
|
|
1157
|
+
if (this.config.link) {
|
|
1158
|
+
this.graph.linkPointerAreaPaint((link, color, ctx) => {
|
|
1006
1159
|
this.config.link.linkPointerAreaPaint(link, color, ctx);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
this.graph.linkPointerAreaPaint();
|
|
1164
|
+
}
|
|
1012
1165
|
}
|
|
1013
1166
|
updateTooltipStyles() {
|
|
1014
1167
|
if (!this.shadowRoot)
|