@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.
@@ -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 source = edges.source[index] ?? 0;
558
- const target = edges.target[index] ?? 0;
559
- traverse(frame, positions.x(source), positions.y(source), positions.x(target), positions.y(target), emit);
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
- const distance = Math.abs((x - ax) * dy - (y - ay) * dx) / length;
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 === "object" && badge !== null) {
1169
- badges[corner] = badge;
1170
- count++;
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 count === 0 ? null : badges;
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
- nodes.image[index] = toText(mark.image?.(datum));
1263
- nodes.badges[index] = readBadges(mark, datum);
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 place = (index, force) => {
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 = force ? this.highlight : this.main;
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
- // src/webgl/shaders.ts
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 = 3u;
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 color = mix(vFill, vStroke, ring);
2155
- float alpha = color.a * inside * vOpacity;
2156
- vec4 result = vec4(color.rgb * alpha, alpha);
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 uint vKind;
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 = glyphKind == 0u ? primary : primary * 11.0 / 12.0;
3077
+ float size = kind == 0u ? primary : primary * 11.0 / 12.0;
2313
3078
  float scale = size / atlasFontSize;
2314
- float below = nodeRadius(style) + 3.0 + (glyphKind == 1u ? primary * 1.25 : 0.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
- vKind = glyphKind;
2319
- vOpacity = look.opacity;
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 uint vKind;
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
- vec4 inkColor = vKind == 0u ? labelInk : labelMuted;
2336
- vec3 rgb = mix(labelHalo.rgb, inkColor.rgb, ink);
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
- for (const program of programs) {
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") draw(edgeProgram, vertexArrays.edges, edgeCapacity, layer.slot, 0);
2853
- else draw(nodeProgram, vertexArrays.nodes, nodeCapacity, layer.slot, 0);
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 = 3;
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 ["selected", "hovered"]) for (const key of this.#members.get(name)?.[kind] ?? []) {
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
- element.append(canvas);
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?.moved) canvas.style.cursor = "grabbing";
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" && (event.shiftKey || event.metaKey || event.ctrlKey);
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-BHwq3LOr.js.map
5145
+ //# sourceMappingURL=mount-graph-CzVfiIte.js.map