@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/src/canvas.ts
CHANGED
|
@@ -24,12 +24,16 @@ import {
|
|
|
24
24
|
} from "./canvas-utils.js";
|
|
25
25
|
|
|
26
26
|
const PADDING = 2;
|
|
27
|
-
|
|
28
27
|
// Arrow geometry constants (shared by self-loop and regular-link drawing paths)
|
|
29
28
|
const ARROW_WH_RATIO = 1.6;
|
|
30
29
|
const ARROW_VLEN_RATIO = 0.2;
|
|
31
30
|
// Multiplier to convert node size → cubic bezier control-point distance for self-loops
|
|
32
31
|
const SELF_LOOP_CURVE_FACTOR = 11.67;
|
|
32
|
+
// Base font size used for the initial measurement and for two-line text.
|
|
33
|
+
const NODE_FONT_SIZE_BASE = 2;
|
|
34
|
+
// Fraction of the chord width that single-line text should fill (0–1).
|
|
35
|
+
// Leaves (1 - ratio)/2 of the radius as horizontal padding on each side.
|
|
36
|
+
const NODE_TEXT_FILL_RATIO = 0.85;
|
|
33
37
|
|
|
34
38
|
// Force constants
|
|
35
39
|
const CHARGE_STRENGTH = -400;
|
|
@@ -96,6 +100,9 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
96
100
|
|
|
97
101
|
private nodeDegreeMap: Map<number, number> = new Map();
|
|
98
102
|
|
|
103
|
+
// Per-node font size cache: computed once per node, read every frame.
|
|
104
|
+
private nodeDisplayFontSize: Map<number, number> = new Map();
|
|
105
|
+
|
|
99
106
|
private relationshipsTextCache: Map<
|
|
100
107
|
string,
|
|
101
108
|
{
|
|
@@ -105,6 +112,12 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
105
112
|
}
|
|
106
113
|
> = new Map();
|
|
107
114
|
|
|
115
|
+
private onFontsLoadingDone = () => {
|
|
116
|
+
this.relationshipsTextCache.clear();
|
|
117
|
+
this.nodeDisplayFontSize.clear();
|
|
118
|
+
this.triggerRender();
|
|
119
|
+
};
|
|
120
|
+
|
|
108
121
|
private viewport: ViewportState;
|
|
109
122
|
|
|
110
123
|
constructor() {
|
|
@@ -148,10 +161,16 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
148
161
|
|
|
149
162
|
this.log('Component connected to DOM');
|
|
150
163
|
this.render();
|
|
164
|
+
|
|
165
|
+
// Text measurements taken before the custom font finishes loading use the
|
|
166
|
+
// fallback system font and produce wrong widths that get locked in the cache.
|
|
167
|
+
// Re-measure on every font-load batch (including the initial one).
|
|
168
|
+
document.fonts.addEventListener("loadingdone", this.onFontsLoadingDone);
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
disconnectedCallback() {
|
|
154
172
|
this.log('Component disconnected from DOM');
|
|
173
|
+
document.fonts.removeEventListener("loadingdone", this.onFontsLoadingDone);
|
|
155
174
|
if (this.resizeObserver) {
|
|
156
175
|
this.resizeObserver.disconnect();
|
|
157
176
|
this.resizeObserver = null;
|
|
@@ -169,7 +188,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
169
188
|
// Update event handlers if they were provided
|
|
170
189
|
if (config.onNodeClick || config.onLinkClick || config.onNodeRightClick || config.onLinkRightClick ||
|
|
171
190
|
config.onNodeHover || config.onLinkHover || config.onBackgroundClick || config.onBackgroundRightClick || config.onZoom ||
|
|
172
|
-
config.onEngineStop || config.isNodeSelected || config.isLinkSelected || config.
|
|
191
|
+
config.onEngineStop || config.isNodeSelected || config.isLinkSelected || config.node || config.link) {
|
|
173
192
|
this.log('Updating event handlers');
|
|
174
193
|
this.updateEventHandlers();
|
|
175
194
|
}
|
|
@@ -574,7 +593,6 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
574
593
|
this.config.onEngineStop();
|
|
575
594
|
}
|
|
576
595
|
})
|
|
577
|
-
.linkLineDash((link: GraphLink) => this.config.linkLineDash?.(link) ?? null)
|
|
578
596
|
.nodeCanvasObject((node: GraphNode, ctx: CanvasRenderingContext2D) => {
|
|
579
597
|
if (this.config.node) {
|
|
580
598
|
this.config.node.nodeCanvasObject(node, ctx);
|
|
@@ -584,7 +602,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
584
602
|
})
|
|
585
603
|
.linkCanvasObject((link: GraphLink, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
|
586
604
|
if (this.config.link) {
|
|
587
|
-
this.config.link.linkCanvasObject(link, ctx);
|
|
605
|
+
this.config.link.linkCanvasObject(link, ctx, globalScale);
|
|
588
606
|
} else {
|
|
589
607
|
this.drawLink(link, ctx, globalScale);
|
|
590
608
|
}
|
|
@@ -604,7 +622,6 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
604
622
|
}
|
|
605
623
|
});
|
|
606
624
|
|
|
607
|
-
|
|
608
625
|
// Setup forces
|
|
609
626
|
this.setupForces();
|
|
610
627
|
this.log('Force graph initialization complete');
|
|
@@ -661,7 +678,11 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
661
678
|
}
|
|
662
679
|
|
|
663
680
|
private drawNode(node: GraphNode, ctx: CanvasRenderingContext2D) {
|
|
664
|
-
|
|
681
|
+
|
|
682
|
+
if (node.x === undefined || node.y === undefined) {
|
|
683
|
+
node.x = 0;
|
|
684
|
+
node.y = 0;
|
|
685
|
+
}
|
|
665
686
|
|
|
666
687
|
ctx.lineWidth = this.config.isNodeSelected?.(node) ? 1 : 0.5;
|
|
667
688
|
ctx.strokeStyle = this.config.foregroundColor;
|
|
@@ -681,15 +702,34 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
681
702
|
ctx.fillStyle = getContrastTextColor(node.color);
|
|
682
703
|
ctx.textAlign = "center";
|
|
683
704
|
ctx.textBaseline = "middle";
|
|
684
|
-
ctx.font = "400 2px SofiaSans";
|
|
685
705
|
|
|
686
706
|
let [line1, line2] = node.displayName;
|
|
707
|
+
const textRadius = node.size - PADDING / 2;
|
|
687
708
|
|
|
688
709
|
if (!line1 && !line2) {
|
|
689
710
|
const text = getNodeDisplayText(node, this.config.captionsKeys, this.config.showPropertyKeyPrefix);
|
|
690
|
-
|
|
711
|
+
|
|
712
|
+
// Measure at the base (smallest) size — one cheap measurement.
|
|
713
|
+
ctx.font = `400 ${NODE_FONT_SIZE_BASE}px SofiaSans`;
|
|
691
714
|
[line1, line2] = wrapTextForCircularNode(ctx, text, textRadius);
|
|
715
|
+
|
|
716
|
+
let chosenSize = NODE_FONT_SIZE_BASE;
|
|
717
|
+
if (!line2) {
|
|
718
|
+
// Single-line: scale up so the text fills NODE_TEXT_FILL_RATIO of the
|
|
719
|
+
// available chord. Font metrics scale linearly so this is exact.
|
|
720
|
+
const measuredWidth = ctx.measureText(line1).width;
|
|
721
|
+
if (measuredWidth > 0) {
|
|
722
|
+
chosenSize = NODE_FONT_SIZE_BASE * (NODE_TEXT_FILL_RATIO * 2 * textRadius / measuredWidth);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
ctx.font = `400 ${chosenSize}px SofiaSans`;
|
|
692
727
|
node.displayName = [line1, line2];
|
|
728
|
+
this.nodeDisplayFontSize.set(node.id, chosenSize);
|
|
729
|
+
} else {
|
|
730
|
+
// Cache hit: the font size was stored when displayName was first computed.
|
|
731
|
+
const chosenSize = this.nodeDisplayFontSize.get(node.id) ?? NODE_FONT_SIZE_BASE;
|
|
732
|
+
ctx.font = `400 ${chosenSize}px SofiaSans`;
|
|
693
733
|
}
|
|
694
734
|
|
|
695
735
|
const textMetrics = ctx.measureText(line1);
|
|
@@ -707,7 +747,10 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
707
747
|
}
|
|
708
748
|
|
|
709
749
|
private pointerNode(node: GraphNode, color: string, ctx: CanvasRenderingContext2D) {
|
|
710
|
-
if (node.x
|
|
750
|
+
if (node.x === undefined || node.y === undefined) {
|
|
751
|
+
node.x = 0;
|
|
752
|
+
node.y = 0;
|
|
753
|
+
};
|
|
711
754
|
|
|
712
755
|
const radius = node.size + PADDING;
|
|
713
756
|
|
|
@@ -721,29 +764,41 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
721
764
|
const start = link.source;
|
|
722
765
|
const end = link.target;
|
|
723
766
|
|
|
724
|
-
if (start.x
|
|
767
|
+
if (start.x === undefined || start.y === undefined || end.x === undefined || end.y === undefined) {
|
|
768
|
+
start.x = 0;
|
|
769
|
+
start.y = 0;
|
|
770
|
+
end.x = 0;
|
|
771
|
+
end.y = 0;
|
|
772
|
+
}
|
|
725
773
|
|
|
726
774
|
let textX;
|
|
727
775
|
let textY;
|
|
728
776
|
let angle;
|
|
729
777
|
|
|
778
|
+
const isLinkSelected = this.config.isLinkSelected?.(link) ?? false;
|
|
779
|
+
const arrowLen = isLinkSelected ? 4 : 2;
|
|
780
|
+
|
|
781
|
+
// Deferred arrowhead — drawn after the label so it is never covered by
|
|
782
|
+
// the label background rect (which happens for short links where the
|
|
783
|
+
// bezier midpoint and the arrow tip are at almost the same position).
|
|
784
|
+
let pendingArrow: { tipX: number; tipY: number; nx: number; ny: number; arrowLen: number; arrowHalfWidth: number } | null = null;
|
|
785
|
+
|
|
730
786
|
if (start.id === end.id) {
|
|
731
787
|
const nodeSize = start.size || 6;
|
|
732
788
|
const d = (link.curve || 0) * nodeSize * SELF_LOOP_CURVE_FACTOR;
|
|
733
789
|
|
|
734
|
-
ctx.lineWidth = (
|
|
735
|
-
ctx.setLineDash(this.config.linkLineDash
|
|
790
|
+
ctx.lineWidth = (isLinkSelected ? 2 : 1) / globalScale;
|
|
791
|
+
if (this.config.linkLineDash) ctx.setLineDash(this.config.linkLineDash(link));
|
|
736
792
|
|
|
737
793
|
// The visible outer edge of the node border is nodeSize + strokeWidth
|
|
738
794
|
// (stroke is centered on nodeSize + strokeWidth/2, so outer edge = nodeSize + strokeWidth).
|
|
739
795
|
const nodeStrokeWidth = this.config.isNodeSelected?.(start) ? 1 : 0.5;
|
|
740
|
-
const borderRadius = nodeSize + nodeStrokeWidth;
|
|
796
|
+
const borderRadius = nodeSize + nodeStrokeWidth + PADDING;
|
|
741
797
|
|
|
742
798
|
// Binary search for tArrow near 1.0 where the curve is at distance borderRadius
|
|
743
799
|
// from the node center (i.e. on the outer edge of the node border stroke).
|
|
744
800
|
// Bezier parametric form: Bx(t)=sx+3(1-t)t²d, By(t)=sy-3(1-t)²td
|
|
745
801
|
// dist(t) = 3*(1-t)*t*|d|*sqrt(t² + (1-t)²)
|
|
746
|
-
const arrowLen = (this.config.isLinkSelected?.(link) ? 4 : 2) / globalScale;
|
|
747
802
|
const arrowHalfWidth = arrowLen / ARROW_WH_RATIO / 2;
|
|
748
803
|
let lo = 0.5, hi = 1.0;
|
|
749
804
|
const absD = Math.abs(d);
|
|
@@ -803,23 +858,7 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
803
858
|
if (tLen !== 0 && canReachBorder) {
|
|
804
859
|
const nx = tdx / tLen;
|
|
805
860
|
const ny = tdy / tLen;
|
|
806
|
-
|
|
807
|
-
ctx.fillStyle = link.color;
|
|
808
|
-
ctx.beginPath();
|
|
809
|
-
ctx.moveTo(tipX, tipY);
|
|
810
|
-
ctx.lineTo(
|
|
811
|
-
tipX - nx * arrowLen + ny * arrowHalfWidth,
|
|
812
|
-
tipY - ny * arrowLen - nx * arrowHalfWidth,
|
|
813
|
-
);
|
|
814
|
-
ctx.lineTo(
|
|
815
|
-
tipX - nx * arrowLen * (1 - ARROW_VLEN_RATIO),
|
|
816
|
-
tipY - ny * arrowLen * (1 - ARROW_VLEN_RATIO),
|
|
817
|
-
);
|
|
818
|
-
ctx.lineTo(
|
|
819
|
-
tipX - nx * arrowLen - ny * arrowHalfWidth,
|
|
820
|
-
tipY - ny * arrowLen + nx * arrowHalfWidth,
|
|
821
|
-
);
|
|
822
|
-
ctx.fill();
|
|
861
|
+
pendingArrow = { tipX, tipY, nx, ny, arrowLen, arrowHalfWidth };
|
|
823
862
|
}
|
|
824
863
|
|
|
825
864
|
// Midpoint of cubic bezier: P0=(sx,sy), P1=(sx,sy-d), P2=(sx+d,sy), P3=(sx,sy)
|
|
@@ -868,24 +907,14 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
868
907
|
if (angle > Math.PI / 2) angle = -(Math.PI - angle);
|
|
869
908
|
if (angle < -Math.PI / 2) angle = -(-Math.PI - angle);
|
|
870
909
|
|
|
871
|
-
//
|
|
872
|
-
// Scale arrow geometry by 1/globalScale so arrowheads remain visually
|
|
873
|
-
// consistent across zoom levels, matching ctx.lineWidth / globalScale.
|
|
874
|
-
const baseArrowLen = this.config.isLinkSelected?.(link) ? 4 : 2;
|
|
875
|
-
const arrowLen = baseArrowLen / globalScale;
|
|
910
|
+
// Draw regular link line and arrowhead
|
|
876
911
|
const arrowHalfWidth = arrowLen / ARROW_WH_RATIO / 2;
|
|
877
912
|
|
|
878
|
-
//
|
|
879
|
-
// Q(t) = (1-t)²·start + 2(1-t)t·control + t²·end
|
|
880
|
-
// is at distance borderRadius from the target node centre.
|
|
913
|
+
// Target-side clip: find t where bezier enters target node border + PADDING
|
|
881
914
|
const endNodeSize = end.size || 6;
|
|
882
|
-
const
|
|
883
|
-
const borderRadius = endNodeSize + endNodeStrokeWidth;
|
|
915
|
+
const borderRadius = endNodeSize + (this.config.isNodeSelected?.(end) ? 1 : 0.5) + PADDING;
|
|
884
916
|
const borderRadiusSq = borderRadius * borderRadius;
|
|
885
917
|
|
|
886
|
-
// When borderRadius is small relative to the chord length, the bezier and
|
|
887
|
-
// chord diverge only near t=1, so a linear approximation is accurate and
|
|
888
|
-
// avoids the per-frame search cost on large graphs.
|
|
889
918
|
let tArrow: number;
|
|
890
919
|
if (borderRadius / distance < 0.02) {
|
|
891
920
|
tArrow = Math.min(1, Math.max(0, 1 - borderRadius / distance));
|
|
@@ -906,25 +935,57 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
906
935
|
}
|
|
907
936
|
const uArrow = 1 - tArrow;
|
|
908
937
|
|
|
909
|
-
// Tip = Q(tArrow)
|
|
910
938
|
const tipX = uArrow * uArrow * start.x + 2 * uArrow * tArrow * controlX + tArrow * tArrow * end.x;
|
|
911
939
|
const tipY = uArrow * uArrow * start.y + 2 * uArrow * tArrow * controlY + tArrow * tArrow * end.y;
|
|
912
940
|
|
|
913
|
-
//
|
|
914
|
-
|
|
915
|
-
const
|
|
916
|
-
const
|
|
941
|
+
// Source-side clip: find t where bezier exits source node border + PADDING
|
|
942
|
+
const startNodeSize = start.size || 6;
|
|
943
|
+
const srcBorderRadius = startNodeSize + (this.config.isNodeSelected?.(start) ? 1 : 0.5) + PADDING;
|
|
944
|
+
const srcBorderRadiusSq = srcBorderRadius * srcBorderRadius;
|
|
945
|
+
|
|
946
|
+
let tStart = 0;
|
|
947
|
+
if (srcBorderRadius / distance < 0.02) {
|
|
948
|
+
tStart = Math.min(0.5, srcBorderRadius / distance);
|
|
949
|
+
} else {
|
|
950
|
+
let lo = 0.0, hi = 0.5;
|
|
951
|
+
for (let i = 0; i < 10; i++) {
|
|
952
|
+
const mid = (lo + hi) / 2;
|
|
953
|
+
const um = 1 - mid;
|
|
954
|
+
const qx = um * um * start.x + 2 * um * mid * controlX + mid * mid * end.x;
|
|
955
|
+
const qy = um * um * start.y + 2 * um * mid * controlY + mid * mid * end.y;
|
|
956
|
+
const dxSrc = qx - start.x;
|
|
957
|
+
const dySrc = qy - start.y;
|
|
958
|
+
if (dxSrc * dxSrc + dySrc * dySrc < srcBorderRadiusSq) lo = mid;
|
|
959
|
+
else hi = mid;
|
|
960
|
+
if (hi - lo < 1e-3) break;
|
|
961
|
+
}
|
|
962
|
+
tStart = (lo + hi) / 2;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Gap start point: Q(tStart)
|
|
966
|
+
const uS = 1 - tStart;
|
|
967
|
+
const gapStartX = uS * uS * start.x + 2 * uS * tStart * controlX + tStart * tStart * end.x;
|
|
968
|
+
const gapStartY = uS * uS * start.y + 2 * uS * tStart * controlY + tStart * tStart * end.y;
|
|
969
|
+
|
|
970
|
+
// Sub-bezier [tStart, tArrow] control point via De Casteljau:
|
|
971
|
+
// Right sub-bezier at tStart → NewP1 = lerp(control, end, tStart)
|
|
972
|
+
// Left sub-curve at tArrow' = (tArrow-tStart)/(1-tStart) → ctrl = lerp(gapStart, NewP1, tArrow')
|
|
973
|
+
const tArrowPrime = tStart < tArrow ? (tArrow - tStart) / (1 - tStart) : 0;
|
|
974
|
+
const newP1X = (1 - tStart) * controlX + tStart * end.x;
|
|
975
|
+
const newP1Y = (1 - tStart) * controlY + tStart * end.y;
|
|
976
|
+
const subCtrlX = (1 - tArrowPrime) * gapStartX + tArrowPrime * newP1X;
|
|
977
|
+
const subCtrlY = (1 - tArrowPrime) * gapStartY + tArrowPrime * newP1Y;
|
|
917
978
|
|
|
918
979
|
ctx.strokeStyle = link.color;
|
|
919
|
-
ctx.lineWidth = (
|
|
980
|
+
ctx.lineWidth = (isLinkSelected ? 2 : 1) / globalScale;
|
|
981
|
+
|
|
920
982
|
ctx.setLineDash(this.config.linkLineDash?.(link) ?? []);
|
|
921
983
|
ctx.beginPath();
|
|
922
|
-
ctx.moveTo(
|
|
923
|
-
ctx.quadraticCurveTo(
|
|
984
|
+
ctx.moveTo(gapStartX, gapStartY);
|
|
985
|
+
ctx.quadraticCurveTo(subCtrlX, subCtrlY, tipX, tipY);
|
|
924
986
|
ctx.stroke();
|
|
925
987
|
ctx.setLineDash([]);
|
|
926
988
|
|
|
927
|
-
// Arrowhead tangent at tArrow: Q'(t) = 2(1-t)(control-start) + 2t(end-control)
|
|
928
989
|
const atx = 2 * uArrow * (controlX - start.x) + 2 * tArrow * (end.x - controlX);
|
|
929
990
|
const aty = 2 * uArrow * (controlY - start.y) + 2 * tArrow * (end.y - controlY);
|
|
930
991
|
const atLen = Math.sqrt(atx * atx + aty * aty);
|
|
@@ -932,40 +993,39 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
932
993
|
if (atLen !== 0) {
|
|
933
994
|
const nx = atx / atLen;
|
|
934
995
|
const ny = aty / atLen;
|
|
935
|
-
|
|
936
|
-
ctx.fillStyle = link.color;
|
|
937
|
-
ctx.beginPath();
|
|
938
|
-
ctx.moveTo(tipX, tipY);
|
|
939
|
-
ctx.lineTo(tipX - nx * arrowLen + ny * arrowHalfWidth, tipY - ny * arrowLen - nx * arrowHalfWidth);
|
|
940
|
-
ctx.lineTo(
|
|
941
|
-
tipX - nx * arrowLen * (1 - ARROW_VLEN_RATIO),
|
|
942
|
-
tipY - ny * arrowLen * (1 - ARROW_VLEN_RATIO),
|
|
943
|
-
);
|
|
944
|
-
ctx.lineTo(tipX - nx * arrowLen - ny * arrowHalfWidth, tipY - ny * arrowLen + nx * arrowHalfWidth);
|
|
945
|
-
ctx.fill();
|
|
996
|
+
pendingArrow = { tipX, tipY, nx, ny, arrowLen, arrowHalfWidth };
|
|
946
997
|
}
|
|
947
998
|
}
|
|
948
999
|
|
|
949
|
-
ctx.font = "400 2px SofiaSans";
|
|
1000
|
+
ctx.font = isLinkSelected ? "700 2px SofiaSans" : "400 2px SofiaSans";
|
|
950
1001
|
ctx.textAlign = "center";
|
|
951
1002
|
// Draw text with alphabetic baseline, positioned so visual center is at y=0
|
|
952
1003
|
ctx.textBaseline = "alphabetic";
|
|
953
1004
|
|
|
954
|
-
|
|
1005
|
+
// Separate cache entries per weight so each state is measured with its own
|
|
1006
|
+
// font, giving equal visual padding regardless of selection state.
|
|
1007
|
+
const cacheKey = `${link.relationship}_${isLinkSelected ? "700" : "400"}`;
|
|
1008
|
+
let cached = this.relationshipsTextCache.get(cacheKey);
|
|
955
1009
|
|
|
956
1010
|
if (!cached) {
|
|
1011
|
+
// ctx.font is already set to the correct weight above; measure it directly.
|
|
957
1012
|
const metrics = ctx.measureText(link.relationship);
|
|
958
|
-
// Use
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
//
|
|
962
|
-
const
|
|
1013
|
+
// Use actual ink bounds for vertical metrics; fontBoundingBox* is the full
|
|
1014
|
+
// line-box and adds excessive space for lighter weights.
|
|
1015
|
+
// Use metrics.width for horizontal extent: actualBoundingBoxLeft/Right are
|
|
1016
|
+
// unreliable with textAlign="center" and can double the value on some engines.
|
|
1017
|
+
const inkAscent = metrics.actualBoundingBoxAscent ?? metrics.fontBoundingBoxAscent;
|
|
1018
|
+
const inkDescent = metrics.actualBoundingBoxDescent ?? metrics.fontBoundingBoxDescent;
|
|
1019
|
+
const inkWidth = metrics.width;
|
|
1020
|
+
const bgPadding = 0.3;
|
|
1021
|
+
|
|
963
1022
|
cached = {
|
|
964
|
-
textWidth:
|
|
965
|
-
textHeight:
|
|
966
|
-
|
|
1023
|
+
textWidth: inkWidth + bgPadding * 2,
|
|
1024
|
+
textHeight: inkAscent + inkDescent + bgPadding * 2,
|
|
1025
|
+
// Shift baseline up so the ink block is centred inside the bg rect.
|
|
1026
|
+
textYOffset: (inkAscent - inkDescent) / 2,
|
|
967
1027
|
};
|
|
968
|
-
this.relationshipsTextCache.set(
|
|
1028
|
+
this.relationshipsTextCache.set(cacheKey, cached);
|
|
969
1029
|
}
|
|
970
1030
|
|
|
971
1031
|
const { textWidth, textHeight, textYOffset } = cached;
|
|
@@ -988,6 +1048,18 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
988
1048
|
ctx.fillStyle = getContrastTextColor(this.config.backgroundColor);
|
|
989
1049
|
ctx.fillText(link.relationship, 0, textYOffset);
|
|
990
1050
|
ctx.restore();
|
|
1051
|
+
|
|
1052
|
+
// Draw arrowhead last so it always appears on top of the label background.
|
|
1053
|
+
if (pendingArrow) {
|
|
1054
|
+
const { tipX, tipY, nx, ny, arrowLen: aLen, arrowHalfWidth: aHW } = pendingArrow;
|
|
1055
|
+
ctx.fillStyle = link.color;
|
|
1056
|
+
ctx.beginPath();
|
|
1057
|
+
ctx.moveTo(tipX, tipY);
|
|
1058
|
+
ctx.lineTo(tipX - nx * aLen + ny * aHW, tipY - ny * aLen - nx * aHW);
|
|
1059
|
+
ctx.lineTo(tipX - nx * aLen * (1 - ARROW_VLEN_RATIO), tipY - ny * aLen * (1 - ARROW_VLEN_RATIO));
|
|
1060
|
+
ctx.lineTo(tipX - nx * aLen - ny * aHW, tipY - ny * aLen + nx * aHW);
|
|
1061
|
+
ctx.fill();
|
|
1062
|
+
}
|
|
991
1063
|
}
|
|
992
1064
|
|
|
993
1065
|
private pointerLink(link: GraphLink, color: string, ctx: CanvasRenderingContext2D) {
|
|
@@ -1010,13 +1082,43 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1010
1082
|
ctx.beginPath();
|
|
1011
1083
|
|
|
1012
1084
|
if (start.id === end.id) {
|
|
1013
|
-
// Self-loop: replicate
|
|
1085
|
+
// Self-loop: replicate exact cubic bezier clip from drawLink
|
|
1014
1086
|
const nodeSize = start.size || 6;
|
|
1015
1087
|
const d = (link.curve || 0) * nodeSize * SELF_LOOP_CURVE_FACTOR;
|
|
1088
|
+
|
|
1089
|
+
const nodeStrokeWidth = this.config.isNodeSelected?.(start) ? 1 : 0.5;
|
|
1090
|
+
const borderRadius = nodeSize + nodeStrokeWidth + PADDING;
|
|
1091
|
+
const absD = Math.abs(d);
|
|
1092
|
+
const maxReachableDist = 3 * 0.5 * 0.5 * absD * Math.sqrt(0.5);
|
|
1093
|
+
const canReachBorder = absD > 0 && maxReachableDist >= borderRadius;
|
|
1094
|
+
|
|
1016
1095
|
ctx.moveTo(start.x, start.y);
|
|
1017
|
-
|
|
1096
|
+
if (canReachBorder) {
|
|
1097
|
+
let lo = 0.5, hi = 1.0;
|
|
1098
|
+
for (let i = 0; i < 20; i++) {
|
|
1099
|
+
const mid = (lo + hi) / 2;
|
|
1100
|
+
const um = 1 - mid;
|
|
1101
|
+
const dist = 3 * um * mid * absD * Math.sqrt(mid * mid + um * um);
|
|
1102
|
+
if (dist > borderRadius) lo = mid;
|
|
1103
|
+
else hi = mid;
|
|
1104
|
+
}
|
|
1105
|
+
const tArrow = (lo + hi) / 2;
|
|
1106
|
+
const uArrow = 1 - tArrow;
|
|
1107
|
+
const tipX = start.x + 3 * uArrow * tArrow * tArrow * d;
|
|
1108
|
+
const tipY = start.y - 3 * uArrow * uArrow * tArrow * d;
|
|
1109
|
+
ctx.bezierCurveTo(
|
|
1110
|
+
start.x,
|
|
1111
|
+
start.y - tArrow * d,
|
|
1112
|
+
start.x + tArrow * tArrow * d,
|
|
1113
|
+
start.y - 2 * tArrow * uArrow * d,
|
|
1114
|
+
tipX,
|
|
1115
|
+
tipY,
|
|
1116
|
+
);
|
|
1117
|
+
} else {
|
|
1118
|
+
ctx.bezierCurveTo(start.x, start.y - d, start.x + d, start.y, start.x, start.y);
|
|
1119
|
+
}
|
|
1018
1120
|
} else {
|
|
1019
|
-
// Regular link: replicate
|
|
1121
|
+
// Regular link: replicate exact quadratic bezier clip from drawLink
|
|
1020
1122
|
const dx = end.x - start.x;
|
|
1021
1123
|
const dy = end.y - start.y;
|
|
1022
1124
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
@@ -1031,18 +1133,69 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1031
1133
|
const controlX = (start.x + end.x) / 2 + perpX * curvature * distance;
|
|
1032
1134
|
const controlY = (start.y + end.y) / 2 + perpY * curvature * distance;
|
|
1033
1135
|
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
const
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1136
|
+
// Use the same borderRadius and binary-search clip as drawLink
|
|
1137
|
+
const endNodeSize = end.size || 6;
|
|
1138
|
+
const borderRadius = endNodeSize + (this.config.isNodeSelected?.(end) ? 1 : 0.5) + PADDING;
|
|
1139
|
+
const borderRadiusSq = borderRadius * borderRadius;
|
|
1140
|
+
|
|
1141
|
+
let tArrow: number;
|
|
1142
|
+
if (borderRadius / distance < 0.02) {
|
|
1143
|
+
tArrow = Math.min(1, Math.max(0, 1 - borderRadius / distance));
|
|
1144
|
+
} else {
|
|
1145
|
+
let lo = 0.5, hi = 1.0;
|
|
1146
|
+
for (let i = 0; i < 10; i++) {
|
|
1147
|
+
const mid = (lo + hi) / 2;
|
|
1148
|
+
const um = 1 - mid;
|
|
1149
|
+
const qx = um * um * start.x + 2 * um * mid * controlX + mid * mid * end.x;
|
|
1150
|
+
const qy = um * um * start.y + 2 * um * mid * controlY + mid * mid * end.y;
|
|
1151
|
+
const dxEnd = qx - end.x;
|
|
1152
|
+
const dyEnd = qy - end.y;
|
|
1153
|
+
if (dxEnd * dxEnd + dyEnd * dyEnd > borderRadiusSq) lo = mid;
|
|
1154
|
+
else hi = mid;
|
|
1155
|
+
if (hi - lo < 1e-3) break;
|
|
1156
|
+
}
|
|
1157
|
+
tArrow = (lo + hi) / 2;
|
|
1158
|
+
}
|
|
1159
|
+
const uArrow = 1 - tArrow;
|
|
1160
|
+
const tipX = uArrow * uArrow * start.x + 2 * uArrow * tArrow * controlX + tArrow * tArrow * end.x;
|
|
1161
|
+
const tipY = uArrow * uArrow * start.y + 2 * uArrow * tArrow * controlY + tArrow * tArrow * end.y;
|
|
1162
|
+
|
|
1163
|
+
// Source-side clip: mirror of drawLink source gap
|
|
1164
|
+
const startNodeSize = start.size || 6;
|
|
1165
|
+
const srcBorderRadius = startNodeSize + (this.config.isNodeSelected?.(start) ? 1 : 0.5) + PADDING;
|
|
1166
|
+
const srcBorderRadiusSq = srcBorderRadius * srcBorderRadius;
|
|
1167
|
+
|
|
1168
|
+
let tStart = 0;
|
|
1169
|
+
if (srcBorderRadius / distance < 0.02) {
|
|
1170
|
+
tStart = Math.min(0.5, srcBorderRadius / distance);
|
|
1171
|
+
} else {
|
|
1172
|
+
let lo = 0.0, hi = 0.5;
|
|
1173
|
+
for (let i = 0; i < 10; i++) {
|
|
1174
|
+
const mid = (lo + hi) / 2;
|
|
1175
|
+
const um = 1 - mid;
|
|
1176
|
+
const qx = um * um * start.x + 2 * um * mid * controlX + mid * mid * end.x;
|
|
1177
|
+
const qy = um * um * start.y + 2 * um * mid * controlY + mid * mid * end.y;
|
|
1178
|
+
const dxSrc = qx - start.x;
|
|
1179
|
+
const dySrc = qy - start.y;
|
|
1180
|
+
if (dxSrc * dxSrc + dySrc * dySrc < srcBorderRadiusSq) lo = mid;
|
|
1181
|
+
else hi = mid;
|
|
1182
|
+
if (hi - lo < 1e-3) break;
|
|
1183
|
+
}
|
|
1184
|
+
tStart = (lo + hi) / 2;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const uS = 1 - tStart;
|
|
1188
|
+
const gapStartX = uS * uS * start.x + 2 * uS * tStart * controlX + tStart * tStart * end.x;
|
|
1189
|
+
const gapStartY = uS * uS * start.y + 2 * uS * tStart * controlY + tStart * tStart * end.y;
|
|
1190
|
+
|
|
1191
|
+
const tArrowPrime = tStart < tArrow ? (tArrow - tStart) / (1 - tStart) : 0;
|
|
1192
|
+
const newP1X = (1 - tStart) * controlX + tStart * end.x;
|
|
1193
|
+
const newP1Y = (1 - tStart) * controlY + tStart * end.y;
|
|
1194
|
+
const subCtrlX = (1 - tArrowPrime) * gapStartX + tArrowPrime * newP1X;
|
|
1195
|
+
const subCtrlY = (1 - tArrowPrime) * gapStartY + tArrowPrime * newP1Y;
|
|
1196
|
+
|
|
1197
|
+
ctx.moveTo(gapStartX, gapStartY);
|
|
1198
|
+
ctx.quadraticCurveTo(subCtrlX, subCtrlY, tipX, tipY);
|
|
1046
1199
|
}
|
|
1047
1200
|
}
|
|
1048
1201
|
|
|
@@ -1163,7 +1316,6 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1163
1316
|
this.config.onEngineStop();
|
|
1164
1317
|
}
|
|
1165
1318
|
})
|
|
1166
|
-
.linkLineDash((link: GraphLink) => this.config.linkLineDash?.(link) ?? null)
|
|
1167
1319
|
.nodeCanvasObject((node: GraphNode, ctx: CanvasRenderingContext2D) => {
|
|
1168
1320
|
if (this.config.node) {
|
|
1169
1321
|
this.config.node.nodeCanvasObject(node, ctx);
|
|
@@ -1173,25 +1325,27 @@ class FalkorDBCanvas extends HTMLElement {
|
|
|
1173
1325
|
})
|
|
1174
1326
|
.linkCanvasObject((link: GraphLink, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
|
1175
1327
|
if (this.config.link) {
|
|
1176
|
-
this.config.link.linkCanvasObject(link, ctx);
|
|
1328
|
+
this.config.link.linkCanvasObject(link, ctx, globalScale);
|
|
1177
1329
|
} else {
|
|
1178
1330
|
this.drawLink(link, ctx, globalScale);
|
|
1179
1331
|
}
|
|
1180
|
-
})
|
|
1181
|
-
.nodePointerAreaPaint((node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => {
|
|
1182
|
-
if (this.config.node) {
|
|
1183
|
-
this.config.node.nodePointerAreaPaint(node, color, ctx);
|
|
1184
|
-
} else {
|
|
1185
|
-
this.pointerNode(node, color, ctx);
|
|
1186
|
-
}
|
|
1187
|
-
})
|
|
1188
|
-
.linkPointerAreaPaint((link: GraphLink, color: string, ctx: CanvasRenderingContext2D) => {
|
|
1189
|
-
if (this.config.link) {
|
|
1190
|
-
this.config.link.linkPointerAreaPaint(link, color, ctx);
|
|
1191
|
-
} else {
|
|
1192
|
-
this.pointerLink(link, color, ctx);
|
|
1193
|
-
}
|
|
1194
1332
|
});
|
|
1333
|
+
|
|
1334
|
+
if (this.config.node) {
|
|
1335
|
+
this.graph.nodePointerAreaPaint((node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => {
|
|
1336
|
+
this.config.node!.nodePointerAreaPaint(node, color, ctx);
|
|
1337
|
+
});
|
|
1338
|
+
} else {
|
|
1339
|
+
this.graph.nodePointerAreaPaint();
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (this.config.link) {
|
|
1343
|
+
this.graph.linkPointerAreaPaint((link: GraphLink, color: string, ctx: CanvasRenderingContext2D) => {
|
|
1344
|
+
this.config.link!.linkPointerAreaPaint(link, color, ctx);
|
|
1345
|
+
});
|
|
1346
|
+
} else {
|
|
1347
|
+
this.graph.linkPointerAreaPaint();
|
|
1348
|
+
}
|
|
1195
1349
|
}
|
|
1196
1350
|
|
|
1197
1351
|
private updateTooltipStyles() {
|