@bruno00o/graph 0.1.0 → 0.3.0
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/README.md +114 -2
- package/dist/db.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{mount-graph-BHwq3LOr.js → mount-graph-CzVfiIte.js} +1389 -67
- package/dist/mount-graph-CzVfiIte.js.map +1 -0
- package/dist/{mount-graph-B88WGOQu.d.ts → mount-graph-DQLST9-_.d.ts} +13 -3
- package/dist/query.d.ts +1 -1
- package/dist/react.d.ts +3 -2
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/dist/{types-D6b86Rt1.d.ts → types-BaflHDaf.d.ts} +11 -4
- package/package.json +1 -1
- package/dist/mount-graph-BHwq3LOr.js.map +0 -1
|
@@ -449,8 +449,125 @@ function createGraphModel(options = {}) {
|
|
|
449
449
|
}
|
|
450
450
|
};
|
|
451
451
|
}
|
|
452
|
+
// src/core/lasso.ts
|
|
453
|
+
function insidePolygon(x, y, polygon) {
|
|
454
|
+
let inside = false;
|
|
455
|
+
let previous = polygon[polygon.length - 1];
|
|
456
|
+
for (const point of polygon) {
|
|
457
|
+
if (previous && point.y > y !== previous.y > y && x < (previous.x - point.x) * (y - point.y) / (previous.y - point.y) + point.x) inside = !inside;
|
|
458
|
+
previous = point;
|
|
459
|
+
}
|
|
460
|
+
return inside;
|
|
461
|
+
}
|
|
462
|
+
function nodesInside(polygon, nodes, positions) {
|
|
463
|
+
if (polygon.length < 3) return [];
|
|
464
|
+
let left = Number.POSITIVE_INFINITY;
|
|
465
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
466
|
+
let top = Number.POSITIVE_INFINITY;
|
|
467
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
468
|
+
for (const point of polygon) {
|
|
469
|
+
left = Math.min(left, point.x);
|
|
470
|
+
right = Math.max(right, point.x);
|
|
471
|
+
top = Math.min(top, point.y);
|
|
472
|
+
bottom = Math.max(bottom, point.y);
|
|
473
|
+
}
|
|
474
|
+
const inside = [];
|
|
475
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
476
|
+
if (nodes.alive[index] !== 1) continue;
|
|
477
|
+
const x = positions.x(index);
|
|
478
|
+
const y = positions.y(index);
|
|
479
|
+
if (x < left || x > right || y < top || y > bottom) continue;
|
|
480
|
+
const key = nodes.keys[index];
|
|
481
|
+
if (key !== void 0 && insidePolygon(x, y, polygon)) inside.push(key);
|
|
482
|
+
}
|
|
483
|
+
return inside;
|
|
484
|
+
}
|
|
485
|
+
// src/core/edge-curve.ts
|
|
486
|
+
const loopTurn = .6;
|
|
487
|
+
const loopGrowth = .5;
|
|
488
|
+
const maxBend = .12;
|
|
489
|
+
const spreadBend = .4;
|
|
490
|
+
function laneBend(position, count) {
|
|
491
|
+
if (count < 2) return 0;
|
|
492
|
+
const step = Math.min(maxBend, spreadBend / (count - 1));
|
|
493
|
+
return (position - (count - 1) / 2) * step;
|
|
494
|
+
}
|
|
495
|
+
function loopReach(radius, ordinal, minimum) {
|
|
496
|
+
return radius + Math.max(radius * 1.5, minimum) * (1 + loopGrowth * ordinal);
|
|
497
|
+
}
|
|
498
|
+
function parallelCurve(ax, ay, dx, dy, bend) {
|
|
499
|
+
const length = Math.hypot(dx - ax, dy - ay);
|
|
500
|
+
const nx = length > 0 ? -(dy - ay) / length : 0;
|
|
501
|
+
const ny = length > 0 ? (dx - ax) / length : 1;
|
|
502
|
+
const offset = 2 * bend * length;
|
|
503
|
+
const px = (ax + dx) / 2 + nx * offset;
|
|
504
|
+
const py = (ay + dy) / 2 + ny * offset;
|
|
505
|
+
return {
|
|
506
|
+
ax,
|
|
507
|
+
ay,
|
|
508
|
+
bx: ax + (px - ax) * 2 / 3,
|
|
509
|
+
by: ay + (py - ay) * 2 / 3,
|
|
510
|
+
cx: dx + (px - dx) * 2 / 3,
|
|
511
|
+
cy: dy + (py - dy) * 2 / 3,
|
|
512
|
+
dx,
|
|
513
|
+
dy
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function loopCurve(x, y, reach) {
|
|
517
|
+
const arm = reach / (.75 * Math.cos(loopTurn));
|
|
518
|
+
const ux = Math.SQRT1_2;
|
|
519
|
+
const uy = -Math.SQRT1_2;
|
|
520
|
+
const cos = Math.cos(loopTurn);
|
|
521
|
+
const sin = Math.sin(loopTurn);
|
|
522
|
+
return {
|
|
523
|
+
ax: x,
|
|
524
|
+
ay: y,
|
|
525
|
+
bx: x + arm * (ux * cos - uy * sin),
|
|
526
|
+
by: y + arm * (ux * sin + uy * cos),
|
|
527
|
+
cx: x + arm * (ux * cos + uy * sin),
|
|
528
|
+
cy: y + arm * (-ux * sin + uy * cos),
|
|
529
|
+
dx: x,
|
|
530
|
+
dy: y
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function pointOnCubic(curve, t) {
|
|
534
|
+
const u = 1 - t;
|
|
535
|
+
const a = u * u * u;
|
|
536
|
+
const b = 3 * u * u * t;
|
|
537
|
+
const c = 3 * u * t * t;
|
|
538
|
+
const d = t * t * t;
|
|
539
|
+
return {
|
|
540
|
+
x: a * curve.ax + b * curve.bx + c * curve.cx + d * curve.dx,
|
|
541
|
+
y: a * curve.ay + b * curve.by + c * curve.cy + d * curve.dy
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function sampleCubic(curve, segments) {
|
|
545
|
+
const points = new Float64Array((segments + 1) * 2);
|
|
546
|
+
for (let step = 0; step <= segments; step++) {
|
|
547
|
+
const point = pointOnCubic(curve, step / segments);
|
|
548
|
+
points[step * 2] = point.x;
|
|
549
|
+
points[step * 2 + 1] = point.y;
|
|
550
|
+
}
|
|
551
|
+
return points;
|
|
552
|
+
}
|
|
553
|
+
function distanceToCubic(curve, x, y, segments) {
|
|
554
|
+
const points = sampleCubic(curve, segments);
|
|
555
|
+
let best = Number.POSITIVE_INFINITY;
|
|
556
|
+
for (let step = 0; step < segments; step++) {
|
|
557
|
+
const ax = points[step * 2] ?? 0;
|
|
558
|
+
const ay = points[step * 2 + 1] ?? 0;
|
|
559
|
+
const vx = (points[step * 2 + 2] ?? 0) - ax;
|
|
560
|
+
const vy = (points[step * 2 + 3] ?? 0) - ay;
|
|
561
|
+
const squared = vx * vx + vy * vy;
|
|
562
|
+
const t = squared > 0 ? Math.min(1, Math.max(0, ((x - ax) * vx + (y - ay) * vy) / squared)) : 0;
|
|
563
|
+
best = Math.min(best, Math.hypot(x - (ax + vx * t), y - (ay + vy * t)));
|
|
564
|
+
}
|
|
565
|
+
return best;
|
|
566
|
+
}
|
|
452
567
|
// src/core/pick-index.ts
|
|
453
568
|
const minimumPickRadius = 2;
|
|
569
|
+
const pickSegments = 16;
|
|
570
|
+
const indexSegments = 8;
|
|
454
571
|
function createGrid(frame, count, visit) {
|
|
455
572
|
const cells = frame.cols * frame.rows;
|
|
456
573
|
const offsets = new Uint32Array(cells + 1);
|
|
@@ -505,6 +622,7 @@ var PickIndex = class {
|
|
|
505
622
|
#maxWorldRadius = 0;
|
|
506
623
|
#maxScreenRadius = 0;
|
|
507
624
|
#maxEdgeHalfWidth = 0;
|
|
625
|
+
#loops = [];
|
|
508
626
|
build(source) {
|
|
509
627
|
const { nodes, edges, positions } = source;
|
|
510
628
|
let count = 0;
|
|
@@ -530,8 +648,25 @@ var PickIndex = class {
|
|
|
530
648
|
if (count === 0) {
|
|
531
649
|
this.#nodes = null;
|
|
532
650
|
this.#edges = null;
|
|
651
|
+
this.#loops = [];
|
|
533
652
|
return;
|
|
534
653
|
}
|
|
654
|
+
this.#loops = [];
|
|
655
|
+
for (let index = 0; index < edges.capacity; index++) {
|
|
656
|
+
if (edges.alive[index] !== 1) continue;
|
|
657
|
+
if (edges.source[index] === edges.target[index]) {
|
|
658
|
+
this.#loops.push(index);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if ((edges.bend[index] ?? 0) === 0) continue;
|
|
662
|
+
const points = sampleCubic(this.#parallel(source, index), indexSegments);
|
|
663
|
+
for (let at = 0; at < points.length; at += 2) {
|
|
664
|
+
minX = Math.min(minX, points[at] ?? minX);
|
|
665
|
+
maxX = Math.max(maxX, points[at] ?? maxX);
|
|
666
|
+
minY = Math.min(minY, points[at + 1] ?? minY);
|
|
667
|
+
maxY = Math.max(maxY, points[at + 1] ?? maxY);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
535
670
|
const spanX = Math.max(maxX - minX, 1);
|
|
536
671
|
const spanY = Math.max(maxY - minY, 1);
|
|
537
672
|
let cell = Math.max(this.#maxWorldRadius * 2, Math.sqrt(spanX * spanY / count) * 2, .001);
|
|
@@ -554,9 +689,15 @@ var PickIndex = class {
|
|
|
554
689
|
this.#edges = createGrid(frame, edges.capacity, (index, emit) => {
|
|
555
690
|
if (edges.alive[index] !== 1) return;
|
|
556
691
|
this.#maxEdgeHalfWidth = Math.max(this.#maxEdgeHalfWidth, (edges.width[index] ?? 0) / 2);
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
|
|
692
|
+
const from = edges.source[index] ?? 0;
|
|
693
|
+
const to = edges.target[index] ?? 0;
|
|
694
|
+
if (from === to) return;
|
|
695
|
+
if ((edges.bend[index] ?? 0) === 0) {
|
|
696
|
+
traverse(frame, positions.x(from), positions.y(from), positions.x(to), positions.y(to), emit);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const points = sampleCubic(this.#parallel(source, index), indexSegments);
|
|
700
|
+
for (let at = 0; at + 3 < points.length; at += 2) traverse(frame, points[at] ?? 0, points[at + 1] ?? 0, points[at + 2] ?? 0, points[at + 3] ?? 0, emit);
|
|
560
701
|
});
|
|
561
702
|
}
|
|
562
703
|
pick(source, x, y, zoom, tolerance) {
|
|
@@ -582,6 +723,18 @@ var PickIndex = class {
|
|
|
582
723
|
for (let at = grid.offsets[cell] ?? 0; at < end; at++) visit(grid.items[at] ?? 0);
|
|
583
724
|
}
|
|
584
725
|
}
|
|
726
|
+
#parallel(source, index) {
|
|
727
|
+
const { edges, positions } = source;
|
|
728
|
+
const from = edges.source[index] ?? 0;
|
|
729
|
+
const to = edges.target[index] ?? 0;
|
|
730
|
+
return parallelCurve(positions.x(from), positions.y(from), positions.x(to), positions.y(to), edges.bend[index] ?? 0);
|
|
731
|
+
}
|
|
732
|
+
#loop(source, index, zoom) {
|
|
733
|
+
const node = source.edges.source[index] ?? 0;
|
|
734
|
+
const size = (source.nodes.size[node] ?? 0) / 2;
|
|
735
|
+
const reach = loopReach(Math.max(source.nodes.screenUnits[node] === 1 ? size : size * zoom, 1.25), source.edges.bend[index] ?? 0, 12) / zoom;
|
|
736
|
+
return loopCurve(source.positions.x(node), source.positions.y(node), reach);
|
|
737
|
+
}
|
|
585
738
|
#radius(source, index, zoom) {
|
|
586
739
|
const size = (source.nodes.size[index] ?? 0) / 2;
|
|
587
740
|
const radius = source.nodes.screenUnits[index] === 1 ? size / zoom : size;
|
|
@@ -612,10 +765,26 @@ var PickIndex = class {
|
|
|
612
765
|
let best = -1;
|
|
613
766
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
614
767
|
let bestOrder = Number.NEGATIVE_INFINITY;
|
|
768
|
+
const consider = (index, distance) => {
|
|
769
|
+
if (distance > ((edges.width[index] ?? 0) / 2 + tolerance) / zoom) return;
|
|
770
|
+
const order = source.order("edges", index);
|
|
771
|
+
if (distance < bestDistance || distance === bestDistance && order > bestOrder) {
|
|
772
|
+
best = index;
|
|
773
|
+
bestDistance = distance;
|
|
774
|
+
bestOrder = order;
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const seen = /* @__PURE__ */ new Set();
|
|
615
778
|
this.#cells(grid, x, y, reach, (index) => {
|
|
616
779
|
if (edges.alive[index] !== 1) return;
|
|
617
780
|
const s = edges.source[index] ?? 0;
|
|
618
781
|
const t = edges.target[index] ?? 0;
|
|
782
|
+
if ((edges.bend[index] ?? 0) !== 0) {
|
|
783
|
+
if (seen.has(index)) return;
|
|
784
|
+
seen.add(index);
|
|
785
|
+
consider(index, distanceToCubic(this.#parallel(source, index), x, y, pickSegments));
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
619
788
|
const ax = positions.x(s);
|
|
620
789
|
const ay = positions.y(s);
|
|
621
790
|
const dx = positions.x(t) - ax;
|
|
@@ -624,15 +793,12 @@ var PickIndex = class {
|
|
|
624
793
|
if (length === 0) return;
|
|
625
794
|
const along = ((x - ax) * dx + (y - ay) * dy) / length;
|
|
626
795
|
if (along < this.#radius(source, s, zoom) || along > length - this.#radius(source, t, zoom)) return;
|
|
627
|
-
|
|
628
|
-
if (distance > ((edges.width[index] ?? 0) / 2 + tolerance) / zoom) return;
|
|
629
|
-
const order = source.order("edges", index);
|
|
630
|
-
if (distance < bestDistance || distance === bestDistance && order > bestOrder) {
|
|
631
|
-
best = index;
|
|
632
|
-
bestDistance = distance;
|
|
633
|
-
bestOrder = order;
|
|
634
|
-
}
|
|
796
|
+
consider(index, Math.abs((x - ax) * dy - (y - ay) * dx) / length);
|
|
635
797
|
});
|
|
798
|
+
for (const index of this.#loops) {
|
|
799
|
+
if (edges.alive[index] !== 1) continue;
|
|
800
|
+
consider(index, distanceToCubic(this.#loop(source, index, zoom), x, y, pickSegments));
|
|
801
|
+
}
|
|
636
802
|
return best;
|
|
637
803
|
}
|
|
638
804
|
};
|
|
@@ -861,7 +1027,10 @@ const defaults = {
|
|
|
861
1027
|
nodeStrokeWidth: 1,
|
|
862
1028
|
edgeWidth: 1.5,
|
|
863
1029
|
edgeWidthRange: [1, 6],
|
|
864
|
-
edgeColor: themeColor("edge")
|
|
1030
|
+
edgeColor: themeColor("edge"),
|
|
1031
|
+
badgeFill: themeColor("badge-fill"),
|
|
1032
|
+
badgeInk: themeColor("badge-ink"),
|
|
1033
|
+
badgeRing: themeColor("node-ring")
|
|
865
1034
|
};
|
|
866
1035
|
const builtInStyles = {
|
|
867
1036
|
nodes: {
|
|
@@ -1015,6 +1184,13 @@ function resolveNumberScale(options, fallback) {
|
|
|
1015
1184
|
};
|
|
1016
1185
|
}
|
|
1017
1186
|
// src/core/columns.ts
|
|
1187
|
+
const noColor = 4294967295;
|
|
1188
|
+
const cornerCodes = {
|
|
1189
|
+
topLeft: 0,
|
|
1190
|
+
topRight: 1,
|
|
1191
|
+
bottomLeft: 2,
|
|
1192
|
+
bottomRight: 3
|
|
1193
|
+
};
|
|
1018
1194
|
const shapeCodes = {
|
|
1019
1195
|
circle: 0,
|
|
1020
1196
|
square: 1,
|
|
@@ -1054,6 +1230,8 @@ var NodeColumns = class {
|
|
|
1054
1230
|
label = [];
|
|
1055
1231
|
secondary = [];
|
|
1056
1232
|
image = [];
|
|
1233
|
+
imageScale = /* @__PURE__ */ new Float32Array(0);
|
|
1234
|
+
imageColor = /* @__PURE__ */ new Uint32Array(0);
|
|
1057
1235
|
badges = [];
|
|
1058
1236
|
reserve(index) {
|
|
1059
1237
|
if (index < this.capacity) return;
|
|
@@ -1067,6 +1245,8 @@ var NodeColumns = class {
|
|
|
1067
1245
|
this.strokeWidth = grow$1(this.strokeWidth, capacity);
|
|
1068
1246
|
this.opacity = grow$1(this.opacity, capacity);
|
|
1069
1247
|
this.screenUnits = grow$1(this.screenUnits, capacity);
|
|
1248
|
+
this.imageScale = grow$1(this.imageScale, capacity);
|
|
1249
|
+
this.imageColor = grow$1(this.imageColor, capacity);
|
|
1070
1250
|
this.capacity = capacity;
|
|
1071
1251
|
}
|
|
1072
1252
|
clear(index) {
|
|
@@ -1091,6 +1271,7 @@ var EdgeColumns = class {
|
|
|
1091
1271
|
dash = /* @__PURE__ */ new Uint8Array(0);
|
|
1092
1272
|
arrow = /* @__PURE__ */ new Uint8Array(0);
|
|
1093
1273
|
opacity = /* @__PURE__ */ new Float32Array(0);
|
|
1274
|
+
bend = /* @__PURE__ */ new Float32Array(0);
|
|
1094
1275
|
label = [];
|
|
1095
1276
|
reserve(index) {
|
|
1096
1277
|
if (index < this.capacity) return;
|
|
@@ -1104,15 +1285,190 @@ var EdgeColumns = class {
|
|
|
1104
1285
|
this.dash = grow$1(this.dash, capacity);
|
|
1105
1286
|
this.arrow = grow$1(this.arrow, capacity);
|
|
1106
1287
|
this.opacity = grow$1(this.opacity, capacity);
|
|
1288
|
+
this.bend = grow$1(this.bend, capacity);
|
|
1107
1289
|
this.capacity = capacity;
|
|
1108
1290
|
}
|
|
1109
1291
|
clear(index) {
|
|
1110
1292
|
if (index >= this.capacity) return;
|
|
1111
1293
|
this.alive[index] = 0;
|
|
1294
|
+
this.bend[index] = 0;
|
|
1112
1295
|
this.keys[index] = void 0;
|
|
1113
1296
|
this.label[index] = null;
|
|
1114
1297
|
}
|
|
1115
1298
|
};
|
|
1299
|
+
// src/core/edge-lanes.ts
|
|
1300
|
+
const empty = -1;
|
|
1301
|
+
const removed = -2;
|
|
1302
|
+
function hash(low, high) {
|
|
1303
|
+
let h = Math.imul(low, 2654435761) ^ Math.imul(high + 2135587861, 2246822519);
|
|
1304
|
+
h ^= h >>> 15;
|
|
1305
|
+
return Math.imul(h, 739982445) ^ h >>> 12;
|
|
1306
|
+
}
|
|
1307
|
+
function grown(array, length) {
|
|
1308
|
+
const next = new Int32Array(length).fill(-1);
|
|
1309
|
+
next.set(array);
|
|
1310
|
+
return next;
|
|
1311
|
+
}
|
|
1312
|
+
function createEdgeLanes() {
|
|
1313
|
+
let mask = -1;
|
|
1314
|
+
let lows = /* @__PURE__ */ new Uint32Array(0);
|
|
1315
|
+
let highs = /* @__PURE__ */ new Uint32Array(0);
|
|
1316
|
+
let heads = /* @__PURE__ */ new Int32Array(0);
|
|
1317
|
+
let tails = /* @__PURE__ */ new Int32Array(0);
|
|
1318
|
+
let counts = /* @__PURE__ */ new Int32Array(0);
|
|
1319
|
+
let stamps = /* @__PURE__ */ new Uint32Array(0);
|
|
1320
|
+
let used = 0;
|
|
1321
|
+
let live = 0;
|
|
1322
|
+
let generation = 0;
|
|
1323
|
+
let slots = /* @__PURE__ */ new Int32Array(0);
|
|
1324
|
+
let nexts = /* @__PURE__ */ new Int32Array(0);
|
|
1325
|
+
let prevs = /* @__PURE__ */ new Int32Array(0);
|
|
1326
|
+
let touched = [];
|
|
1327
|
+
function reserve(index) {
|
|
1328
|
+
if (index < slots.length) return;
|
|
1329
|
+
const length = Math.max(64, slots.length * 2, index + 1);
|
|
1330
|
+
slots = grown(slots, length);
|
|
1331
|
+
nexts = grown(nexts, length);
|
|
1332
|
+
prevs = grown(prevs, length);
|
|
1333
|
+
}
|
|
1334
|
+
function locate(low, high) {
|
|
1335
|
+
let slot = hash(low, high) & mask;
|
|
1336
|
+
let free = -1;
|
|
1337
|
+
for (;;) {
|
|
1338
|
+
const head = heads[slot] ?? empty;
|
|
1339
|
+
if (head === empty) return -(free >= 0 ? free : slot) - 1;
|
|
1340
|
+
if (head === removed) {
|
|
1341
|
+
if (free < 0) free = slot;
|
|
1342
|
+
} else if (lows[slot] === low && highs[slot] === high) return slot;
|
|
1343
|
+
slot = slot + 1 & mask;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function resize(minimum) {
|
|
1347
|
+
let capacity = 16;
|
|
1348
|
+
while (capacity < minimum) capacity *= 2;
|
|
1349
|
+
const oldLows = lows;
|
|
1350
|
+
const oldHighs = highs;
|
|
1351
|
+
const oldHeads = heads;
|
|
1352
|
+
const oldTails = tails;
|
|
1353
|
+
const oldCounts = counts;
|
|
1354
|
+
mask = capacity - 1;
|
|
1355
|
+
lows = new Uint32Array(capacity);
|
|
1356
|
+
highs = new Uint32Array(capacity);
|
|
1357
|
+
heads = new Int32Array(capacity).fill(empty);
|
|
1358
|
+
tails = new Int32Array(capacity).fill(-1);
|
|
1359
|
+
counts = new Int32Array(capacity);
|
|
1360
|
+
stamps = new Uint32Array(capacity);
|
|
1361
|
+
generation = 0;
|
|
1362
|
+
used = 0;
|
|
1363
|
+
live = 0;
|
|
1364
|
+
for (let old = 0; old < oldHeads.length; old++) {
|
|
1365
|
+
const first = oldHeads[old] ?? empty;
|
|
1366
|
+
if (first < 0) continue;
|
|
1367
|
+
const low = oldLows[old] ?? 0;
|
|
1368
|
+
const high = oldHighs[old] ?? 0;
|
|
1369
|
+
const slot = -locate(low, high) - 1;
|
|
1370
|
+
lows[slot] = low;
|
|
1371
|
+
highs[slot] = high;
|
|
1372
|
+
heads[slot] = first;
|
|
1373
|
+
tails[slot] = oldTails[old] ?? -1;
|
|
1374
|
+
counts[slot] = oldCounts[old] ?? 0;
|
|
1375
|
+
used++;
|
|
1376
|
+
live++;
|
|
1377
|
+
for (let at = first; at >= 0; at = nexts[at] ?? -1) slots[at] = slot;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function unlink(index) {
|
|
1381
|
+
const slot = slots[index] ?? -1;
|
|
1382
|
+
if (slot < 0) return;
|
|
1383
|
+
const prev = prevs[index] ?? -1;
|
|
1384
|
+
const next = nexts[index] ?? -1;
|
|
1385
|
+
if (prev >= 0) nexts[prev] = next;
|
|
1386
|
+
else heads[slot] = next;
|
|
1387
|
+
if (next >= 0) prevs[next] = prev;
|
|
1388
|
+
else tails[slot] = prev;
|
|
1389
|
+
slots[index] = -1;
|
|
1390
|
+
const count = (counts[slot] ?? 1) - 1;
|
|
1391
|
+
counts[slot] = count;
|
|
1392
|
+
if (count === 0) {
|
|
1393
|
+
heads[slot] = removed;
|
|
1394
|
+
live--;
|
|
1395
|
+
} else touched.push(heads[slot] ?? -1);
|
|
1396
|
+
}
|
|
1397
|
+
function link(index, source, target) {
|
|
1398
|
+
reserve(index);
|
|
1399
|
+
const low = Math.min(source, target);
|
|
1400
|
+
const high = Math.max(source, target);
|
|
1401
|
+
const current = slots[index] ?? -1;
|
|
1402
|
+
if (current >= 0) {
|
|
1403
|
+
if (lows[current] === low && highs[current] === high) return;
|
|
1404
|
+
unlink(index);
|
|
1405
|
+
}
|
|
1406
|
+
if ((used + 1) * 2 > mask + 1) resize(Math.max(16, (live + 1) * 4));
|
|
1407
|
+
const found = locate(low, high);
|
|
1408
|
+
prevs[index] = -1;
|
|
1409
|
+
nexts[index] = -1;
|
|
1410
|
+
if (found >= 0) {
|
|
1411
|
+
const tail = tails[found] ?? -1;
|
|
1412
|
+
nexts[tail] = index;
|
|
1413
|
+
prevs[index] = tail;
|
|
1414
|
+
tails[found] = index;
|
|
1415
|
+
counts[found] = (counts[found] ?? 0) + 1;
|
|
1416
|
+
slots[index] = found;
|
|
1417
|
+
} else {
|
|
1418
|
+
const slot = -found - 1;
|
|
1419
|
+
if (heads[slot] === empty) used++;
|
|
1420
|
+
live++;
|
|
1421
|
+
lows[slot] = low;
|
|
1422
|
+
highs[slot] = high;
|
|
1423
|
+
heads[slot] = index;
|
|
1424
|
+
tails[slot] = index;
|
|
1425
|
+
counts[slot] = 1;
|
|
1426
|
+
slots[index] = slot;
|
|
1427
|
+
}
|
|
1428
|
+
touched.push(index);
|
|
1429
|
+
}
|
|
1430
|
+
function write(edges, dirty) {
|
|
1431
|
+
if (touched.length === 0) return;
|
|
1432
|
+
generation = generation + 1 >>> 0;
|
|
1433
|
+
if (generation === 0) {
|
|
1434
|
+
stamps.fill(0);
|
|
1435
|
+
generation = 1;
|
|
1436
|
+
}
|
|
1437
|
+
const bends = edges.bend;
|
|
1438
|
+
for (const edge of touched) {
|
|
1439
|
+
const slot = edge >= 0 ? slots[edge] ?? -1 : -1;
|
|
1440
|
+
if (slot < 0 || stamps[slot] === generation) continue;
|
|
1441
|
+
stamps[slot] = generation;
|
|
1442
|
+
const count = counts[slot] ?? 0;
|
|
1443
|
+
const loop = lows[slot] === highs[slot];
|
|
1444
|
+
if (count === 1 && !loop) {
|
|
1445
|
+
const only = heads[slot] ?? 0;
|
|
1446
|
+
if (bends[only] !== 0) {
|
|
1447
|
+
bends[only] = 0;
|
|
1448
|
+
dirty.add(only);
|
|
1449
|
+
}
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
let position = 0;
|
|
1453
|
+
for (let at = heads[slot] ?? -1; at >= 0; at = nexts[at] ?? -1) {
|
|
1454
|
+
const source = edges.source[at] ?? 0;
|
|
1455
|
+
const target = edges.target[at] ?? 0;
|
|
1456
|
+
const bend = loop ? position : Math.fround(laneBend(position, count) * (source < target ? 1 : -1)) || 0;
|
|
1457
|
+
if (bends[at] !== bend) {
|
|
1458
|
+
bends[at] = bend;
|
|
1459
|
+
dirty.add(at);
|
|
1460
|
+
}
|
|
1461
|
+
position++;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
touched = [];
|
|
1465
|
+
}
|
|
1466
|
+
return {
|
|
1467
|
+
link,
|
|
1468
|
+
unlink,
|
|
1469
|
+
write
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1116
1472
|
// src/core/palette.ts
|
|
1117
1473
|
var Palette = class {
|
|
1118
1474
|
#ids = /* @__PURE__ */ new Map();
|
|
@@ -1159,18 +1515,37 @@ function finiteOr(value, fallback) {
|
|
|
1159
1515
|
function codeOf(codes, value) {
|
|
1160
1516
|
return typeof value === "string" && Object.hasOwn(codes, value) ? codes[value] : 0;
|
|
1161
1517
|
}
|
|
1162
|
-
function readBadges(mark, datum) {
|
|
1518
|
+
function readBadges(mark, datum, palette) {
|
|
1163
1519
|
if (mark.badges.length === 0) return null;
|
|
1164
|
-
const badges =
|
|
1165
|
-
let count = 0;
|
|
1520
|
+
const badges = [];
|
|
1166
1521
|
for (const [corner, read] of mark.badges) {
|
|
1167
1522
|
const badge = read(datum);
|
|
1168
|
-
if (typeof badge
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1523
|
+
if (typeof badge !== "object" || badge === null) continue;
|
|
1524
|
+
const { text, fill, color } = badge;
|
|
1525
|
+
badges.push({
|
|
1526
|
+
corner: cornerCodes[corner],
|
|
1527
|
+
text: typeof text === "number" ? String(text) : toText(text) || null,
|
|
1528
|
+
fill: palette.id(toColor(fill) ?? defaults.badgeFill),
|
|
1529
|
+
color: palette.id(toColor(color) ?? defaults.badgeInk),
|
|
1530
|
+
ring: palette.id(defaults.badgeRing)
|
|
1531
|
+
});
|
|
1172
1532
|
}
|
|
1173
|
-
return
|
|
1533
|
+
return badges.length === 0 ? null : badges;
|
|
1534
|
+
}
|
|
1535
|
+
function readImage(value) {
|
|
1536
|
+
if (typeof value === "string") return value === "" ? null : {
|
|
1537
|
+
src: value,
|
|
1538
|
+
scale: 1,
|
|
1539
|
+
color: null
|
|
1540
|
+
};
|
|
1541
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1542
|
+
const { src, scale, color } = value;
|
|
1543
|
+
if (typeof src !== "string" || src === "") return null;
|
|
1544
|
+
return {
|
|
1545
|
+
src,
|
|
1546
|
+
scale: Math.max(0, finiteOr(scale, 1)),
|
|
1547
|
+
color: toColor(color)
|
|
1548
|
+
};
|
|
1174
1549
|
}
|
|
1175
1550
|
function layersOf(marks) {
|
|
1176
1551
|
const slots = {
|
|
@@ -1186,6 +1561,7 @@ function createScene() {
|
|
|
1186
1561
|
const nodes = new NodeColumns();
|
|
1187
1562
|
const edges = new EdgeColumns();
|
|
1188
1563
|
const palette = new Palette();
|
|
1564
|
+
const lanes = createEdgeLanes();
|
|
1189
1565
|
const slots = /* @__PURE__ */ new Map();
|
|
1190
1566
|
let layers = [];
|
|
1191
1567
|
let layersDirty = false;
|
|
@@ -1259,8 +1635,11 @@ function createScene() {
|
|
|
1259
1635
|
nodes.screenUnits[index] = mark.screenUnits ? 1 : 0;
|
|
1260
1636
|
nodes.label[index] = toText(mark.label?.(datum));
|
|
1261
1637
|
nodes.secondary[index] = toText(mark.secondary?.(datum));
|
|
1262
|
-
|
|
1263
|
-
nodes.
|
|
1638
|
+
const image = readImage(mark.image?.(datum));
|
|
1639
|
+
nodes.image[index] = image?.src ?? null;
|
|
1640
|
+
nodes.imageScale[index] = image?.scale ?? 1;
|
|
1641
|
+
nodes.imageColor[index] = image?.color ? palette.id(image.color) : noColor;
|
|
1642
|
+
nodes.badges[index] = readBadges(mark, datum, palette);
|
|
1264
1643
|
dirtyNodes.add(index);
|
|
1265
1644
|
}
|
|
1266
1645
|
function writeEdge(model, record) {
|
|
@@ -1273,6 +1652,7 @@ function createScene() {
|
|
|
1273
1652
|
edges.slot[index] = slots.get(record.mark) ?? 0;
|
|
1274
1653
|
edges.source[index] = model.node(record.source)?.index ?? 0;
|
|
1275
1654
|
edges.target[index] = model.node(record.target)?.index ?? 0;
|
|
1655
|
+
lanes.link(index, edges.source[index] ?? 0, edges.target[index] ?? 0);
|
|
1276
1656
|
edges.color[index] = palette.id(color);
|
|
1277
1657
|
edges.width[index] = Number.isNaN(width) ? defaults.edgeWidth : widthScale.map(width, widthDomain);
|
|
1278
1658
|
edges.dash[index] = codeOf(dashCodes, mark.dash?.(datum));
|
|
@@ -1290,6 +1670,7 @@ function createScene() {
|
|
|
1290
1670
|
dirtyNodes.add(index);
|
|
1291
1671
|
}
|
|
1292
1672
|
function removeEdge(index) {
|
|
1673
|
+
lanes.unlink(index);
|
|
1293
1674
|
edges.clear(index);
|
|
1294
1675
|
widthValues[index] = NaN;
|
|
1295
1676
|
edgeColorValues[index] = void 0;
|
|
@@ -1381,6 +1762,7 @@ function createScene() {
|
|
|
1381
1762
|
}
|
|
1382
1763
|
for (const record of nodeRecords.values()) writeNode(record);
|
|
1383
1764
|
for (const record of edgeRecords.values()) writeEdge(model, record);
|
|
1765
|
+
lanes.write(edges, dirtyEdges);
|
|
1384
1766
|
},
|
|
1385
1767
|
takeDirty() {
|
|
1386
1768
|
const dirty = {
|
|
@@ -1397,6 +1779,47 @@ function createScene() {
|
|
|
1397
1779
|
}
|
|
1398
1780
|
};
|
|
1399
1781
|
}
|
|
1782
|
+
// src/core/spatial.ts
|
|
1783
|
+
const sideWeight = 2;
|
|
1784
|
+
const cone = 2;
|
|
1785
|
+
function nearestNode(point, nodes, positions) {
|
|
1786
|
+
let best = -1;
|
|
1787
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1788
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
1789
|
+
if (nodes.alive[index] !== 1) continue;
|
|
1790
|
+
const distance = Math.hypot(positions.x(index) - point.x, positions.y(index) - point.y);
|
|
1791
|
+
if (distance < bestDistance) {
|
|
1792
|
+
best = index;
|
|
1793
|
+
bestDistance = distance;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return best;
|
|
1797
|
+
}
|
|
1798
|
+
function nodeToward(from, direction, nodes, positions) {
|
|
1799
|
+
const x = positions.x(from);
|
|
1800
|
+
const y = positions.y(from);
|
|
1801
|
+
const length = Math.hypot(direction.x, direction.y);
|
|
1802
|
+
if (length === 0) return -1;
|
|
1803
|
+
const dx = direction.x / length;
|
|
1804
|
+
const dy = direction.y / length;
|
|
1805
|
+
let best = -1;
|
|
1806
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1807
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
1808
|
+
if (index === from || nodes.alive[index] !== 1) continue;
|
|
1809
|
+
const vx = positions.x(index) - x;
|
|
1810
|
+
const vy = positions.y(index) - y;
|
|
1811
|
+
const along = vx * dx + vy * dy;
|
|
1812
|
+
if (along <= 0) continue;
|
|
1813
|
+
const side = Math.abs(vx * dy - vy * dx);
|
|
1814
|
+
if (side > along * cone) continue;
|
|
1815
|
+
const score = along + side * sideWeight;
|
|
1816
|
+
if (score < bestScore) {
|
|
1817
|
+
best = index;
|
|
1818
|
+
bestScore = score;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return best;
|
|
1822
|
+
}
|
|
1400
1823
|
const noPaint = 4294967295;
|
|
1401
1824
|
const tableOffset = {
|
|
1402
1825
|
nodes: 0,
|
|
@@ -1878,12 +2301,162 @@ var GlyphAtlas = class {
|
|
|
1878
2301
|
this.generation++;
|
|
1879
2302
|
}
|
|
1880
2303
|
};
|
|
2304
|
+
const levels = Math.log2(256) + 1;
|
|
2305
|
+
const maxLayers = 256;
|
|
2306
|
+
var ImageAtlas = class {
|
|
2307
|
+
#gl;
|
|
2308
|
+
#invalidate;
|
|
2309
|
+
#entries = /* @__PURE__ */ new Map();
|
|
2310
|
+
#free = [];
|
|
2311
|
+
#placeholder;
|
|
2312
|
+
#texture = null;
|
|
2313
|
+
#capacity = 0;
|
|
2314
|
+
#next = 0;
|
|
2315
|
+
#loaded = /* @__PURE__ */ new Set();
|
|
2316
|
+
#mipsStale = false;
|
|
2317
|
+
#canvas = null;
|
|
2318
|
+
#warnedFull = false;
|
|
2319
|
+
#destroyed = false;
|
|
2320
|
+
constructor(gl, invalidate) {
|
|
2321
|
+
this.#gl = gl;
|
|
2322
|
+
this.#invalidate = invalidate;
|
|
2323
|
+
this.#placeholder = gl.createTexture();
|
|
2324
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#placeholder);
|
|
2325
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, 1, 1, 1);
|
|
2326
|
+
}
|
|
2327
|
+
get texture() {
|
|
2328
|
+
return this.#texture ?? this.#placeholder;
|
|
2329
|
+
}
|
|
2330
|
+
layer(src) {
|
|
2331
|
+
return this.#entries.get(src)?.layer ?? -1;
|
|
2332
|
+
}
|
|
2333
|
+
acquire(src) {
|
|
2334
|
+
const entry = this.#entries.get(src);
|
|
2335
|
+
if (entry) {
|
|
2336
|
+
entry.count++;
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
this.#entries.set(src, {
|
|
2340
|
+
count: 1,
|
|
2341
|
+
layer: -1
|
|
2342
|
+
});
|
|
2343
|
+
const image = new Image();
|
|
2344
|
+
image.crossOrigin = "anonymous";
|
|
2345
|
+
image.decoding = "async";
|
|
2346
|
+
image.src = src;
|
|
2347
|
+
image.decode().then(() => this.#store(src, image), () => this.#fail(src));
|
|
2348
|
+
}
|
|
2349
|
+
release(src) {
|
|
2350
|
+
const entry = this.#entries.get(src);
|
|
2351
|
+
if (!entry || --entry.count > 0) return;
|
|
2352
|
+
this.#entries.delete(src);
|
|
2353
|
+
if (entry.layer >= 0) this.#free.push(entry.layer);
|
|
2354
|
+
}
|
|
2355
|
+
takeLoaded() {
|
|
2356
|
+
if (this.#loaded.size === 0) return null;
|
|
2357
|
+
const loaded = this.#loaded;
|
|
2358
|
+
this.#loaded = /* @__PURE__ */ new Set();
|
|
2359
|
+
return loaded;
|
|
2360
|
+
}
|
|
2361
|
+
prepare() {
|
|
2362
|
+
if (!this.#mipsStale || !this.#texture) return;
|
|
2363
|
+
this.#mipsStale = false;
|
|
2364
|
+
const gl = this.#gl;
|
|
2365
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#texture);
|
|
2366
|
+
gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
|
|
2367
|
+
}
|
|
2368
|
+
destroy() {
|
|
2369
|
+
this.#destroyed = true;
|
|
2370
|
+
this.#gl.deleteTexture(this.#placeholder);
|
|
2371
|
+
if (this.#texture) this.#gl.deleteTexture(this.#texture);
|
|
2372
|
+
}
|
|
2373
|
+
#fail(src) {
|
|
2374
|
+
if (this.#destroyed) return;
|
|
2375
|
+
if (dev && this.#entries.has(src)) console.warn(`graph: cannot load the image ${src}`);
|
|
2376
|
+
}
|
|
2377
|
+
#store(src, image) {
|
|
2378
|
+
const entry = this.#entries.get(src);
|
|
2379
|
+
if (this.#destroyed || !entry || entry.layer >= 0) return;
|
|
2380
|
+
const layer = this.#free.pop() ?? this.#next;
|
|
2381
|
+
if (layer >= this.#capacity && !this.#grow()) {
|
|
2382
|
+
if (dev && !this.#warnedFull) {
|
|
2383
|
+
this.#warnedFull = true;
|
|
2384
|
+
console.warn(`graph: more than ${maxLayers} distinct images, the others are not drawn`);
|
|
2385
|
+
}
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
if (layer === this.#next) this.#next++;
|
|
2389
|
+
const canvas = this.#draw(image);
|
|
2390
|
+
const gl = this.#gl;
|
|
2391
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#texture);
|
|
2392
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
2393
|
+
try {
|
|
2394
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, 256, 256, 1, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
|
2395
|
+
} catch {
|
|
2396
|
+
this.#free.push(layer);
|
|
2397
|
+
this.#fail(src);
|
|
2398
|
+
return;
|
|
2399
|
+
} finally {
|
|
2400
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
2401
|
+
}
|
|
2402
|
+
entry.layer = layer;
|
|
2403
|
+
this.#mipsStale = true;
|
|
2404
|
+
this.#loaded.add(src);
|
|
2405
|
+
this.#invalidate();
|
|
2406
|
+
}
|
|
2407
|
+
#draw(image) {
|
|
2408
|
+
this.#canvas ??= Object.assign(document.createElement("canvas"), {
|
|
2409
|
+
width: 256,
|
|
2410
|
+
height: 256
|
|
2411
|
+
});
|
|
2412
|
+
const context = this.#canvas.getContext("2d");
|
|
2413
|
+
if (context) {
|
|
2414
|
+
context.clearRect(0, 0, 256, 256);
|
|
2415
|
+
const width = image.naturalWidth;
|
|
2416
|
+
const height = image.naturalHeight;
|
|
2417
|
+
const scale = width > 0 && height > 0 ? 256 / Math.min(width, height) : 0;
|
|
2418
|
+
const drawnWidth = scale > 0 ? width * scale : 256;
|
|
2419
|
+
const drawnHeight = scale > 0 ? height * scale : 256;
|
|
2420
|
+
context.drawImage(image, (256 - drawnWidth) / 2, (256 - drawnHeight) / 2, drawnWidth, drawnHeight);
|
|
2421
|
+
}
|
|
2422
|
+
return this.#canvas;
|
|
2423
|
+
}
|
|
2424
|
+
#grow() {
|
|
2425
|
+
if (this.#capacity >= maxLayers) return false;
|
|
2426
|
+
const gl = this.#gl;
|
|
2427
|
+
const capacity = Math.min(maxLayers, Math.max(4, this.#capacity * 2));
|
|
2428
|
+
const texture = gl.createTexture();
|
|
2429
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
|
|
2430
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, levels, gl.RGBA8, 256, 256, capacity);
|
|
2431
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
2432
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
2433
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2434
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2435
|
+
const previous = this.#texture;
|
|
2436
|
+
if (previous) {
|
|
2437
|
+
const framebuffer = gl.createFramebuffer();
|
|
2438
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer);
|
|
2439
|
+
for (let layer = 0; layer < this.#capacity; layer++) {
|
|
2440
|
+
gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, previous, 0, layer);
|
|
2441
|
+
gl.copyTexSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, 0, 0, 256, 256);
|
|
2442
|
+
}
|
|
2443
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
2444
|
+
gl.deleteFramebuffer(framebuffer);
|
|
2445
|
+
gl.deleteTexture(previous);
|
|
2446
|
+
}
|
|
2447
|
+
this.#texture = texture;
|
|
2448
|
+
this.#capacity = capacity;
|
|
2449
|
+
this.#mipsStale = true;
|
|
2450
|
+
return true;
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
1881
2453
|
const primarySize = 12;
|
|
1882
2454
|
const lineHeight = 1.25;
|
|
1883
2455
|
const labelMin = 8;
|
|
1884
2456
|
const cell = 8;
|
|
1885
2457
|
const maxLabels = 4e3;
|
|
1886
2458
|
const cullMargin = 400;
|
|
2459
|
+
const forcedStates = 6;
|
|
1887
2460
|
var GlyphBuffer = class {
|
|
1888
2461
|
data = /* @__PURE__ */ new ArrayBuffer(8192);
|
|
1889
2462
|
count = 0;
|
|
@@ -1946,7 +2519,9 @@ var LabelLayer = class {
|
|
|
1946
2519
|
const highlighted = new Set(frame.highlighted);
|
|
1947
2520
|
const worldPrimary = primarySize * Math.min(1, view.zoom);
|
|
1948
2521
|
let placed = 0;
|
|
1949
|
-
const
|
|
2522
|
+
const forced = (index) => ((frame.nodeMask[index] ?? 0) & forcedStates) !== 0;
|
|
2523
|
+
const place = (index, highlight) => {
|
|
2524
|
+
const force = highlight && forced(index);
|
|
1950
2525
|
const label = nodes.label[index];
|
|
1951
2526
|
if (!label || nodes.alive[index] !== 1) return;
|
|
1952
2527
|
if (!labelVisible(frame.nodeMask[index] ?? 0, frame.labelFlags)) return;
|
|
@@ -1977,12 +2552,13 @@ var LabelLayer = class {
|
|
|
1977
2552
|
for (let row = r0; row <= r1; row++) for (let col = c0; col <= c1; col++) if (occupancy[row * cols + col] === 1) return;
|
|
1978
2553
|
}
|
|
1979
2554
|
for (let row = r0; row <= r1; row++) occupancy.fill(1, row * cols + c0, row * cols + c1 + 1);
|
|
1980
|
-
const target =
|
|
2555
|
+
const target = highlight ? this.highlight : this.main;
|
|
1981
2556
|
target.push(index, 0, line, atlas.height);
|
|
1982
2557
|
if (secondary) target.push(index, 1, secondary, atlas.height);
|
|
1983
2558
|
placed++;
|
|
1984
2559
|
};
|
|
1985
|
-
for (const index of highlighted) place(index, true);
|
|
2560
|
+
for (const index of highlighted) if (forced(index)) place(index, true);
|
|
2561
|
+
for (const index of highlighted) if (!forced(index)) place(index, true);
|
|
1986
2562
|
if (worldPrimary < labelMin && !frame.screenLabels) return;
|
|
1987
2563
|
const xy = positions.xy;
|
|
1988
2564
|
const order = frame.order;
|
|
@@ -2015,12 +2591,14 @@ function sortBySize(nodes, order) {
|
|
|
2015
2591
|
screenLabels
|
|
2016
2592
|
};
|
|
2017
2593
|
}
|
|
2018
|
-
|
|
2594
|
+
const badgeFont = .66;
|
|
2595
|
+
const badgeRing = 1.5;
|
|
2019
2596
|
const header = `#version 300 es
|
|
2020
2597
|
precision highp float;
|
|
2021
2598
|
precision highp int;
|
|
2022
2599
|
precision highp sampler2D;
|
|
2023
2600
|
precision highp usampler2D;
|
|
2601
|
+
precision highp sampler2DArray;
|
|
2024
2602
|
layout(std140) uniform Frame {
|
|
2025
2603
|
vec2 center;
|
|
2026
2604
|
vec2 viewport;
|
|
@@ -2040,7 +2618,7 @@ layout(std140) uniform States {
|
|
|
2040
2618
|
uniform int layerSlot;
|
|
2041
2619
|
uniform int overlay;
|
|
2042
2620
|
uniform sampler2D palette;
|
|
2043
|
-
const uint highlightMask =
|
|
2621
|
+
const uint highlightMask = 7u;
|
|
2044
2622
|
const uint none = 0xffffffffu;
|
|
2045
2623
|
const vec4 hidden = vec4(2.0, 2.0, 2.0, 1.0);
|
|
2046
2624
|
vec2 toScreen(vec2 world) { return (world - center) * zoom + viewport * 0.5; }
|
|
@@ -2085,6 +2663,13 @@ float nodeDiameter(vec4 style) {
|
|
|
2085
2663
|
return (flags & 4u) != 0u ? style.x : style.x * zoom;
|
|
2086
2664
|
}
|
|
2087
2665
|
float nodeRadius(vec4 style) { return max(nodeDiameter(style) * 0.5, 1.25); }
|
|
2666
|
+
float badgeHeight(vec4 style) { return min(nodeDiameter(style) * 0.55, ${18 .toFixed(1)}); }
|
|
2667
|
+
vec2 badgeCenter(vec2 xy, vec4 style, uint corner) {
|
|
2668
|
+
uint shape = uint(style.w + 0.5) & 3u;
|
|
2669
|
+
float reach = shape == 0u ? 0.70710678 : shape == 1u ? 0.86 : 0.6;
|
|
2670
|
+
vec2 side = vec2((corner & 1u) != 0u ? 1.0 : -1.0, (corner & 2u) != 0u ? 1.0 : -1.0);
|
|
2671
|
+
return toScreen(xy) + side * reach * nodeRadius(style);
|
|
2672
|
+
}
|
|
2088
2673
|
`;
|
|
2089
2674
|
const edgeData = `
|
|
2090
2675
|
uniform usampler2D edgeTopology;
|
|
@@ -2092,9 +2677,13 @@ uniform sampler2D edgeStyle;
|
|
|
2092
2677
|
uniform usampler2D edgeState;
|
|
2093
2678
|
`;
|
|
2094
2679
|
const nodeVertex = `${header}${nodeData}
|
|
2680
|
+
uniform usampler2D nodeImage;
|
|
2095
2681
|
in vec2 corner;
|
|
2096
2682
|
in uint instance;
|
|
2097
2683
|
out vec2 vLocal;
|
|
2684
|
+
flat out float vImageLayer;
|
|
2685
|
+
flat out float vImageScale;
|
|
2686
|
+
flat out vec4 vImageColor;
|
|
2098
2687
|
out float vRadius;
|
|
2099
2688
|
out float vStrokeWidth;
|
|
2100
2689
|
out float vHaloWidth;
|
|
@@ -2125,11 +2714,19 @@ void main() {
|
|
|
2125
2714
|
vFill = paletteColor(look.fill);
|
|
2126
2715
|
vStroke = paletteColor(look.stroke);
|
|
2127
2716
|
vOpacity = look.opacity;
|
|
2717
|
+
uvec4 image = texelFetch(nodeImage, t, 0);
|
|
2718
|
+
vImageLayer = float(image.x) - 1.0;
|
|
2719
|
+
vImageScale = max(uintBitsToFloat(image.z), 0.001);
|
|
2720
|
+
vImageColor = image.y == none ? vec4(-1.0) : paletteColor(image.y);
|
|
2128
2721
|
gl_Position = toClip(toScreen(xy) + vLocal);
|
|
2129
2722
|
}
|
|
2130
2723
|
`;
|
|
2131
2724
|
const nodeFragment = `${header}
|
|
2725
|
+
uniform sampler2DArray images;
|
|
2132
2726
|
in vec2 vLocal;
|
|
2727
|
+
flat in float vImageLayer;
|
|
2728
|
+
flat in float vImageScale;
|
|
2729
|
+
flat in vec4 vImageColor;
|
|
2133
2730
|
in float vRadius;
|
|
2134
2731
|
in float vStrokeWidth;
|
|
2135
2732
|
in float vHaloWidth;
|
|
@@ -2151,9 +2748,17 @@ void main() {
|
|
|
2151
2748
|
float d = shapeDistance(vLocal, vRadius);
|
|
2152
2749
|
float inside = 1.0 - smoothstep(-aa, aa, d);
|
|
2153
2750
|
float ring = vStrokeWidth > 0.0 ? smoothstep(-vStrokeWidth - aa, -vStrokeWidth + aa, d) : 0.0;
|
|
2154
|
-
vec4
|
|
2155
|
-
|
|
2156
|
-
|
|
2751
|
+
vec4 fill = vec4(vFill.rgb * vFill.a, vFill.a);
|
|
2752
|
+
if (vImageLayer >= 0.0) {
|
|
2753
|
+
vec2 uv = vLocal / (vRadius * 2.0 * vImageScale) + 0.5;
|
|
2754
|
+
vec4 picture = texture(images, vec3(uv, vImageLayer));
|
|
2755
|
+
vec2 within = step(vec2(0.0), uv) * step(uv, vec2(1.0));
|
|
2756
|
+
picture *= within.x * within.y;
|
|
2757
|
+
if (vImageColor.a >= 0.0) picture = vec4(vImageColor.rgb * vImageColor.a, vImageColor.a) * picture.a;
|
|
2758
|
+
fill = picture + fill * (1.0 - picture.a);
|
|
2759
|
+
}
|
|
2760
|
+
vec4 color = mix(fill, vec4(vStroke.rgb * vStroke.a, vStroke.a), ring);
|
|
2761
|
+
vec4 result = color * inside * vOpacity;
|
|
2157
2762
|
if (vHaloWidth > 0.0) {
|
|
2158
2763
|
float h = haloColor.a * (1.0 - smoothstep(0.0, vHaloWidth, d)) * vOpacity;
|
|
2159
2764
|
result += vec4(haloColor.rgb * h, h) * (1.0 - result.a);
|
|
@@ -2187,6 +2792,10 @@ void main() {
|
|
|
2187
2792
|
gl_Position = hidden;
|
|
2188
2793
|
return;
|
|
2189
2794
|
}
|
|
2795
|
+
if (topology.x == topology.y || style.z != 0.0) {
|
|
2796
|
+
gl_Position = hidden;
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2190
2799
|
Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z), 32);
|
|
2191
2800
|
ivec2 s = texel(topology.x);
|
|
2192
2801
|
ivec2 e = texel(topology.y);
|
|
@@ -2231,6 +2840,143 @@ void main() {
|
|
|
2231
2840
|
gl_Position = visible > 0.5 ? toClip(ps + dir * along + normal * across) : hidden;
|
|
2232
2841
|
}
|
|
2233
2842
|
`;
|
|
2843
|
+
const segments = 24 .toFixed(1);
|
|
2844
|
+
const curveVertex = `${header}${nodeData}${edgeData}
|
|
2845
|
+
in vec3 corner;
|
|
2846
|
+
in uint instance;
|
|
2847
|
+
out vec2 vPos;
|
|
2848
|
+
out float vStart;
|
|
2849
|
+
out float vEnd;
|
|
2850
|
+
out float vHalfWidth;
|
|
2851
|
+
out float vArrowLength;
|
|
2852
|
+
out float vArrowHalf;
|
|
2853
|
+
out float vHaloWidth;
|
|
2854
|
+
flat out uint vDash;
|
|
2855
|
+
flat out uint vArrow;
|
|
2856
|
+
flat out vec4 vColor;
|
|
2857
|
+
flat out float vOpacity;
|
|
2858
|
+
vec2 cubicAt(vec2 a, vec2 b, vec2 c, vec2 d, float t) {
|
|
2859
|
+
float u = 1.0 - t;
|
|
2860
|
+
return u * u * u * a + 3.0 * u * u * t * b + 3.0 * u * t * t * c + t * t * t * d;
|
|
2861
|
+
}
|
|
2862
|
+
vec2 cubicTangent(vec2 a, vec2 b, vec2 c, vec2 d, float t) {
|
|
2863
|
+
float u = 1.0 - t;
|
|
2864
|
+
return 3.0 * u * u * (b - a) + 6.0 * u * t * (c - b) + 3.0 * t * t * (d - c);
|
|
2865
|
+
}
|
|
2866
|
+
float crossing(vec2 a, vec2 b, vec2 c, vec2 d, vec2 point, float radius, float lo, float hi, bool rising) {
|
|
2867
|
+
for (int i = 0; i < 14; i++) {
|
|
2868
|
+
float mid = (lo + hi) * 0.5;
|
|
2869
|
+
bool inside = distance(cubicAt(a, b, c, d, mid), point) < radius;
|
|
2870
|
+
if (inside == rising) lo = mid; else hi = mid;
|
|
2871
|
+
}
|
|
2872
|
+
return (lo + hi) * 0.5;
|
|
2873
|
+
}
|
|
2874
|
+
float arcLength(vec2 a, vec2 b, vec2 c, vec2 d, float from, float to) {
|
|
2875
|
+
float total = 0.0;
|
|
2876
|
+
vec2 previous = cubicAt(a, b, c, d, from);
|
|
2877
|
+
for (int i = 1; i <= 8; i++) {
|
|
2878
|
+
vec2 next = cubicAt(a, b, c, d, mix(from, to, float(i) / 8.0));
|
|
2879
|
+
total += distance(previous, next);
|
|
2880
|
+
previous = next;
|
|
2881
|
+
}
|
|
2882
|
+
return total;
|
|
2883
|
+
}
|
|
2884
|
+
vec2 turn(vec2 v, float angle) {
|
|
2885
|
+
float c = cos(angle);
|
|
2886
|
+
float s = sin(angle);
|
|
2887
|
+
return vec2(v.x * c - v.y * s, v.x * s + v.y * c);
|
|
2888
|
+
}
|
|
2889
|
+
void main() {
|
|
2890
|
+
ivec2 t = texel(instance);
|
|
2891
|
+
uvec4 topology = texelFetch(edgeTopology, t, 0);
|
|
2892
|
+
vec4 style = texelFetch(edgeStyle, t, 0);
|
|
2893
|
+
uint packed = topology.w;
|
|
2894
|
+
uint mask = texelFetch(edgeState, t, 0).r;
|
|
2895
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
2896
|
+
bool loop = topology.x == topology.y;
|
|
2897
|
+
if ((packed & 16u) == 0u || (!loop && style.z == 0.0) || (overlay == 0 && (int(packed >> 5u) != layerSlot || highlighted))) {
|
|
2898
|
+
gl_Position = hidden;
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z), 32);
|
|
2902
|
+
ivec2 s = texel(topology.x);
|
|
2903
|
+
ivec2 e = texel(topology.y);
|
|
2904
|
+
vec2 a = toScreen(texelFetch(nodePosition, s, 0).xy);
|
|
2905
|
+
vec2 d = toScreen(texelFetch(nodePosition, e, 0).xy);
|
|
2906
|
+
float sourceRadius = nodeRadius(texelFetch(nodeStyle, s, 0));
|
|
2907
|
+
float targetRadius = nodeRadius(texelFetch(nodeStyle, e, 0));
|
|
2908
|
+
vec2 b;
|
|
2909
|
+
vec2 c;
|
|
2910
|
+
if (loop) {
|
|
2911
|
+
float reach = sourceRadius + max(sourceRadius * 1.5, ${12 .toFixed(1)}) * (1.0 + ${loopGrowth.toFixed(2)} * style.z);
|
|
2912
|
+
float arm = reach / (0.75 * cos(${loopTurn.toFixed(4)}));
|
|
2913
|
+
vec2 u = vec2(0.70710678, -0.70710678);
|
|
2914
|
+
b = a + arm * turn(u, ${loopTurn.toFixed(4)});
|
|
2915
|
+
c = a + arm * turn(u, -${loopTurn.toFixed(4)});
|
|
2916
|
+
} else {
|
|
2917
|
+
vec2 delta = d - a;
|
|
2918
|
+
float len = length(delta);
|
|
2919
|
+
vec2 side = len > 0.0 ? vec2(-delta.y, delta.x) / len : vec2(0.0, 1.0);
|
|
2920
|
+
vec2 control = (a + d) * 0.5 + side * (2.0 * style.z * len);
|
|
2921
|
+
b = a + (control - a) * (2.0 / 3.0);
|
|
2922
|
+
c = d + (control - d) * (2.0 / 3.0);
|
|
2923
|
+
}
|
|
2924
|
+
float t0 = crossing(a, b, c, d, a, sourceRadius, 0.0, 0.5, true);
|
|
2925
|
+
float t1 = crossing(a, b, c, d, d, targetRadius, 0.5, 1.0, false);
|
|
2926
|
+
float visible = arcLength(a, b, c, d, t0, t1);
|
|
2927
|
+
float width = max(look.strokeWidth, 0.5);
|
|
2928
|
+
uint arrow = (packed >> 2u) & 3u;
|
|
2929
|
+
float arrowLength = arrow != 0u && visible > width * 8.0 ? max(width * 4.0, 6.0) : 0.0;
|
|
2930
|
+
float arrowHalf = max(width * 1.6, 3.0);
|
|
2931
|
+
float halo = look.halo > 0.5 ? haloWidth : 0.0;
|
|
2932
|
+
float margin = 0.5 / pixelRatio + 0.5;
|
|
2933
|
+
float startHead = (arrow & 1u) != 0u ? arrowLength : 0.0;
|
|
2934
|
+
float endHead = (arrow & 2u) != 0u ? arrowLength : 0.0;
|
|
2935
|
+
vec2 first = cubicAt(a, b, c, d, t0);
|
|
2936
|
+
vec2 last = cubicAt(a, b, c, d, t1);
|
|
2937
|
+
float ta = startHead > 0.0 ? crossing(a, b, c, d, first, startHead, t0, t1, true) : t0;
|
|
2938
|
+
float tb = endHead > 0.0 ? crossing(a, b, c, d, last, endHead, t0, t1, false) : t1;
|
|
2939
|
+
float body = width * 0.5 + halo + margin;
|
|
2940
|
+
float head = max(body, arrowHalf + halo + margin);
|
|
2941
|
+
int station = int(corner.x + 0.5);
|
|
2942
|
+
float at;
|
|
2943
|
+
float along;
|
|
2944
|
+
float push = 0.0;
|
|
2945
|
+
if (station == 0) {
|
|
2946
|
+
at = t0;
|
|
2947
|
+
along = -margin;
|
|
2948
|
+
push = -margin;
|
|
2949
|
+
} else if (station > int(${segments}) + 1) {
|
|
2950
|
+
at = t1;
|
|
2951
|
+
along = visible + margin;
|
|
2952
|
+
push = margin;
|
|
2953
|
+
} else {
|
|
2954
|
+
float f = float(station - 1) / ${segments};
|
|
2955
|
+
at = mix(ta, tb, f);
|
|
2956
|
+
along = mix(startHead, visible - endHead, f);
|
|
2957
|
+
}
|
|
2958
|
+
vec2 tangent = cubicTangent(a, b, c, d, at);
|
|
2959
|
+
float tangentLength = length(tangent);
|
|
2960
|
+
vec2 dir = tangentLength > 0.0 ? tangent / tangentLength : vec2(1.0, 0.0);
|
|
2961
|
+
vec2 normal = vec2(-dir.y, dir.x);
|
|
2962
|
+
float extent = corner.z < 0.5 ? body
|
|
2963
|
+
: corner.z < 1.5 ? (startHead > 0.0 ? head : body)
|
|
2964
|
+
: (endHead > 0.0 ? head : body);
|
|
2965
|
+
float across = corner.y * extent;
|
|
2966
|
+
vPos = vec2(along, across);
|
|
2967
|
+
vStart = 0.0;
|
|
2968
|
+
vEnd = visible;
|
|
2969
|
+
vHalfWidth = width * 0.5;
|
|
2970
|
+
vArrowLength = arrowLength;
|
|
2971
|
+
vArrowHalf = arrowHalf;
|
|
2972
|
+
vHaloWidth = halo;
|
|
2973
|
+
vDash = packed & 3u;
|
|
2974
|
+
vArrow = arrow;
|
|
2975
|
+
vColor = paletteColor(look.stroke);
|
|
2976
|
+
vOpacity = look.opacity;
|
|
2977
|
+
gl_Position = visible > 0.5 ? toClip(cubicAt(a, b, c, d, at) + dir * push + normal * across) : hidden;
|
|
2978
|
+
}
|
|
2979
|
+
`;
|
|
2234
2980
|
const edgeFragment = `${header}
|
|
2235
2981
|
in vec2 vPos;
|
|
2236
2982
|
in float vStart;
|
|
@@ -2295,7 +3041,8 @@ in vec4 glyphRect;
|
|
|
2295
3041
|
in vec4 glyphUv;
|
|
2296
3042
|
out vec2 vUv;
|
|
2297
3043
|
out float vScale;
|
|
2298
|
-
flat out
|
|
3044
|
+
flat out vec4 vInk;
|
|
3045
|
+
flat out float vHalo;
|
|
2299
3046
|
flat out float vOpacity;
|
|
2300
3047
|
void main() {
|
|
2301
3048
|
ivec2 t = texel(glyphNode);
|
|
@@ -2304,19 +3051,36 @@ void main() {
|
|
|
2304
3051
|
uint flags = uint(style.w + 0.5);
|
|
2305
3052
|
uint mask = texelFetch(nodeState, t, 0).r;
|
|
2306
3053
|
Look look = resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y), 0);
|
|
3054
|
+
vUv = mix(glyphUv.xy, glyphUv.zw, corner);
|
|
3055
|
+
vOpacity = look.opacity;
|
|
3056
|
+
uint kind = glyphKind & 3u;
|
|
3057
|
+
if (kind == 2u) {
|
|
3058
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
3059
|
+
float height = badgeHeight(style);
|
|
3060
|
+
if ((flags & 8u) == 0u || highlighted != (overlay == 1) || height < ${12 .toFixed(1)}) {
|
|
3061
|
+
gl_Position = hidden;
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
float font = height * ${badgeFont.toFixed(2)};
|
|
3065
|
+
vec2 center = badgeCenter(texelFetch(nodePosition, t, 0).xy, style, (glyphKind >> 2u) & 3u);
|
|
3066
|
+
vScale = font / atlasFontSize;
|
|
3067
|
+
vInk = paletteColor(glyphKind >> 4u);
|
|
3068
|
+
vHalo = 0.0;
|
|
3069
|
+
gl_Position = toClip(center - vec2(0.0, font * 0.65) + (glyphRect.xy + corner * glyphRect.zw) * vScale);
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
2307
3072
|
float primary = (flags & 4u) != 0u ? 12.0 : 12.0 * min(1.0, zoom);
|
|
2308
3073
|
if ((flags & 8u) == 0u || look.label < 0.5 || primary < labelMin) {
|
|
2309
3074
|
gl_Position = hidden;
|
|
2310
3075
|
return;
|
|
2311
3076
|
}
|
|
2312
|
-
float size =
|
|
3077
|
+
float size = kind == 0u ? primary : primary * 11.0 / 12.0;
|
|
2313
3078
|
float scale = size / atlasFontSize;
|
|
2314
|
-
float below = nodeRadius(style) + 3.0 + (
|
|
3079
|
+
float below = nodeRadius(style) + 3.0 + (kind == 1u ? primary * 1.25 : 0.0);
|
|
2315
3080
|
vec2 anchor = toScreen(texelFetch(nodePosition, t, 0).xy) + vec2(0.0, below);
|
|
2316
|
-
vUv = mix(glyphUv.xy, glyphUv.zw, corner);
|
|
2317
3081
|
vScale = scale;
|
|
2318
|
-
|
|
2319
|
-
|
|
3082
|
+
vInk = kind == 0u ? labelInk : labelMuted;
|
|
3083
|
+
vHalo = 1.0;
|
|
2320
3084
|
gl_Position = toClip(anchor + (glyphRect.xy + corner * glyphRect.zw) * scale);
|
|
2321
3085
|
}
|
|
2322
3086
|
`;
|
|
@@ -2324,21 +3088,78 @@ const labelFragment = `${header}
|
|
|
2324
3088
|
uniform sampler2D atlas;
|
|
2325
3089
|
in vec2 vUv;
|
|
2326
3090
|
in float vScale;
|
|
2327
|
-
flat in
|
|
3091
|
+
flat in vec4 vInk;
|
|
3092
|
+
flat in float vHalo;
|
|
2328
3093
|
flat in float vOpacity;
|
|
2329
3094
|
out vec4 fragColor;
|
|
2330
3095
|
void main() {
|
|
2331
3096
|
float d = texture(atlas, vUv).r;
|
|
2332
3097
|
float gamma = 0.08 / max(vScale * pixelRatio, 0.05);
|
|
2333
3098
|
float ink = smoothstep(0.75 - gamma, 0.75 + gamma, d);
|
|
2334
|
-
float halo = smoothstep(0.55 - gamma, 0.55 + gamma, d);
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
float alpha = max(halo * labelHalo.a, ink * inkColor.a) * vOpacity;
|
|
3099
|
+
float halo = smoothstep(0.55 - gamma, 0.55 + gamma, d) * vHalo;
|
|
3100
|
+
vec3 rgb = vHalo > 0.5 ? mix(labelHalo.rgb, vInk.rgb, ink) : vInk.rgb;
|
|
3101
|
+
float alpha = max(halo * labelHalo.a, ink * vInk.a) * vOpacity;
|
|
2338
3102
|
if (alpha < 0.002) discard;
|
|
2339
3103
|
fragColor = vec4(rgb * alpha, alpha);
|
|
2340
3104
|
}
|
|
2341
3105
|
`;
|
|
3106
|
+
const badgeVertex = `${header}${nodeData}
|
|
3107
|
+
in vec2 corner;
|
|
3108
|
+
in uint badgeNode;
|
|
3109
|
+
in uint badgeInfo;
|
|
3110
|
+
in uint badgeFill;
|
|
3111
|
+
in uint badgeRingColor;
|
|
3112
|
+
in float badgeText;
|
|
3113
|
+
out vec2 vLocal;
|
|
3114
|
+
flat out vec2 vHalf;
|
|
3115
|
+
flat out vec4 vFill;
|
|
3116
|
+
flat out vec4 vRing;
|
|
3117
|
+
flat out float vOpacity;
|
|
3118
|
+
void main() {
|
|
3119
|
+
ivec2 t = texel(badgeNode);
|
|
3120
|
+
vec4 style = texelFetch(nodeStyle, t, 0);
|
|
3121
|
+
uvec4 paint = texelFetch(nodePaint, t, 0);
|
|
3122
|
+
uint flags = uint(style.w + 0.5);
|
|
3123
|
+
uint mask = texelFetch(nodeState, t, 0).r;
|
|
3124
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
3125
|
+
float height = badgeHeight(style);
|
|
3126
|
+
if ((flags & 8u) == 0u || highlighted != (overlay == 1) || height < ${10 .toFixed(1)}) {
|
|
3127
|
+
gl_Position = hidden;
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
Look look = resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y), 0);
|
|
3131
|
+
bool text = (badgeInfo & 4u) != 0u && height >= ${12 .toFixed(1)};
|
|
3132
|
+
float font = height * ${badgeFont.toFixed(2)};
|
|
3133
|
+
float width = text ? max(height, badgeText * font / ${24 .toFixed(1)} + height * 0.6) : height;
|
|
3134
|
+
vec2 center = badgeCenter(texelFetch(nodePosition, t, 0).xy, style, badgeInfo & 3u);
|
|
3135
|
+
vHalf = vec2(width, height) * 0.5;
|
|
3136
|
+
vLocal = corner * (vHalf + ${badgeRing.toFixed(1)} + 1.0);
|
|
3137
|
+
vFill = paletteColor(badgeFill);
|
|
3138
|
+
vRing = paletteColor(badgeRingColor);
|
|
3139
|
+
vOpacity = look.opacity;
|
|
3140
|
+
gl_Position = toClip(center + vLocal);
|
|
3141
|
+
}
|
|
3142
|
+
`;
|
|
3143
|
+
const badgeFragment = `${header}
|
|
3144
|
+
in vec2 vLocal;
|
|
3145
|
+
flat in vec2 vHalf;
|
|
3146
|
+
flat in vec4 vFill;
|
|
3147
|
+
flat in vec4 vRing;
|
|
3148
|
+
flat in float vOpacity;
|
|
3149
|
+
out vec4 fragColor;
|
|
3150
|
+
void main() {
|
|
3151
|
+
float aa = 0.5 / pixelRatio + 0.25;
|
|
3152
|
+
vec2 q = vec2(max(abs(vLocal.x) - (vHalf.x - vHalf.y), 0.0), vLocal.y);
|
|
3153
|
+
float d = length(q) - vHalf.y;
|
|
3154
|
+
float inner = 1.0 - smoothstep(-aa, aa, d);
|
|
3155
|
+
float outer = 1.0 - smoothstep(${badgeRing.toFixed(1)} - aa, ${badgeRing.toFixed(1)} + aa, d);
|
|
3156
|
+
vec4 fill = vec4(vFill.rgb * vFill.a, vFill.a);
|
|
3157
|
+
vec4 ring = vec4(vRing.rgb * vRing.a, vRing.a);
|
|
3158
|
+
vec4 result = mix(ring, fill, inner) * outer * vOpacity;
|
|
3159
|
+
if (result.a < 0.002) discard;
|
|
3160
|
+
fragColor = result;
|
|
3161
|
+
}
|
|
3162
|
+
`;
|
|
2342
3163
|
const cornerNode = new Float32Array([
|
|
2343
3164
|
-1,
|
|
2344
3165
|
-1,
|
|
@@ -2387,6 +3208,27 @@ const cornerEdge = new Float32Array([
|
|
|
2387
3208
|
1,
|
|
2388
3209
|
2
|
|
2389
3210
|
]);
|
|
3211
|
+
function curveCorners() {
|
|
3212
|
+
const values = [
|
|
3213
|
+
0,
|
|
3214
|
+
-1,
|
|
3215
|
+
1,
|
|
3216
|
+
0,
|
|
3217
|
+
1,
|
|
3218
|
+
1,
|
|
3219
|
+
1,
|
|
3220
|
+
-1,
|
|
3221
|
+
1,
|
|
3222
|
+
1,
|
|
3223
|
+
1,
|
|
3224
|
+
1
|
|
3225
|
+
];
|
|
3226
|
+
for (let station = 1; station <= 25; station++) values.push(station, -1, 0, station, 1, 0);
|
|
3227
|
+
const end = 25;
|
|
3228
|
+
values.push(end, -1, 2, end, 1, 2, 26, -1, 2, 26, 1, 2);
|
|
3229
|
+
return new Float32Array(values);
|
|
3230
|
+
}
|
|
3231
|
+
const cornerCurve = curveCorners();
|
|
2390
3232
|
const cornerLabel = new Float32Array([
|
|
2391
3233
|
0,
|
|
2392
3234
|
0,
|
|
@@ -2404,6 +3246,7 @@ const blocks = {
|
|
|
2404
3246
|
};
|
|
2405
3247
|
const paletteWidth = 256;
|
|
2406
3248
|
const labelInterval = 120;
|
|
3249
|
+
const badgeStride = 20;
|
|
2407
3250
|
const units = {
|
|
2408
3251
|
palette: 0,
|
|
2409
3252
|
nodePosition: 1,
|
|
@@ -2413,7 +3256,9 @@ const units = {
|
|
|
2413
3256
|
edgeTopology: 5,
|
|
2414
3257
|
edgeStyle: 6,
|
|
2415
3258
|
edgeState: 7,
|
|
2416
|
-
atlas: 8
|
|
3259
|
+
atlas: 8,
|
|
3260
|
+
images: 9,
|
|
3261
|
+
nodeImage: 10
|
|
2417
3262
|
};
|
|
2418
3263
|
function iota(length) {
|
|
2419
3264
|
const values = new Uint32Array(length);
|
|
@@ -2423,7 +3268,7 @@ function iota(length) {
|
|
|
2423
3268
|
function rgba(color) {
|
|
2424
3269
|
return color.map((channel) => channel / 255);
|
|
2425
3270
|
}
|
|
2426
|
-
function createWebglRenderer(canvas) {
|
|
3271
|
+
function createWebglRenderer(canvas, invalidate = () => {}) {
|
|
2427
3272
|
const context = canvas.getContext("webgl2", {
|
|
2428
3273
|
alpha: true,
|
|
2429
3274
|
antialias: false,
|
|
@@ -2440,6 +3285,7 @@ function createWebglRenderer(canvas) {
|
|
|
2440
3285
|
const nodeStyle = new DataTexture(gl, format.rgba32f, f32);
|
|
2441
3286
|
const nodePaint = new DataTexture(gl, format.rgba32ui, u32);
|
|
2442
3287
|
const nodeState = new DataTexture(gl, format.r32ui, u32);
|
|
3288
|
+
const nodeImage = new DataTexture(gl, format.rgba32ui, u32);
|
|
2443
3289
|
const edgeTopology = new DataTexture(gl, format.rgba32ui, u32);
|
|
2444
3290
|
const edgeStyle = new DataTexture(gl, format.rgba32f, f32);
|
|
2445
3291
|
const edgeState = new DataTexture(gl, format.r32ui, u32);
|
|
@@ -2447,8 +3293,12 @@ function createWebglRenderer(canvas) {
|
|
|
2447
3293
|
nodePosition,
|
|
2448
3294
|
nodeStyle,
|
|
2449
3295
|
nodePaint,
|
|
2450
|
-
nodeState
|
|
3296
|
+
nodeState,
|
|
3297
|
+
nodeImage
|
|
2451
3298
|
];
|
|
3299
|
+
const images = new ImageAtlas(gl, invalidate);
|
|
3300
|
+
const scaleBits = /* @__PURE__ */ new Float32Array(1);
|
|
3301
|
+
const scaleWord = new Uint32Array(scaleBits.buffer);
|
|
2452
3302
|
const edgeTextures = [
|
|
2453
3303
|
edgeTopology,
|
|
2454
3304
|
edgeStyle,
|
|
@@ -2482,47 +3332,61 @@ function createWebglRenderer(canvas) {
|
|
|
2482
3332
|
const nodeProgram = createProgram(gl, nodeVertex, nodeFragment, blocks);
|
|
2483
3333
|
const edgeProgram = createProgram(gl, edgeVertex, edgeFragment, blocks);
|
|
2484
3334
|
const labelProgram = createProgram(gl, labelVertex, labelFragment, blocks);
|
|
3335
|
+
const badgeProgram = createProgram(gl, badgeVertex, badgeFragment, blocks);
|
|
2485
3336
|
const programs = [
|
|
2486
3337
|
nodeProgram,
|
|
2487
3338
|
edgeProgram,
|
|
2488
|
-
labelProgram
|
|
3339
|
+
labelProgram,
|
|
3340
|
+
badgeProgram
|
|
2489
3341
|
];
|
|
2490
|
-
|
|
3342
|
+
const locations = /* @__PURE__ */ new Map();
|
|
3343
|
+
function prepare(program) {
|
|
2491
3344
|
gl.useProgram(program);
|
|
2492
3345
|
for (const [name, unit] of Object.entries(units)) {
|
|
2493
3346
|
const location = gl.getUniformLocation(program, name);
|
|
2494
3347
|
if (location) gl.uniform1i(location, unit);
|
|
2495
3348
|
}
|
|
3349
|
+
locations.set(program, {
|
|
3350
|
+
layerSlot: gl.getUniformLocation(program, "layerSlot"),
|
|
3351
|
+
overlay: gl.getUniformLocation(program, "overlay")
|
|
3352
|
+
});
|
|
2496
3353
|
}
|
|
3354
|
+
for (const program of programs) prepare(program);
|
|
2497
3355
|
gl.useProgram(labelProgram);
|
|
2498
3356
|
gl.uniform1f(gl.getUniformLocation(labelProgram, "atlasFontSize"), 24);
|
|
2499
|
-
const locations = new Map(programs.map((program) => [program, {
|
|
2500
|
-
layerSlot: gl.getUniformLocation(program, "layerSlot"),
|
|
2501
|
-
overlay: gl.getUniformLocation(program, "overlay")
|
|
2502
|
-
}]));
|
|
2503
3357
|
const nodeCorner = createBuffer(gl, cornerNode, gl.STATIC_DRAW);
|
|
2504
3358
|
const edgeCorner = createBuffer(gl, cornerEdge, gl.STATIC_DRAW);
|
|
3359
|
+
const curveCorner = createBuffer(gl, cornerCurve, gl.STATIC_DRAW);
|
|
2505
3360
|
const labelCorner = createBuffer(gl, cornerLabel, gl.STATIC_DRAW);
|
|
2506
3361
|
const nodeIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.STATIC_DRAW);
|
|
2507
3362
|
const edgeIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.STATIC_DRAW);
|
|
2508
3363
|
const nodeOverlayIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
|
|
2509
3364
|
const edgeOverlayIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
|
|
3365
|
+
const curveIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
|
|
2510
3366
|
const labelMainBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
2511
3367
|
const labelHighlightBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
3368
|
+
const badgeBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
3369
|
+
const badgeGlyphBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
2512
3370
|
const warmIndex = createBuffer(gl, /* @__PURE__ */ new Uint32Array(1), gl.STATIC_DRAW);
|
|
2513
3371
|
const warmGlyph = createBuffer(gl, /* @__PURE__ */ new Uint8Array(32), gl.STATIC_DRAW);
|
|
3372
|
+
const warmBadge = createBuffer(gl, new Uint8Array(badgeStride), gl.STATIC_DRAW);
|
|
2514
3373
|
const buffers = [
|
|
2515
3374
|
nodeCorner,
|
|
2516
3375
|
edgeCorner,
|
|
3376
|
+
curveCorner,
|
|
2517
3377
|
labelCorner,
|
|
2518
3378
|
nodeIndices,
|
|
2519
3379
|
edgeIndices,
|
|
2520
3380
|
nodeOverlayIndices,
|
|
2521
3381
|
edgeOverlayIndices,
|
|
3382
|
+
curveIndices,
|
|
2522
3383
|
labelMainBuffer,
|
|
2523
3384
|
labelHighlightBuffer,
|
|
3385
|
+
badgeBuffer,
|
|
3386
|
+
badgeGlyphBuffer,
|
|
2524
3387
|
warmIndex,
|
|
2525
|
-
warmGlyph
|
|
3388
|
+
warmGlyph,
|
|
3389
|
+
warmBadge
|
|
2526
3390
|
];
|
|
2527
3391
|
const corner = (buffer, size = 2) => ({
|
|
2528
3392
|
name: "corner",
|
|
@@ -2581,24 +3445,61 @@ function createWebglRenderer(canvas) {
|
|
|
2581
3445
|
offset: 24
|
|
2582
3446
|
}
|
|
2583
3447
|
];
|
|
3448
|
+
const badgeField = (buffer, name, offset, type = gl.UNSIGNED_INT) => ({
|
|
3449
|
+
name,
|
|
3450
|
+
buffer,
|
|
3451
|
+
size: 1,
|
|
3452
|
+
type,
|
|
3453
|
+
integer: type === gl.UNSIGNED_INT,
|
|
3454
|
+
divisor: 1,
|
|
3455
|
+
stride: badgeStride,
|
|
3456
|
+
offset
|
|
3457
|
+
});
|
|
3458
|
+
const badgeAttributes = (buffer) => [
|
|
3459
|
+
corner(nodeCorner),
|
|
3460
|
+
badgeField(buffer, "badgeNode", 0),
|
|
3461
|
+
badgeField(buffer, "badgeInfo", 4),
|
|
3462
|
+
badgeField(buffer, "badgeFill", 8),
|
|
3463
|
+
badgeField(buffer, "badgeRingColor", 12),
|
|
3464
|
+
badgeField(buffer, "badgeText", 16, gl.FLOAT)
|
|
3465
|
+
];
|
|
2584
3466
|
const vertexArrays = {
|
|
2585
3467
|
nodes: createVertexArray(gl, nodeProgram, [corner(nodeCorner), instance(nodeIndices)]),
|
|
2586
3468
|
nodeOverlay: createVertexArray(gl, nodeProgram, [corner(nodeCorner), instance(nodeOverlayIndices)]),
|
|
2587
3469
|
edges: createVertexArray(gl, edgeProgram, [corner(edgeCorner, 3), instance(edgeIndices)]),
|
|
2588
3470
|
edgeOverlay: createVertexArray(gl, edgeProgram, [corner(edgeCorner, 3), instance(edgeOverlayIndices)]),
|
|
2589
3471
|
labels: createVertexArray(gl, labelProgram, glyphs(labelMainBuffer)),
|
|
2590
|
-
labelHighlight: createVertexArray(gl, labelProgram, glyphs(labelHighlightBuffer))
|
|
3472
|
+
labelHighlight: createVertexArray(gl, labelProgram, glyphs(labelHighlightBuffer)),
|
|
3473
|
+
badges: createVertexArray(gl, badgeProgram, badgeAttributes(badgeBuffer)),
|
|
3474
|
+
badgeGlyphs: createVertexArray(gl, labelProgram, glyphs(badgeGlyphBuffer))
|
|
2591
3475
|
};
|
|
2592
3476
|
const vertexCounts = /* @__PURE__ */ new Map([
|
|
2593
3477
|
[nodeProgram, cornerNode.length / 2],
|
|
2594
3478
|
[edgeProgram, cornerEdge.length / 3],
|
|
2595
|
-
[labelProgram, cornerLabel.length / 2]
|
|
3479
|
+
[labelProgram, cornerLabel.length / 2],
|
|
3480
|
+
[badgeProgram, cornerNode.length / 2]
|
|
2596
3481
|
]);
|
|
2597
3482
|
let warmed = false;
|
|
3483
|
+
let curvePass = null;
|
|
3484
|
+
function curves() {
|
|
3485
|
+
if (curvePass) return curvePass;
|
|
3486
|
+
const program = createProgram(gl, curveVertex, edgeFragment, blocks);
|
|
3487
|
+
prepare(program);
|
|
3488
|
+
programs.push(program);
|
|
3489
|
+
vertexCounts.set(program, cornerCurve.length / 3);
|
|
3490
|
+
curvePass = {
|
|
3491
|
+
program,
|
|
3492
|
+
main: createVertexArray(gl, program, [corner(curveCorner, 3), instance(curveIndices)]),
|
|
3493
|
+
overlay: createVertexArray(gl, program, [corner(curveCorner, 3), instance(edgeOverlayIndices)])
|
|
3494
|
+
};
|
|
3495
|
+
return curvePass;
|
|
3496
|
+
}
|
|
2598
3497
|
let nodeCapacity = 0;
|
|
2599
3498
|
let edgeCapacity = 0;
|
|
2600
3499
|
let nodeOverlayCount = 0;
|
|
2601
3500
|
let edgeOverlayCount = 0;
|
|
3501
|
+
const curved = /* @__PURE__ */ new Set();
|
|
3502
|
+
let curvedChanged = false;
|
|
2602
3503
|
let input = null;
|
|
2603
3504
|
const atlas = new GlyphAtlas("system-ui, sans-serif");
|
|
2604
3505
|
const labels = new LabelLayer();
|
|
@@ -2610,6 +3511,13 @@ function createWebglRenderer(canvas) {
|
|
|
2610
3511
|
let orderStale = true;
|
|
2611
3512
|
let lastLabelTime = Number.NEGATIVE_INFINITY;
|
|
2612
3513
|
let lastLabelView = "";
|
|
3514
|
+
const imageOf = [];
|
|
3515
|
+
const badged = /* @__PURE__ */ new Set();
|
|
3516
|
+
const badgeGlyphs = new GlyphBuffer();
|
|
3517
|
+
let badgeData = /* @__PURE__ */ new ArrayBuffer(1280);
|
|
3518
|
+
let badgeCount = 0;
|
|
3519
|
+
let badgesStale = false;
|
|
3520
|
+
let badgeGeneration = -1;
|
|
2613
3521
|
function packNode(nodes, index) {
|
|
2614
3522
|
const at = index * 4;
|
|
2615
3523
|
const flags = (nodes.shape[index] ?? 0) | (nodes.screenUnits[index] ?? 0) << 2 | (nodes.alive[index] ?? 0) << 3;
|
|
@@ -2620,6 +3528,73 @@ function createWebglRenderer(canvas) {
|
|
|
2620
3528
|
nodePaint.data[at] = nodes.fill[index] ?? 0;
|
|
2621
3529
|
nodePaint.data[at + 1] = nodes.stroke[index] ?? 0;
|
|
2622
3530
|
nodePaint.data[at + 2] = nodes.slot[index] ?? 0;
|
|
3531
|
+
const src = nodes.alive[index] === 1 ? nodes.image[index] ?? null : null;
|
|
3532
|
+
const previous = imageOf[index] ?? null;
|
|
3533
|
+
if (src !== previous) {
|
|
3534
|
+
if (previous) images.release(previous);
|
|
3535
|
+
if (src) images.acquire(src);
|
|
3536
|
+
imageOf[index] = src;
|
|
3537
|
+
}
|
|
3538
|
+
scaleBits[0] = nodes.imageScale[index] ?? 1;
|
|
3539
|
+
nodeImage.data[at] = src ? images.layer(src) + 1 : 0;
|
|
3540
|
+
nodeImage.data[at + 1] = nodes.imageColor[index] ?? 4294967295;
|
|
3541
|
+
nodeImage.data[at + 2] = scaleWord[0] ?? 0;
|
|
3542
|
+
const badges = nodes.alive[index] === 1 ? nodes.badges[index] : null;
|
|
3543
|
+
if (badges || badged.has(index)) badgesStale = true;
|
|
3544
|
+
if (badges) badged.add(index);
|
|
3545
|
+
else badged.delete(index);
|
|
3546
|
+
}
|
|
3547
|
+
function refreshImages(nodes) {
|
|
3548
|
+
const loaded = images.takeLoaded();
|
|
3549
|
+
if (!loaded) return;
|
|
3550
|
+
let first = Number.POSITIVE_INFINITY;
|
|
3551
|
+
let last = -1;
|
|
3552
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
3553
|
+
const src = imageOf[index];
|
|
3554
|
+
if (!src || !loaded.has(src)) continue;
|
|
3555
|
+
nodeImage.data[index * 4] = images.layer(src) + 1;
|
|
3556
|
+
first = Math.min(first, index);
|
|
3557
|
+
last = Math.max(last, index);
|
|
3558
|
+
}
|
|
3559
|
+
nodeImage.upload(first, last);
|
|
3560
|
+
}
|
|
3561
|
+
function rebuildBadges() {
|
|
3562
|
+
if (!input) return;
|
|
3563
|
+
const nodes = input.scene.nodes;
|
|
3564
|
+
const generation = atlas.generation;
|
|
3565
|
+
badgeGlyphs.clear();
|
|
3566
|
+
badgeCount = 0;
|
|
3567
|
+
let words = new Uint32Array(badgeData);
|
|
3568
|
+
let floats = new Float32Array(badgeData);
|
|
3569
|
+
for (const index of badged) for (const badge of nodes.badges[index] ?? []) {
|
|
3570
|
+
if ((badgeCount + 1) * badgeStride > badgeData.byteLength) {
|
|
3571
|
+
const grown = /* @__PURE__ */ new ArrayBuffer(badgeData.byteLength * 2);
|
|
3572
|
+
new Uint8Array(grown).set(new Uint8Array(badgeData));
|
|
3573
|
+
badgeData = grown;
|
|
3574
|
+
words = new Uint32Array(badgeData);
|
|
3575
|
+
floats = new Float32Array(badgeData);
|
|
3576
|
+
}
|
|
3577
|
+
const line = badge.text ? atlas.line(badge.text, "500") : null;
|
|
3578
|
+
const at = badgeCount * (badgeStride / 4);
|
|
3579
|
+
words[at] = index;
|
|
3580
|
+
words[at + 1] = badge.corner | (line ? 4 : 0);
|
|
3581
|
+
words[at + 2] = badge.fill;
|
|
3582
|
+
words[at + 3] = badge.ring;
|
|
3583
|
+
floats[at + 4] = line?.width ?? 0;
|
|
3584
|
+
if (line) badgeGlyphs.push(index, 2 | badge.corner << 2 | badge.color << 4, line, atlas.height);
|
|
3585
|
+
badgeCount++;
|
|
3586
|
+
}
|
|
3587
|
+
if (atlas.generation !== generation) {
|
|
3588
|
+
labelsStale = true;
|
|
3589
|
+
rebuildBadges();
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
badgesStale = false;
|
|
3593
|
+
badgeGeneration = atlas.generation;
|
|
3594
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, badgeBuffer);
|
|
3595
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(badgeData, 0, badgeCount * badgeStride), gl.DYNAMIC_DRAW);
|
|
3596
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, badgeGlyphBuffer);
|
|
3597
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(badgeGlyphs.data, 0, badgeGlyphs.count * 32), gl.DYNAMIC_DRAW);
|
|
2623
3598
|
}
|
|
2624
3599
|
function packEdge(edges, index) {
|
|
2625
3600
|
const at = index * 4;
|
|
@@ -2629,6 +3604,14 @@ function createWebglRenderer(canvas) {
|
|
|
2629
3604
|
edgeTopology.data[at + 3] = (edges.dash[index] ?? 0) | (edges.arrow[index] ?? 0) << 2 | (edges.alive[index] ?? 0) << 4 | (edges.slot[index] ?? 0) << 5;
|
|
2630
3605
|
edgeStyle.data[at] = edges.width[index] ?? 0;
|
|
2631
3606
|
edgeStyle.data[at + 1] = edges.opacity[index] ?? 1;
|
|
3607
|
+
const bend = edges.bend[index] ?? 0;
|
|
3608
|
+
edgeStyle.data[at + 2] = bend;
|
|
3609
|
+
const isCurved = edges.alive[index] === 1 && (bend !== 0 || edges.source[index] === edges.target[index]);
|
|
3610
|
+
if (isCurved !== curved.has(index)) {
|
|
3611
|
+
if (isCurved) curved.add(index);
|
|
3612
|
+
else curved.delete(index);
|
|
3613
|
+
curvedChanged = true;
|
|
3614
|
+
}
|
|
2632
3615
|
}
|
|
2633
3616
|
function syncNodes(next) {
|
|
2634
3617
|
const nodes = next.scene.nodes;
|
|
@@ -2651,6 +3634,7 @@ function createWebglRenderer(canvas) {
|
|
|
2651
3634
|
}
|
|
2652
3635
|
nodeStyle.upload(first, last);
|
|
2653
3636
|
nodePaint.upload(first, last);
|
|
3637
|
+
nodeImage.upload(first, last);
|
|
2654
3638
|
if (next.positionsChanged || next.full || grown) {
|
|
2655
3639
|
const xy = next.positions.xy;
|
|
2656
3640
|
nodePosition.data.set(xy.subarray(0, Math.min(xy.length, nodePosition.data.length)));
|
|
@@ -2688,6 +3672,11 @@ function createWebglRenderer(canvas) {
|
|
|
2688
3672
|
}
|
|
2689
3673
|
edgeTopology.upload(first, last);
|
|
2690
3674
|
edgeStyle.upload(first, last);
|
|
3675
|
+
if (curvedChanged) {
|
|
3676
|
+
curvedChanged = false;
|
|
3677
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, curveIndices);
|
|
3678
|
+
gl.bufferData(gl.ARRAY_BUFFER, Uint32Array.from(curved), gl.DYNAMIC_DRAW);
|
|
3679
|
+
}
|
|
2691
3680
|
const states = next.states;
|
|
2692
3681
|
const range = states.edgeDirty.take();
|
|
2693
3682
|
if (next.full || grown) {
|
|
@@ -2788,7 +3777,8 @@ function createWebglRenderer(canvas) {
|
|
|
2788
3777
|
const passes = [
|
|
2789
3778
|
[nodeProgram, [corner(nodeCorner), instance(warmIndex)]],
|
|
2790
3779
|
[edgeProgram, [corner(edgeCorner, 3), instance(warmIndex)]],
|
|
2791
|
-
[labelProgram, glyphs(warmGlyph)]
|
|
3780
|
+
[labelProgram, glyphs(warmGlyph)],
|
|
3781
|
+
[badgeProgram, badgeAttributes(warmBadge)]
|
|
2792
3782
|
];
|
|
2793
3783
|
gl.enable(gl.SCISSOR_TEST);
|
|
2794
3784
|
gl.scissor(0, 0, 0, 0);
|
|
@@ -2805,6 +3795,7 @@ function createWebglRenderer(canvas) {
|
|
|
2805
3795
|
sync(next) {
|
|
2806
3796
|
input = next;
|
|
2807
3797
|
syncNodes(next);
|
|
3798
|
+
refreshImages(next.scene.nodes);
|
|
2808
3799
|
syncEdges(next);
|
|
2809
3800
|
syncStates(next);
|
|
2810
3801
|
if (next.dirty.nodes.size > 0 || next.full) labelsStale = true;
|
|
@@ -2841,7 +3832,12 @@ function createWebglRenderer(canvas) {
|
|
|
2841
3832
|
frameBlock[7] = 8;
|
|
2842
3833
|
gl.bindBuffer(gl.UNIFORM_BUFFER, frameBuffer);
|
|
2843
3834
|
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, frameBlock);
|
|
3835
|
+
if (badgesStale || badgeGeneration !== atlas.generation) rebuildBadges();
|
|
3836
|
+
const generation = atlas.generation;
|
|
2844
3837
|
const wait = selectLabels(frame, performance.now());
|
|
3838
|
+
if (atlas.generation !== generation) rebuildBadges();
|
|
3839
|
+
syncAtlas();
|
|
3840
|
+
images.prepare();
|
|
2845
3841
|
gl.viewport(0, 0, width, height);
|
|
2846
3842
|
gl.clearColor(0, 0, 0, 0);
|
|
2847
3843
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
@@ -2849,11 +3845,23 @@ function createWebglRenderer(canvas) {
|
|
|
2849
3845
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
2850
3846
|
bind();
|
|
2851
3847
|
if (!warmed) warm();
|
|
2852
|
-
for (const layer of input?.scene.layers ?? []) if (layer.kind === "edges")
|
|
2853
|
-
|
|
3848
|
+
for (const layer of input?.scene.layers ?? []) if (layer.kind === "edges") {
|
|
3849
|
+
draw(edgeProgram, vertexArrays.edges, edgeCapacity, layer.slot, 0);
|
|
3850
|
+
if (curved.size > 0) {
|
|
3851
|
+
const pass = curves();
|
|
3852
|
+
draw(pass.program, pass.main, curved.size, layer.slot, 0);
|
|
3853
|
+
}
|
|
3854
|
+
} else draw(nodeProgram, vertexArrays.nodes, nodeCapacity, layer.slot, 0);
|
|
3855
|
+
draw(badgeProgram, vertexArrays.badges, badgeCount, 0, 0);
|
|
3856
|
+
draw(labelProgram, vertexArrays.badgeGlyphs, badgeGlyphs.count, 0, 0);
|
|
2854
3857
|
draw(labelProgram, vertexArrays.labels, labels.main.count, 0, 0);
|
|
2855
3858
|
draw(edgeProgram, vertexArrays.edgeOverlay, edgeOverlayCount, 0, 1);
|
|
3859
|
+
if (curvePass && curved.size > 0) draw(curvePass.program, curvePass.overlay, edgeOverlayCount, 0, 1);
|
|
2856
3860
|
draw(nodeProgram, vertexArrays.nodeOverlay, nodeOverlayCount, 0, 1);
|
|
3861
|
+
if (nodeOverlayCount > 0) {
|
|
3862
|
+
draw(badgeProgram, vertexArrays.badges, badgeCount, 0, 1);
|
|
3863
|
+
draw(labelProgram, vertexArrays.badgeGlyphs, badgeGlyphs.count, 0, 1);
|
|
3864
|
+
}
|
|
2857
3865
|
draw(labelProgram, vertexArrays.labelHighlight, labels.highlight.count, 0, 1);
|
|
2858
3866
|
gl.bindVertexArray(null);
|
|
2859
3867
|
return wait;
|
|
@@ -2862,12 +3870,17 @@ function createWebglRenderer(canvas) {
|
|
|
2862
3870
|
for (const texture of [...nodeTextures, ...edgeTextures]) texture.destroy();
|
|
2863
3871
|
gl.deleteTexture(palette);
|
|
2864
3872
|
gl.deleteTexture(atlasTexture);
|
|
3873
|
+
images.destroy();
|
|
2865
3874
|
for (const buffer of [
|
|
2866
3875
|
...buffers,
|
|
2867
3876
|
frameBuffer,
|
|
2868
3877
|
statesBuffer
|
|
2869
3878
|
]) gl.deleteBuffer(buffer);
|
|
2870
3879
|
for (const vao of Object.values(vertexArrays)) gl.deleteVertexArray(vao);
|
|
3880
|
+
if (curvePass) {
|
|
3881
|
+
gl.deleteVertexArray(curvePass.main);
|
|
3882
|
+
gl.deleteVertexArray(curvePass.overlay);
|
|
3883
|
+
}
|
|
2871
3884
|
for (const program of programs) gl.deleteProgram(program);
|
|
2872
3885
|
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
2873
3886
|
}
|
|
@@ -2882,12 +3895,15 @@ function createWebglRenderer(canvas) {
|
|
|
2882
3895
|
[units.edgeTopology, edgeTopology.texture],
|
|
2883
3896
|
[units.edgeStyle, edgeStyle.texture],
|
|
2884
3897
|
[units.edgeState, edgeState.texture],
|
|
2885
|
-
[units.atlas, atlasTexture]
|
|
3898
|
+
[units.atlas, atlasTexture],
|
|
3899
|
+
[units.nodeImage, nodeImage.texture]
|
|
2886
3900
|
];
|
|
2887
3901
|
for (const [unit, texture] of textures) {
|
|
2888
3902
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
2889
3903
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
2890
3904
|
}
|
|
3905
|
+
gl.activeTexture(gl.TEXTURE0 + units.images);
|
|
3906
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, images.texture);
|
|
2891
3907
|
}
|
|
2892
3908
|
}
|
|
2893
3909
|
// src/host/events.ts
|
|
@@ -2917,8 +3933,71 @@ var Emitter = class {
|
|
|
2917
3933
|
this.#handlers.clear();
|
|
2918
3934
|
}
|
|
2919
3935
|
};
|
|
3936
|
+
// src/host/keyboard.ts
|
|
3937
|
+
const arrows = {
|
|
3938
|
+
ArrowLeft: {
|
|
3939
|
+
x: -1,
|
|
3940
|
+
y: 0
|
|
3941
|
+
},
|
|
3942
|
+
ArrowRight: {
|
|
3943
|
+
x: 1,
|
|
3944
|
+
y: 0
|
|
3945
|
+
},
|
|
3946
|
+
ArrowUp: {
|
|
3947
|
+
x: 0,
|
|
3948
|
+
y: -1
|
|
3949
|
+
},
|
|
3950
|
+
ArrowDown: {
|
|
3951
|
+
x: 0,
|
|
3952
|
+
y: 1
|
|
3953
|
+
}
|
|
3954
|
+
};
|
|
3955
|
+
const keyZoom = 1.5;
|
|
3956
|
+
function keyAction(event) {
|
|
3957
|
+
if (event.altKey) return null;
|
|
3958
|
+
const command = event.ctrlKey || event.metaKey;
|
|
3959
|
+
const direction = arrows[event.key];
|
|
3960
|
+
if (direction) {
|
|
3961
|
+
if (command) return null;
|
|
3962
|
+
return event.shiftKey ? {
|
|
3963
|
+
type: "pan",
|
|
3964
|
+
direction
|
|
3965
|
+
} : {
|
|
3966
|
+
type: "move",
|
|
3967
|
+
direction
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
switch (event.key) {
|
|
3971
|
+
case "Enter":
|
|
3972
|
+
case " ": return {
|
|
3973
|
+
type: "activate",
|
|
3974
|
+
additive: event.shiftKey || command
|
|
3975
|
+
};
|
|
3976
|
+
case "Escape": return { type: "escape" };
|
|
3977
|
+
}
|
|
3978
|
+
if (command) return null;
|
|
3979
|
+
switch (event.key) {
|
|
3980
|
+
case "+":
|
|
3981
|
+
case "=": return {
|
|
3982
|
+
type: "zoom",
|
|
3983
|
+
factor: keyZoom
|
|
3984
|
+
};
|
|
3985
|
+
case "-":
|
|
3986
|
+
case "_": return {
|
|
3987
|
+
type: "zoom",
|
|
3988
|
+
factor: 1 / keyZoom
|
|
3989
|
+
};
|
|
3990
|
+
case "0": return { type: "fit" };
|
|
3991
|
+
}
|
|
3992
|
+
return null;
|
|
3993
|
+
}
|
|
2920
3994
|
// src/host/state-registry.ts
|
|
2921
|
-
const highlightBits =
|
|
3995
|
+
const highlightBits = 7;
|
|
3996
|
+
const highlightStates = [
|
|
3997
|
+
"selected",
|
|
3998
|
+
"hovered",
|
|
3999
|
+
"focused"
|
|
4000
|
+
];
|
|
2922
4001
|
const kinds = ["nodes", "edges"];
|
|
2923
4002
|
function members() {
|
|
2924
4003
|
return {
|
|
@@ -2990,6 +4069,7 @@ var StateRegistry = class {
|
|
|
2990
4069
|
apply(changes) {
|
|
2991
4070
|
let selection = false;
|
|
2992
4071
|
let hovered = false;
|
|
4072
|
+
let focused = false;
|
|
2993
4073
|
const forget = (kind, removed) => {
|
|
2994
4074
|
for (const { key, index } of removed) {
|
|
2995
4075
|
this.#setMask(kind, index, 0);
|
|
@@ -2997,6 +4077,7 @@ var StateRegistry = class {
|
|
|
2997
4077
|
if (!this.#members.get(name)?.[kind].delete(key)) continue;
|
|
2998
4078
|
if (name === "selected") selection = true;
|
|
2999
4079
|
if (name === "hovered") hovered = true;
|
|
4080
|
+
if (name === "focused") focused = true;
|
|
3000
4081
|
}
|
|
3001
4082
|
}
|
|
3002
4083
|
};
|
|
@@ -3009,7 +4090,8 @@ var StateRegistry = class {
|
|
|
3009
4090
|
learn("edges", changes.edges.added);
|
|
3010
4091
|
return {
|
|
3011
4092
|
selection,
|
|
3012
|
-
hovered
|
|
4093
|
+
hovered,
|
|
4094
|
+
focused
|
|
3013
4095
|
};
|
|
3014
4096
|
}
|
|
3015
4097
|
takeHighlighted() {
|
|
@@ -3017,7 +4099,7 @@ var StateRegistry = class {
|
|
|
3017
4099
|
this.#highlightDirty = false;
|
|
3018
4100
|
const indices = (kind) => {
|
|
3019
4101
|
const found = /* @__PURE__ */ new Set();
|
|
3020
|
-
for (const name of
|
|
4102
|
+
for (const name of highlightStates) for (const key of this.#members.get(name)?.[kind] ?? []) {
|
|
3021
4103
|
const index = this.#index(kind, key);
|
|
3022
4104
|
if (index >= 0) found.add(index);
|
|
3023
4105
|
}
|
|
@@ -3062,6 +4144,14 @@ var StateRegistry = class {
|
|
|
3062
4144
|
}
|
|
3063
4145
|
};
|
|
3064
4146
|
// src/host/mount-graph.ts
|
|
4147
|
+
const svgNamespace = "http://www.w3.org/2000/svg";
|
|
4148
|
+
const lassoStep = 3;
|
|
4149
|
+
const keyPan = .1;
|
|
4150
|
+
const followMargin = 48;
|
|
4151
|
+
const keyDuration = 200;
|
|
4152
|
+
const visuallyHidden = "position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap";
|
|
4153
|
+
const focusOutline = "2px solid var(--graph-focus-ring,#c93700)";
|
|
4154
|
+
const lassoStyle = "fill:color-mix(in srgb,var(--graph-accent,#c93700) 10%,transparent);stroke:var(--graph-accent,#c93700);stroke-width:1.5;stroke-dasharray:4 3;stroke-linejoin:round";
|
|
3065
4155
|
const defaultPadding = 32;
|
|
3066
4156
|
const clickSlop = 4;
|
|
3067
4157
|
const pickTolerance = 4;
|
|
@@ -3116,11 +4206,15 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3116
4206
|
element.setAttribute("data-graph", "");
|
|
3117
4207
|
element.setAttribute("role", "img");
|
|
3118
4208
|
element.setAttribute("aria-label", options.ariaLabel);
|
|
3119
|
-
element.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden";
|
|
4209
|
+
element.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden;outline:none;outline-offset:-2px";
|
|
3120
4210
|
const canvas = document.createElement("canvas");
|
|
3121
4211
|
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;outline:none";
|
|
3122
|
-
|
|
4212
|
+
const announcer = document.createElement("div");
|
|
4213
|
+
announcer.setAttribute("aria-live", "polite");
|
|
4214
|
+
announcer.style.cssText = visuallyHidden;
|
|
4215
|
+
element.append(canvas, announcer);
|
|
3123
4216
|
container.append(element);
|
|
4217
|
+
let remembered = null;
|
|
3124
4218
|
let frameRequest = 0;
|
|
3125
4219
|
let laterRequest;
|
|
3126
4220
|
let needsFlush = false;
|
|
@@ -3148,7 +4242,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3148
4242
|
const emitter = new Emitter();
|
|
3149
4243
|
const pickIndex = new PickIndex();
|
|
3150
4244
|
const themeResolver = new ThemeResolver(element);
|
|
3151
|
-
let renderer = createRenderer(canvas);
|
|
4245
|
+
let renderer = createRenderer(canvas, () => request());
|
|
3152
4246
|
registry.define(freeStateNames(definition.states));
|
|
3153
4247
|
if (options.selection !== void 0) registry.set("selected", options.selection);
|
|
3154
4248
|
const store = createStore({
|
|
@@ -3160,6 +4254,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3160
4254
|
width: 0,
|
|
3161
4255
|
height: 0,
|
|
3162
4256
|
hovered: null,
|
|
4257
|
+
focused: null,
|
|
3163
4258
|
selection: controlled ? registry.members("selected") : noKeys,
|
|
3164
4259
|
states: freeStates(),
|
|
3165
4260
|
positionsVersion: 0
|
|
@@ -3352,16 +4447,197 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3352
4447
|
updateCursor();
|
|
3353
4448
|
request();
|
|
3354
4449
|
}
|
|
4450
|
+
function nodePoint(key) {
|
|
4451
|
+
const record = model.node(key);
|
|
4452
|
+
return record ? {
|
|
4453
|
+
x: positions.x(record.index),
|
|
4454
|
+
y: positions.y(record.index)
|
|
4455
|
+
} : null;
|
|
4456
|
+
}
|
|
4457
|
+
function focusPoint(focus) {
|
|
4458
|
+
if (focus.kind === "node") return nodePoint(focus.key);
|
|
4459
|
+
const ends = model.endpoints(focus.key);
|
|
4460
|
+
const a = ends ? nodePoint(ends.source) : null;
|
|
4461
|
+
const b = ends ? nodePoint(ends.target) : null;
|
|
4462
|
+
return a && b ? {
|
|
4463
|
+
x: (a.x + b.x) / 2,
|
|
4464
|
+
y: (a.y + b.y) / 2
|
|
4465
|
+
} : null;
|
|
4466
|
+
}
|
|
4467
|
+
function labelOf(focus) {
|
|
4468
|
+
const record = focus.kind === "node" ? model.node(focus.key) : model.edge(focus.key);
|
|
4469
|
+
const labels = focus.kind === "node" ? scene.nodes.label : scene.edges.label;
|
|
4470
|
+
return record && labels[record.index] || String(focus.key);
|
|
4471
|
+
}
|
|
4472
|
+
function announce(text) {
|
|
4473
|
+
announcer.textContent = announcer.textContent === text ? `${text}\u00a0` : text;
|
|
4474
|
+
}
|
|
4475
|
+
function reveal(focus) {
|
|
4476
|
+
const point = focusPoint(focus);
|
|
4477
|
+
if (!point) return;
|
|
4478
|
+
const { view } = store.get();
|
|
4479
|
+
const size = viewport();
|
|
4480
|
+
const screen = toScreen(view, size, point);
|
|
4481
|
+
if (!(screen.x >= followMargin && screen.x <= size.width - followMargin && screen.y >= followMargin && screen.y <= size.height - followMargin)) animateTo({
|
|
4482
|
+
...view,
|
|
4483
|
+
x: point.x,
|
|
4484
|
+
y: point.y
|
|
4485
|
+
}, keyDuration);
|
|
4486
|
+
}
|
|
4487
|
+
function setFocused(next, follow = false) {
|
|
4488
|
+
if (sameTarget(store.get().focused, next)) return;
|
|
4489
|
+
registry.set("focused", next ? { [kindOf[next.kind]]: [next.key] } : {});
|
|
4490
|
+
patch({ focused: next });
|
|
4491
|
+
if (next) {
|
|
4492
|
+
element.style.outline = "none";
|
|
4493
|
+
announce(labelOf(next));
|
|
4494
|
+
if (follow) reveal(next);
|
|
4495
|
+
}
|
|
4496
|
+
emitter.emit("focuschange", { target: next });
|
|
4497
|
+
request();
|
|
4498
|
+
}
|
|
4499
|
+
function moveFocus(direction) {
|
|
4500
|
+
const current = store.get().focused;
|
|
4501
|
+
const from = current?.kind === "node" ? model.node(current.key)?.index ?? -1 : -1;
|
|
4502
|
+
let index = from >= 0 ? nodeToward(from, direction, scene.nodes, positions) : -1;
|
|
4503
|
+
if (from < 0) {
|
|
4504
|
+
const selected = registry.members("selected").nodes;
|
|
4505
|
+
const last = selected[selected.length - 1];
|
|
4506
|
+
const center = store.get().view;
|
|
4507
|
+
index = last !== void 0 ? model.node(last)?.index ?? -1 : nearestNode((current && focusPoint(current)) ?? center, scene.nodes, positions);
|
|
4508
|
+
}
|
|
4509
|
+
const key = index >= 0 ? scene.nodes.keys[index] : void 0;
|
|
4510
|
+
const next = key !== void 0 ? target("node", key) : null;
|
|
4511
|
+
if (next) setFocused(next, true);
|
|
4512
|
+
}
|
|
4513
|
+
function focusVisible() {
|
|
4514
|
+
try {
|
|
4515
|
+
return element.matches(":focus-visible");
|
|
4516
|
+
} catch {
|
|
4517
|
+
return true;
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
function onFocus() {
|
|
4521
|
+
if (!definition.interaction.keyboard || !focusVisible()) return;
|
|
4522
|
+
const restored = remembered ? target(remembered.kind, remembered.key) : null;
|
|
4523
|
+
remembered = null;
|
|
4524
|
+
if (restored) setFocused(restored, true);
|
|
4525
|
+
else if (!store.get().focused) element.style.outline = focusOutline;
|
|
4526
|
+
}
|
|
4527
|
+
function onBlur() {
|
|
4528
|
+
element.style.outline = "none";
|
|
4529
|
+
const current = store.get().focused;
|
|
4530
|
+
if (!current) return;
|
|
4531
|
+
remembered = current;
|
|
4532
|
+
setFocused(null);
|
|
4533
|
+
}
|
|
4534
|
+
function cancelLasso() {
|
|
4535
|
+
if (!gesture?.lasso) return false;
|
|
4536
|
+
if (canvas.hasPointerCapture?.(gesture.id)) canvas.releasePointerCapture(gesture.id);
|
|
4537
|
+
gesture = null;
|
|
4538
|
+
drawLasso(null);
|
|
4539
|
+
updateCursor();
|
|
4540
|
+
return true;
|
|
4541
|
+
}
|
|
4542
|
+
function runKey(event) {
|
|
4543
|
+
const action = keyAction(event);
|
|
4544
|
+
if (!action) return false;
|
|
4545
|
+
const { interaction } = definition;
|
|
4546
|
+
const { view, focused, selection } = store.get();
|
|
4547
|
+
const size = viewport();
|
|
4548
|
+
switch (action.type) {
|
|
4549
|
+
case "move":
|
|
4550
|
+
moveFocus(action.direction);
|
|
4551
|
+
return true;
|
|
4552
|
+
case "pan": {
|
|
4553
|
+
if (!interaction.pan) return false;
|
|
4554
|
+
const step = Math.min(size.width, size.height) * keyPan;
|
|
4555
|
+
commitView(panBy(view, -action.direction.x * step, -action.direction.y * step));
|
|
4556
|
+
return true;
|
|
4557
|
+
}
|
|
4558
|
+
case "zoom": {
|
|
4559
|
+
if (!interaction.zoom) return false;
|
|
4560
|
+
const point = focused ? focusPoint(focused) : null;
|
|
4561
|
+
animateTo(zoomAround(view, size, point ? toScreen(view, size, point) : {
|
|
4562
|
+
x: size.width / 2,
|
|
4563
|
+
y: size.height / 2
|
|
4564
|
+
}, action.factor), keyDuration);
|
|
4565
|
+
return true;
|
|
4566
|
+
}
|
|
4567
|
+
case "fit": return fitTo(void 0, { duration: keyDuration });
|
|
4568
|
+
case "activate":
|
|
4569
|
+
if (!focused || interaction.select === false) return false;
|
|
4570
|
+
selectTarget(focused, action.additive);
|
|
4571
|
+
return true;
|
|
4572
|
+
case "escape":
|
|
4573
|
+
if (cancelLasso()) return true;
|
|
4574
|
+
if (interaction.select !== false && selection.nodes.length + selection.edges.length > 0) {
|
|
4575
|
+
requestSelection(noKeys);
|
|
4576
|
+
return true;
|
|
4577
|
+
}
|
|
4578
|
+
if (!focused) return false;
|
|
4579
|
+
setFocused(null);
|
|
4580
|
+
return true;
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
function onKeyDown(event) {
|
|
4584
|
+
if (!definition.interaction.keyboard || event.target !== element) return;
|
|
4585
|
+
if (runKey(event)) event.preventDefault();
|
|
4586
|
+
}
|
|
4587
|
+
function applyKeyboard() {
|
|
4588
|
+
if (definition.interaction.keyboard) {
|
|
4589
|
+
element.tabIndex = 0;
|
|
4590
|
+
element.setAttribute("role", "application");
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
element.removeAttribute("tabindex");
|
|
4594
|
+
element.setAttribute("role", "img");
|
|
4595
|
+
element.style.outline = "none";
|
|
4596
|
+
setFocused(null);
|
|
4597
|
+
}
|
|
4598
|
+
let lassoLayer = null;
|
|
4599
|
+
function drawLasso(points) {
|
|
4600
|
+
if (!points) {
|
|
4601
|
+
lassoLayer?.remove();
|
|
4602
|
+
lassoLayer = null;
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
if (!lassoLayer) {
|
|
4606
|
+
lassoLayer = document.createElementNS(svgNamespace, "svg");
|
|
4607
|
+
lassoLayer.setAttribute("aria-hidden", "true");
|
|
4608
|
+
lassoLayer.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible";
|
|
4609
|
+
const path = document.createElementNS(svgNamespace, "path");
|
|
4610
|
+
path.style.cssText = lassoStyle;
|
|
4611
|
+
lassoLayer.append(path);
|
|
4612
|
+
element.append(lassoLayer);
|
|
4613
|
+
}
|
|
4614
|
+
lassoLayer.firstElementChild?.setAttribute("d", `M${points.map((point) => `${point.x} ${point.y}`).join("L")}Z`);
|
|
4615
|
+
}
|
|
4616
|
+
function selectInside(points) {
|
|
4617
|
+
const view = store.get().view;
|
|
4618
|
+
const size = viewport();
|
|
4619
|
+
const inside = nodesInside(points.map((point) => toWorld(view, size, point)), scene.nodes, positions).filter((key) => !registry.has("selected", "nodes", key));
|
|
4620
|
+
if (inside.length === 0) return;
|
|
4621
|
+
const current = registry.members("selected");
|
|
4622
|
+
requestSelection({
|
|
4623
|
+
...current,
|
|
4624
|
+
nodes: [...current.nodes, ...inside]
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
3355
4627
|
function updateCursor() {
|
|
3356
4628
|
const interactive = definition.interaction.select !== false || definition.interaction.drag;
|
|
3357
|
-
if (gesture?.
|
|
4629
|
+
if (gesture?.lasso) canvas.style.cursor = "crosshair";
|
|
4630
|
+
else if (gesture?.moved) canvas.style.cursor = "grabbing";
|
|
3358
4631
|
else if (store.get().hovered && interactive) canvas.style.cursor = "pointer";
|
|
3359
4632
|
else canvas.style.cursor = "";
|
|
3360
4633
|
}
|
|
3361
4634
|
function selectOnClick(clicked, event) {
|
|
4635
|
+
selectTarget(clicked, event.shiftKey || event.metaKey || event.ctrlKey);
|
|
4636
|
+
}
|
|
4637
|
+
function selectTarget(clicked, modifier) {
|
|
3362
4638
|
const mode = definition.interaction.select;
|
|
3363
4639
|
if (mode === false) return;
|
|
3364
|
-
const additive = mode === "multiple" &&
|
|
4640
|
+
const additive = mode === "multiple" && modifier;
|
|
3365
4641
|
if (!clicked) {
|
|
3366
4642
|
if (!additive) requestSelection(noKeys);
|
|
3367
4643
|
return;
|
|
@@ -3400,6 +4676,14 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3400
4676
|
const fresh = target(hovered.kind, hovered.key);
|
|
3401
4677
|
if (fresh && fresh.datum !== hovered.datum) patch({ hovered: fresh });
|
|
3402
4678
|
}
|
|
4679
|
+
const focused = store.get().focused;
|
|
4680
|
+
if (result.focused) {
|
|
4681
|
+
patch({ focused: null });
|
|
4682
|
+
emitter.emit("focuschange", { target: null });
|
|
4683
|
+
} else if (focused) {
|
|
4684
|
+
const fresh = target(focused.kind, focused.key);
|
|
4685
|
+
if (fresh && fresh.datum !== focused.datum) patch({ focused: fresh });
|
|
4686
|
+
}
|
|
3403
4687
|
}
|
|
3404
4688
|
function movePositions(indices, to, duration) {
|
|
3405
4689
|
if (indices.length === 0) return;
|
|
@@ -3581,18 +4865,24 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3581
4865
|
function onPointerDown(event) {
|
|
3582
4866
|
if (event.button !== 0 || gesture) return;
|
|
3583
4867
|
viewAnimation = null;
|
|
4868
|
+
remembered = null;
|
|
4869
|
+
setFocused(null);
|
|
3584
4870
|
const point = local(event);
|
|
3585
4871
|
const hit = pickAt(point);
|
|
3586
4872
|
const drag = hit ? startDrag(hit, point) : null;
|
|
4873
|
+
const { lasso, select } = definition.interaction;
|
|
4874
|
+
const lassoing = !hit && event.shiftKey && lasso && select === "multiple";
|
|
3587
4875
|
gesture = {
|
|
3588
4876
|
id: event.pointerId,
|
|
3589
4877
|
start: point,
|
|
3590
4878
|
last: point,
|
|
3591
4879
|
target: hit,
|
|
3592
4880
|
drag,
|
|
4881
|
+
lasso: lassoing ? [point] : null,
|
|
3593
4882
|
moved: false
|
|
3594
4883
|
};
|
|
3595
4884
|
canvas.setPointerCapture?.(event.pointerId);
|
|
4885
|
+
if (lassoing) updateCursor();
|
|
3596
4886
|
}
|
|
3597
4887
|
function onPointerMove(event) {
|
|
3598
4888
|
const point = local(event);
|
|
@@ -3605,6 +4895,16 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3605
4895
|
gesture.moved = true;
|
|
3606
4896
|
updateCursor();
|
|
3607
4897
|
}
|
|
4898
|
+
const lasso = gesture.lasso;
|
|
4899
|
+
if (lasso) {
|
|
4900
|
+
const last = lasso[lasso.length - 1] ?? gesture.start;
|
|
4901
|
+
if (gesture.moved && Math.hypot(point.x - last.x, point.y - last.y) >= lassoStep) {
|
|
4902
|
+
lasso.push(point);
|
|
4903
|
+
drawLasso(lasso);
|
|
4904
|
+
}
|
|
4905
|
+
gesture.last = point;
|
|
4906
|
+
return;
|
|
4907
|
+
}
|
|
3608
4908
|
const drag = gesture.drag;
|
|
3609
4909
|
if (drag) {
|
|
3610
4910
|
if (!gesture.moved) return;
|
|
@@ -3641,6 +4941,10 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3641
4941
|
gesture = null;
|
|
3642
4942
|
if (canvas.hasPointerCapture?.(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
|
|
3643
4943
|
if (ended.drag?.started) emitter.emit("dragend", dragEvent(ended.drag, event));
|
|
4944
|
+
if (ended.lasso) {
|
|
4945
|
+
drawLasso(null);
|
|
4946
|
+
if (!cancelled && ended.moved) selectInside(ended.lasso);
|
|
4947
|
+
}
|
|
3644
4948
|
if (!cancelled && !ended.moved) {
|
|
3645
4949
|
emitter.emit("click", pointerEvent(event, ended.target));
|
|
3646
4950
|
selectOnClick(ended.target, event);
|
|
@@ -3678,7 +4982,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3678
4982
|
}
|
|
3679
4983
|
function onContextRestored() {
|
|
3680
4984
|
contextLost = false;
|
|
3681
|
-
renderer = createRenderer(canvas);
|
|
4985
|
+
renderer = createRenderer(canvas, () => request());
|
|
3682
4986
|
full = true;
|
|
3683
4987
|
refreshTheme();
|
|
3684
4988
|
}
|
|
@@ -3699,6 +5003,13 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3699
5003
|
["webglcontextrestored", onContextRestored]
|
|
3700
5004
|
];
|
|
3701
5005
|
for (const [name, listener, listenerOptions] of listeners) canvas.addEventListener(name, listener, listenerOptions);
|
|
5006
|
+
const elementListeners = [
|
|
5007
|
+
["keydown", onKeyDown],
|
|
5008
|
+
["focus", onFocus],
|
|
5009
|
+
["blur", onBlur]
|
|
5010
|
+
];
|
|
5011
|
+
for (const [name, listener] of elementListeners) element.addEventListener(name, listener);
|
|
5012
|
+
applyKeyboard();
|
|
3702
5013
|
model.setMarks(definition.marks);
|
|
3703
5014
|
scene.setDefinition(definition);
|
|
3704
5015
|
applyLayers();
|
|
@@ -3733,6 +5044,15 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3733
5044
|
set: (keys) => requestSelection(toKeys(keys)),
|
|
3734
5045
|
clear: () => requestSelection(noKeys)
|
|
3735
5046
|
},
|
|
5047
|
+
focus: {
|
|
5048
|
+
get: () => store.get().focused,
|
|
5049
|
+
set: (next) => {
|
|
5050
|
+
const resolved = next ? target(next.kind, next.key) : null;
|
|
5051
|
+
if (next && !resolved) return;
|
|
5052
|
+
if (resolved && definition.interaction.keyboard) element.focus({ preventScroll: true });
|
|
5053
|
+
setFocused(resolved, true);
|
|
5054
|
+
}
|
|
5055
|
+
},
|
|
3736
5056
|
states: {
|
|
3737
5057
|
set: (name, keys) => {
|
|
3738
5058
|
if (registry.set(declared(name), keys)) publishState(name);
|
|
@@ -3795,6 +5115,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3795
5115
|
applyLayers();
|
|
3796
5116
|
if (registry.define(freeStateNames(definition.states))) patch({ states: freeStates() });
|
|
3797
5117
|
applyStyles();
|
|
5118
|
+
applyKeyboard();
|
|
3798
5119
|
updateCursor();
|
|
3799
5120
|
request(true);
|
|
3800
5121
|
}
|
|
@@ -3807,6 +5128,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3807
5128
|
resizeObserver.disconnect();
|
|
3808
5129
|
stopTheme();
|
|
3809
5130
|
for (const [name, listener] of listeners) canvas.removeEventListener(name, listener);
|
|
5131
|
+
for (const [name, listener] of elementListeners) element.removeEventListener(name, listener);
|
|
3810
5132
|
emitter.clear();
|
|
3811
5133
|
model.destroy();
|
|
3812
5134
|
renderer.destroy();
|
|
@@ -3820,4 +5142,4 @@ function mountGraph(container, options) {
|
|
|
3820
5142
|
}
|
|
3821
5143
|
export { freeStateNames as n, maxFreeStates as r, mountGraph as t };
|
|
3822
5144
|
|
|
3823
|
-
//# sourceMappingURL=mount-graph-
|
|
5145
|
+
//# sourceMappingURL=mount-graph-CzVfiIte.js.map
|