@bruno00o/graph 0.1.0 → 0.2.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,92 @@ function createGraphModel(options = {}) {
449
449
  }
450
450
  };
451
451
  }
452
+ // src/core/edge-curve.ts
453
+ const loopTurn = .6;
454
+ const loopGrowth = .5;
455
+ const maxBend = .12;
456
+ const spreadBend = .4;
457
+ function laneBend(position, count) {
458
+ if (count < 2) return 0;
459
+ const step = Math.min(maxBend, spreadBend / (count - 1));
460
+ return (position - (count - 1) / 2) * step;
461
+ }
462
+ function loopReach(radius, ordinal, minimum) {
463
+ return radius + Math.max(radius * 1.5, minimum) * (1 + loopGrowth * ordinal);
464
+ }
465
+ function parallelCurve(ax, ay, dx, dy, bend) {
466
+ const length = Math.hypot(dx - ax, dy - ay);
467
+ const nx = length > 0 ? -(dy - ay) / length : 0;
468
+ const ny = length > 0 ? (dx - ax) / length : 1;
469
+ const offset = 2 * bend * length;
470
+ const px = (ax + dx) / 2 + nx * offset;
471
+ const py = (ay + dy) / 2 + ny * offset;
472
+ return {
473
+ ax,
474
+ ay,
475
+ bx: ax + (px - ax) * 2 / 3,
476
+ by: ay + (py - ay) * 2 / 3,
477
+ cx: dx + (px - dx) * 2 / 3,
478
+ cy: dy + (py - dy) * 2 / 3,
479
+ dx,
480
+ dy
481
+ };
482
+ }
483
+ function loopCurve(x, y, reach) {
484
+ const arm = reach / (.75 * Math.cos(loopTurn));
485
+ const ux = Math.SQRT1_2;
486
+ const uy = -Math.SQRT1_2;
487
+ const cos = Math.cos(loopTurn);
488
+ const sin = Math.sin(loopTurn);
489
+ return {
490
+ ax: x,
491
+ ay: y,
492
+ bx: x + arm * (ux * cos - uy * sin),
493
+ by: y + arm * (ux * sin + uy * cos),
494
+ cx: x + arm * (ux * cos + uy * sin),
495
+ cy: y + arm * (-ux * sin + uy * cos),
496
+ dx: x,
497
+ dy: y
498
+ };
499
+ }
500
+ function pointOnCubic(curve, t) {
501
+ const u = 1 - t;
502
+ const a = u * u * u;
503
+ const b = 3 * u * u * t;
504
+ const c = 3 * u * t * t;
505
+ const d = t * t * t;
506
+ return {
507
+ x: a * curve.ax + b * curve.bx + c * curve.cx + d * curve.dx,
508
+ y: a * curve.ay + b * curve.by + c * curve.cy + d * curve.dy
509
+ };
510
+ }
511
+ function sampleCubic(curve, segments) {
512
+ const points = new Float64Array((segments + 1) * 2);
513
+ for (let step = 0; step <= segments; step++) {
514
+ const point = pointOnCubic(curve, step / segments);
515
+ points[step * 2] = point.x;
516
+ points[step * 2 + 1] = point.y;
517
+ }
518
+ return points;
519
+ }
520
+ function distanceToCubic(curve, x, y, segments) {
521
+ const points = sampleCubic(curve, segments);
522
+ let best = Number.POSITIVE_INFINITY;
523
+ for (let step = 0; step < segments; step++) {
524
+ const ax = points[step * 2] ?? 0;
525
+ const ay = points[step * 2 + 1] ?? 0;
526
+ const vx = (points[step * 2 + 2] ?? 0) - ax;
527
+ const vy = (points[step * 2 + 3] ?? 0) - ay;
528
+ const squared = vx * vx + vy * vy;
529
+ const t = squared > 0 ? Math.min(1, Math.max(0, ((x - ax) * vx + (y - ay) * vy) / squared)) : 0;
530
+ best = Math.min(best, Math.hypot(x - (ax + vx * t), y - (ay + vy * t)));
531
+ }
532
+ return best;
533
+ }
452
534
  // src/core/pick-index.ts
453
535
  const minimumPickRadius = 2;
536
+ const pickSegments = 16;
537
+ const indexSegments = 8;
454
538
  function createGrid(frame, count, visit) {
455
539
  const cells = frame.cols * frame.rows;
456
540
  const offsets = new Uint32Array(cells + 1);
@@ -505,6 +589,7 @@ var PickIndex = class {
505
589
  #maxWorldRadius = 0;
506
590
  #maxScreenRadius = 0;
507
591
  #maxEdgeHalfWidth = 0;
592
+ #loops = [];
508
593
  build(source) {
509
594
  const { nodes, edges, positions } = source;
510
595
  let count = 0;
@@ -530,8 +615,25 @@ var PickIndex = class {
530
615
  if (count === 0) {
531
616
  this.#nodes = null;
532
617
  this.#edges = null;
618
+ this.#loops = [];
533
619
  return;
534
620
  }
621
+ this.#loops = [];
622
+ for (let index = 0; index < edges.capacity; index++) {
623
+ if (edges.alive[index] !== 1) continue;
624
+ if (edges.source[index] === edges.target[index]) {
625
+ this.#loops.push(index);
626
+ continue;
627
+ }
628
+ if ((edges.bend[index] ?? 0) === 0) continue;
629
+ const points = sampleCubic(this.#parallel(source, index), indexSegments);
630
+ for (let at = 0; at < points.length; at += 2) {
631
+ minX = Math.min(minX, points[at] ?? minX);
632
+ maxX = Math.max(maxX, points[at] ?? maxX);
633
+ minY = Math.min(minY, points[at + 1] ?? minY);
634
+ maxY = Math.max(maxY, points[at + 1] ?? maxY);
635
+ }
636
+ }
535
637
  const spanX = Math.max(maxX - minX, 1);
536
638
  const spanY = Math.max(maxY - minY, 1);
537
639
  let cell = Math.max(this.#maxWorldRadius * 2, Math.sqrt(spanX * spanY / count) * 2, .001);
@@ -554,9 +656,15 @@ var PickIndex = class {
554
656
  this.#edges = createGrid(frame, edges.capacity, (index, emit) => {
555
657
  if (edges.alive[index] !== 1) return;
556
658
  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);
659
+ const from = edges.source[index] ?? 0;
660
+ const to = edges.target[index] ?? 0;
661
+ if (from === to) return;
662
+ if ((edges.bend[index] ?? 0) === 0) {
663
+ traverse(frame, positions.x(from), positions.y(from), positions.x(to), positions.y(to), emit);
664
+ return;
665
+ }
666
+ const points = sampleCubic(this.#parallel(source, index), indexSegments);
667
+ 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
668
  });
561
669
  }
562
670
  pick(source, x, y, zoom, tolerance) {
@@ -582,6 +690,18 @@ var PickIndex = class {
582
690
  for (let at = grid.offsets[cell] ?? 0; at < end; at++) visit(grid.items[at] ?? 0);
583
691
  }
584
692
  }
693
+ #parallel(source, index) {
694
+ const { edges, positions } = source;
695
+ const from = edges.source[index] ?? 0;
696
+ const to = edges.target[index] ?? 0;
697
+ return parallelCurve(positions.x(from), positions.y(from), positions.x(to), positions.y(to), edges.bend[index] ?? 0);
698
+ }
699
+ #loop(source, index, zoom) {
700
+ const node = source.edges.source[index] ?? 0;
701
+ const size = (source.nodes.size[node] ?? 0) / 2;
702
+ const reach = loopReach(Math.max(source.nodes.screenUnits[node] === 1 ? size : size * zoom, 1.25), source.edges.bend[index] ?? 0, 12) / zoom;
703
+ return loopCurve(source.positions.x(node), source.positions.y(node), reach);
704
+ }
585
705
  #radius(source, index, zoom) {
586
706
  const size = (source.nodes.size[index] ?? 0) / 2;
587
707
  const radius = source.nodes.screenUnits[index] === 1 ? size / zoom : size;
@@ -612,10 +732,26 @@ var PickIndex = class {
612
732
  let best = -1;
613
733
  let bestDistance = Number.POSITIVE_INFINITY;
614
734
  let bestOrder = Number.NEGATIVE_INFINITY;
735
+ const consider = (index, distance) => {
736
+ if (distance > ((edges.width[index] ?? 0) / 2 + tolerance) / zoom) return;
737
+ const order = source.order("edges", index);
738
+ if (distance < bestDistance || distance === bestDistance && order > bestOrder) {
739
+ best = index;
740
+ bestDistance = distance;
741
+ bestOrder = order;
742
+ }
743
+ };
744
+ const seen = /* @__PURE__ */ new Set();
615
745
  this.#cells(grid, x, y, reach, (index) => {
616
746
  if (edges.alive[index] !== 1) return;
617
747
  const s = edges.source[index] ?? 0;
618
748
  const t = edges.target[index] ?? 0;
749
+ if ((edges.bend[index] ?? 0) !== 0) {
750
+ if (seen.has(index)) return;
751
+ seen.add(index);
752
+ consider(index, distanceToCubic(this.#parallel(source, index), x, y, pickSegments));
753
+ return;
754
+ }
619
755
  const ax = positions.x(s);
620
756
  const ay = positions.y(s);
621
757
  const dx = positions.x(t) - ax;
@@ -624,15 +760,12 @@ var PickIndex = class {
624
760
  if (length === 0) return;
625
761
  const along = ((x - ax) * dx + (y - ay) * dy) / length;
626
762
  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
- }
763
+ consider(index, Math.abs((x - ax) * dy - (y - ay) * dx) / length);
635
764
  });
765
+ for (const index of this.#loops) {
766
+ if (edges.alive[index] !== 1) continue;
767
+ consider(index, distanceToCubic(this.#loop(source, index, zoom), x, y, pickSegments));
768
+ }
636
769
  return best;
637
770
  }
638
771
  };
@@ -1091,6 +1224,7 @@ var EdgeColumns = class {
1091
1224
  dash = /* @__PURE__ */ new Uint8Array(0);
1092
1225
  arrow = /* @__PURE__ */ new Uint8Array(0);
1093
1226
  opacity = /* @__PURE__ */ new Float32Array(0);
1227
+ bend = /* @__PURE__ */ new Float32Array(0);
1094
1228
  label = [];
1095
1229
  reserve(index) {
1096
1230
  if (index < this.capacity) return;
@@ -1104,15 +1238,190 @@ var EdgeColumns = class {
1104
1238
  this.dash = grow$1(this.dash, capacity);
1105
1239
  this.arrow = grow$1(this.arrow, capacity);
1106
1240
  this.opacity = grow$1(this.opacity, capacity);
1241
+ this.bend = grow$1(this.bend, capacity);
1107
1242
  this.capacity = capacity;
1108
1243
  }
1109
1244
  clear(index) {
1110
1245
  if (index >= this.capacity) return;
1111
1246
  this.alive[index] = 0;
1247
+ this.bend[index] = 0;
1112
1248
  this.keys[index] = void 0;
1113
1249
  this.label[index] = null;
1114
1250
  }
1115
1251
  };
1252
+ // src/core/edge-lanes.ts
1253
+ const empty = -1;
1254
+ const removed = -2;
1255
+ function hash(low, high) {
1256
+ let h = Math.imul(low, 2654435761) ^ Math.imul(high + 2135587861, 2246822519);
1257
+ h ^= h >>> 15;
1258
+ return Math.imul(h, 739982445) ^ h >>> 12;
1259
+ }
1260
+ function grown(array, length) {
1261
+ const next = new Int32Array(length).fill(-1);
1262
+ next.set(array);
1263
+ return next;
1264
+ }
1265
+ function createEdgeLanes() {
1266
+ let mask = -1;
1267
+ let lows = /* @__PURE__ */ new Uint32Array(0);
1268
+ let highs = /* @__PURE__ */ new Uint32Array(0);
1269
+ let heads = /* @__PURE__ */ new Int32Array(0);
1270
+ let tails = /* @__PURE__ */ new Int32Array(0);
1271
+ let counts = /* @__PURE__ */ new Int32Array(0);
1272
+ let stamps = /* @__PURE__ */ new Uint32Array(0);
1273
+ let used = 0;
1274
+ let live = 0;
1275
+ let generation = 0;
1276
+ let slots = /* @__PURE__ */ new Int32Array(0);
1277
+ let nexts = /* @__PURE__ */ new Int32Array(0);
1278
+ let prevs = /* @__PURE__ */ new Int32Array(0);
1279
+ let touched = [];
1280
+ function reserve(index) {
1281
+ if (index < slots.length) return;
1282
+ const length = Math.max(64, slots.length * 2, index + 1);
1283
+ slots = grown(slots, length);
1284
+ nexts = grown(nexts, length);
1285
+ prevs = grown(prevs, length);
1286
+ }
1287
+ function locate(low, high) {
1288
+ let slot = hash(low, high) & mask;
1289
+ let free = -1;
1290
+ for (;;) {
1291
+ const head = heads[slot] ?? empty;
1292
+ if (head === empty) return -(free >= 0 ? free : slot) - 1;
1293
+ if (head === removed) {
1294
+ if (free < 0) free = slot;
1295
+ } else if (lows[slot] === low && highs[slot] === high) return slot;
1296
+ slot = slot + 1 & mask;
1297
+ }
1298
+ }
1299
+ function resize(minimum) {
1300
+ let capacity = 16;
1301
+ while (capacity < minimum) capacity *= 2;
1302
+ const oldLows = lows;
1303
+ const oldHighs = highs;
1304
+ const oldHeads = heads;
1305
+ const oldTails = tails;
1306
+ const oldCounts = counts;
1307
+ mask = capacity - 1;
1308
+ lows = new Uint32Array(capacity);
1309
+ highs = new Uint32Array(capacity);
1310
+ heads = new Int32Array(capacity).fill(empty);
1311
+ tails = new Int32Array(capacity).fill(-1);
1312
+ counts = new Int32Array(capacity);
1313
+ stamps = new Uint32Array(capacity);
1314
+ generation = 0;
1315
+ used = 0;
1316
+ live = 0;
1317
+ for (let old = 0; old < oldHeads.length; old++) {
1318
+ const first = oldHeads[old] ?? empty;
1319
+ if (first < 0) continue;
1320
+ const low = oldLows[old] ?? 0;
1321
+ const high = oldHighs[old] ?? 0;
1322
+ const slot = -locate(low, high) - 1;
1323
+ lows[slot] = low;
1324
+ highs[slot] = high;
1325
+ heads[slot] = first;
1326
+ tails[slot] = oldTails[old] ?? -1;
1327
+ counts[slot] = oldCounts[old] ?? 0;
1328
+ used++;
1329
+ live++;
1330
+ for (let at = first; at >= 0; at = nexts[at] ?? -1) slots[at] = slot;
1331
+ }
1332
+ }
1333
+ function unlink(index) {
1334
+ const slot = slots[index] ?? -1;
1335
+ if (slot < 0) return;
1336
+ const prev = prevs[index] ?? -1;
1337
+ const next = nexts[index] ?? -1;
1338
+ if (prev >= 0) nexts[prev] = next;
1339
+ else heads[slot] = next;
1340
+ if (next >= 0) prevs[next] = prev;
1341
+ else tails[slot] = prev;
1342
+ slots[index] = -1;
1343
+ const count = (counts[slot] ?? 1) - 1;
1344
+ counts[slot] = count;
1345
+ if (count === 0) {
1346
+ heads[slot] = removed;
1347
+ live--;
1348
+ } else touched.push(heads[slot] ?? -1);
1349
+ }
1350
+ function link(index, source, target) {
1351
+ reserve(index);
1352
+ const low = source < target ? source : target;
1353
+ const high = source < target ? target : source;
1354
+ const current = slots[index] ?? -1;
1355
+ if (current >= 0) {
1356
+ if (lows[current] === low && highs[current] === high) return;
1357
+ unlink(index);
1358
+ }
1359
+ if ((used + 1) * 2 > mask + 1) resize(Math.max(16, (live + 1) * 4));
1360
+ const found = locate(low, high);
1361
+ prevs[index] = -1;
1362
+ nexts[index] = -1;
1363
+ if (found >= 0) {
1364
+ const tail = tails[found] ?? -1;
1365
+ nexts[tail] = index;
1366
+ prevs[index] = tail;
1367
+ tails[found] = index;
1368
+ counts[found] = (counts[found] ?? 0) + 1;
1369
+ slots[index] = found;
1370
+ } else {
1371
+ const slot = -found - 1;
1372
+ if (heads[slot] === empty) used++;
1373
+ live++;
1374
+ lows[slot] = low;
1375
+ highs[slot] = high;
1376
+ heads[slot] = index;
1377
+ tails[slot] = index;
1378
+ counts[slot] = 1;
1379
+ slots[index] = slot;
1380
+ }
1381
+ touched.push(index);
1382
+ }
1383
+ function write(edges, dirty) {
1384
+ if (touched.length === 0) return;
1385
+ generation = generation + 1 >>> 0;
1386
+ if (generation === 0) {
1387
+ stamps.fill(0);
1388
+ generation = 1;
1389
+ }
1390
+ const bends = edges.bend;
1391
+ for (const edge of touched) {
1392
+ const slot = edge >= 0 ? slots[edge] ?? -1 : -1;
1393
+ if (slot < 0 || stamps[slot] === generation) continue;
1394
+ stamps[slot] = generation;
1395
+ const count = counts[slot] ?? 0;
1396
+ const loop = lows[slot] === highs[slot];
1397
+ if (count === 1 && !loop) {
1398
+ const only = heads[slot] ?? 0;
1399
+ if (bends[only] !== 0) {
1400
+ bends[only] = 0;
1401
+ dirty.add(only);
1402
+ }
1403
+ continue;
1404
+ }
1405
+ let position = 0;
1406
+ for (let at = heads[slot] ?? -1; at >= 0; at = nexts[at] ?? -1) {
1407
+ const source = edges.source[at] ?? 0;
1408
+ const target = edges.target[at] ?? 0;
1409
+ const bend = loop ? position : Math.fround(laneBend(position, count) * (source < target ? 1 : -1)) || 0;
1410
+ if (bends[at] !== bend) {
1411
+ bends[at] = bend;
1412
+ dirty.add(at);
1413
+ }
1414
+ position++;
1415
+ }
1416
+ }
1417
+ touched = [];
1418
+ }
1419
+ return {
1420
+ link,
1421
+ unlink,
1422
+ write
1423
+ };
1424
+ }
1116
1425
  // src/core/palette.ts
1117
1426
  var Palette = class {
1118
1427
  #ids = /* @__PURE__ */ new Map();
@@ -1186,6 +1495,7 @@ function createScene() {
1186
1495
  const nodes = new NodeColumns();
1187
1496
  const edges = new EdgeColumns();
1188
1497
  const palette = new Palette();
1498
+ const lanes = createEdgeLanes();
1189
1499
  const slots = /* @__PURE__ */ new Map();
1190
1500
  let layers = [];
1191
1501
  let layersDirty = false;
@@ -1273,6 +1583,7 @@ function createScene() {
1273
1583
  edges.slot[index] = slots.get(record.mark) ?? 0;
1274
1584
  edges.source[index] = model.node(record.source)?.index ?? 0;
1275
1585
  edges.target[index] = model.node(record.target)?.index ?? 0;
1586
+ lanes.link(index, edges.source[index] ?? 0, edges.target[index] ?? 0);
1276
1587
  edges.color[index] = palette.id(color);
1277
1588
  edges.width[index] = Number.isNaN(width) ? defaults.edgeWidth : widthScale.map(width, widthDomain);
1278
1589
  edges.dash[index] = codeOf(dashCodes, mark.dash?.(datum));
@@ -1290,6 +1601,7 @@ function createScene() {
1290
1601
  dirtyNodes.add(index);
1291
1602
  }
1292
1603
  function removeEdge(index) {
1604
+ lanes.unlink(index);
1293
1605
  edges.clear(index);
1294
1606
  widthValues[index] = NaN;
1295
1607
  edgeColorValues[index] = void 0;
@@ -1381,6 +1693,7 @@ function createScene() {
1381
1693
  }
1382
1694
  for (const record of nodeRecords.values()) writeNode(record);
1383
1695
  for (const record of edgeRecords.values()) writeEdge(model, record);
1696
+ lanes.write(edges, dirtyEdges);
1384
1697
  },
1385
1698
  takeDirty() {
1386
1699
  const dirty = {
@@ -2187,6 +2500,10 @@ void main() {
2187
2500
  gl_Position = hidden;
2188
2501
  return;
2189
2502
  }
2503
+ if (topology.x == topology.y || style.z != 0.0) {
2504
+ gl_Position = hidden;
2505
+ return;
2506
+ }
2190
2507
  Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z), 32);
2191
2508
  ivec2 s = texel(topology.x);
2192
2509
  ivec2 e = texel(topology.y);
@@ -2231,6 +2548,143 @@ void main() {
2231
2548
  gl_Position = visible > 0.5 ? toClip(ps + dir * along + normal * across) : hidden;
2232
2549
  }
2233
2550
  `;
2551
+ const segments = 24 .toFixed(1);
2552
+ const curveVertex = `${header}${nodeData}${edgeData}
2553
+ in vec3 corner;
2554
+ in uint instance;
2555
+ out vec2 vPos;
2556
+ out float vStart;
2557
+ out float vEnd;
2558
+ out float vHalfWidth;
2559
+ out float vArrowLength;
2560
+ out float vArrowHalf;
2561
+ out float vHaloWidth;
2562
+ flat out uint vDash;
2563
+ flat out uint vArrow;
2564
+ flat out vec4 vColor;
2565
+ flat out float vOpacity;
2566
+ vec2 cubicAt(vec2 a, vec2 b, vec2 c, vec2 d, float t) {
2567
+ float u = 1.0 - t;
2568
+ return u * u * u * a + 3.0 * u * u * t * b + 3.0 * u * t * t * c + t * t * t * d;
2569
+ }
2570
+ vec2 cubicTangent(vec2 a, vec2 b, vec2 c, vec2 d, float t) {
2571
+ float u = 1.0 - t;
2572
+ return 3.0 * u * u * (b - a) + 6.0 * u * t * (c - b) + 3.0 * t * t * (d - c);
2573
+ }
2574
+ float crossing(vec2 a, vec2 b, vec2 c, vec2 d, vec2 point, float radius, float lo, float hi, bool rising) {
2575
+ for (int i = 0; i < 14; i++) {
2576
+ float mid = (lo + hi) * 0.5;
2577
+ bool inside = distance(cubicAt(a, b, c, d, mid), point) < radius;
2578
+ if (inside == rising) lo = mid; else hi = mid;
2579
+ }
2580
+ return (lo + hi) * 0.5;
2581
+ }
2582
+ float arcLength(vec2 a, vec2 b, vec2 c, vec2 d, float from, float to) {
2583
+ float total = 0.0;
2584
+ vec2 previous = cubicAt(a, b, c, d, from);
2585
+ for (int i = 1; i <= 8; i++) {
2586
+ vec2 next = cubicAt(a, b, c, d, mix(from, to, float(i) / 8.0));
2587
+ total += distance(previous, next);
2588
+ previous = next;
2589
+ }
2590
+ return total;
2591
+ }
2592
+ vec2 turn(vec2 v, float angle) {
2593
+ float c = cos(angle);
2594
+ float s = sin(angle);
2595
+ return vec2(v.x * c - v.y * s, v.x * s + v.y * c);
2596
+ }
2597
+ void main() {
2598
+ ivec2 t = texel(instance);
2599
+ uvec4 topology = texelFetch(edgeTopology, t, 0);
2600
+ vec4 style = texelFetch(edgeStyle, t, 0);
2601
+ uint packed = topology.w;
2602
+ uint mask = texelFetch(edgeState, t, 0).r;
2603
+ bool highlighted = (mask & highlightMask) != 0u;
2604
+ bool loop = topology.x == topology.y;
2605
+ if ((packed & 16u) == 0u || (!loop && style.z == 0.0) || (overlay == 0 && (int(packed >> 5u) != layerSlot || highlighted))) {
2606
+ gl_Position = hidden;
2607
+ return;
2608
+ }
2609
+ Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z), 32);
2610
+ ivec2 s = texel(topology.x);
2611
+ ivec2 e = texel(topology.y);
2612
+ vec2 a = toScreen(texelFetch(nodePosition, s, 0).xy);
2613
+ vec2 d = toScreen(texelFetch(nodePosition, e, 0).xy);
2614
+ float sourceRadius = nodeRadius(texelFetch(nodeStyle, s, 0));
2615
+ float targetRadius = nodeRadius(texelFetch(nodeStyle, e, 0));
2616
+ vec2 b;
2617
+ vec2 c;
2618
+ if (loop) {
2619
+ float reach = sourceRadius + max(sourceRadius * 1.5, ${12 .toFixed(1)}) * (1.0 + ${loopGrowth.toFixed(2)} * style.z);
2620
+ float arm = reach / (0.75 * cos(${loopTurn.toFixed(4)}));
2621
+ vec2 u = vec2(0.70710678, -0.70710678);
2622
+ b = a + arm * turn(u, ${loopTurn.toFixed(4)});
2623
+ c = a + arm * turn(u, -${loopTurn.toFixed(4)});
2624
+ } else {
2625
+ vec2 delta = d - a;
2626
+ float len = length(delta);
2627
+ vec2 side = len > 0.0 ? vec2(-delta.y, delta.x) / len : vec2(0.0, 1.0);
2628
+ vec2 control = (a + d) * 0.5 + side * (2.0 * style.z * len);
2629
+ b = a + (control - a) * (2.0 / 3.0);
2630
+ c = d + (control - d) * (2.0 / 3.0);
2631
+ }
2632
+ float t0 = crossing(a, b, c, d, a, sourceRadius, 0.0, 0.5, true);
2633
+ float t1 = crossing(a, b, c, d, d, targetRadius, 0.5, 1.0, false);
2634
+ float visible = arcLength(a, b, c, d, t0, t1);
2635
+ float width = max(look.strokeWidth, 0.5);
2636
+ uint arrow = (packed >> 2u) & 3u;
2637
+ float arrowLength = arrow != 0u && visible > width * 8.0 ? max(width * 4.0, 6.0) : 0.0;
2638
+ float arrowHalf = max(width * 1.6, 3.0);
2639
+ float halo = look.halo > 0.5 ? haloWidth : 0.0;
2640
+ float margin = 0.5 / pixelRatio + 0.5;
2641
+ float startHead = (arrow & 1u) != 0u ? arrowLength : 0.0;
2642
+ float endHead = (arrow & 2u) != 0u ? arrowLength : 0.0;
2643
+ vec2 first = cubicAt(a, b, c, d, t0);
2644
+ vec2 last = cubicAt(a, b, c, d, t1);
2645
+ float ta = startHead > 0.0 ? crossing(a, b, c, d, first, startHead, t0, t1, true) : t0;
2646
+ float tb = endHead > 0.0 ? crossing(a, b, c, d, last, endHead, t0, t1, false) : t1;
2647
+ float body = width * 0.5 + halo + margin;
2648
+ float head = max(body, arrowHalf + halo + margin);
2649
+ int station = int(corner.x + 0.5);
2650
+ float at;
2651
+ float along;
2652
+ float push = 0.0;
2653
+ if (station == 0) {
2654
+ at = t0;
2655
+ along = -margin;
2656
+ push = -margin;
2657
+ } else if (station > int(${segments}) + 1) {
2658
+ at = t1;
2659
+ along = visible + margin;
2660
+ push = margin;
2661
+ } else {
2662
+ float f = float(station - 1) / ${segments};
2663
+ at = mix(ta, tb, f);
2664
+ along = mix(startHead, visible - endHead, f);
2665
+ }
2666
+ vec2 tangent = cubicTangent(a, b, c, d, at);
2667
+ float tangentLength = length(tangent);
2668
+ vec2 dir = tangentLength > 0.0 ? tangent / tangentLength : vec2(1.0, 0.0);
2669
+ vec2 normal = vec2(-dir.y, dir.x);
2670
+ float extent = corner.z < 0.5 ? body
2671
+ : corner.z < 1.5 ? (startHead > 0.0 ? head : body)
2672
+ : (endHead > 0.0 ? head : body);
2673
+ float across = corner.y * extent;
2674
+ vPos = vec2(along, across);
2675
+ vStart = 0.0;
2676
+ vEnd = visible;
2677
+ vHalfWidth = width * 0.5;
2678
+ vArrowLength = arrowLength;
2679
+ vArrowHalf = arrowHalf;
2680
+ vHaloWidth = halo;
2681
+ vDash = packed & 3u;
2682
+ vArrow = arrow;
2683
+ vColor = paletteColor(look.stroke);
2684
+ vOpacity = look.opacity;
2685
+ gl_Position = visible > 0.5 ? toClip(cubicAt(a, b, c, d, at) + dir * push + normal * across) : hidden;
2686
+ }
2687
+ `;
2234
2688
  const edgeFragment = `${header}
2235
2689
  in vec2 vPos;
2236
2690
  in float vStart;
@@ -2387,6 +2841,27 @@ const cornerEdge = new Float32Array([
2387
2841
  1,
2388
2842
  2
2389
2843
  ]);
2844
+ function curveCorners() {
2845
+ const values = [
2846
+ 0,
2847
+ -1,
2848
+ 1,
2849
+ 0,
2850
+ 1,
2851
+ 1,
2852
+ 1,
2853
+ -1,
2854
+ 1,
2855
+ 1,
2856
+ 1,
2857
+ 1
2858
+ ];
2859
+ for (let station = 1; station <= 25; station++) values.push(station, -1, 0, station, 1, 0);
2860
+ const end = 25;
2861
+ values.push(end, -1, 2, end, 1, 2, 26, -1, 2, 26, 1, 2);
2862
+ return new Float32Array(values);
2863
+ }
2864
+ const cornerCurve = curveCorners();
2390
2865
  const cornerLabel = new Float32Array([
2391
2866
  0,
2392
2867
  0,
@@ -2487,26 +2962,30 @@ function createWebglRenderer(canvas) {
2487
2962
  edgeProgram,
2488
2963
  labelProgram
2489
2964
  ];
2490
- for (const program of programs) {
2965
+ const locations = /* @__PURE__ */ new Map();
2966
+ function prepare(program) {
2491
2967
  gl.useProgram(program);
2492
2968
  for (const [name, unit] of Object.entries(units)) {
2493
2969
  const location = gl.getUniformLocation(program, name);
2494
2970
  if (location) gl.uniform1i(location, unit);
2495
2971
  }
2972
+ locations.set(program, {
2973
+ layerSlot: gl.getUniformLocation(program, "layerSlot"),
2974
+ overlay: gl.getUniformLocation(program, "overlay")
2975
+ });
2496
2976
  }
2977
+ for (const program of programs) prepare(program);
2497
2978
  gl.useProgram(labelProgram);
2498
2979
  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
2980
  const nodeCorner = createBuffer(gl, cornerNode, gl.STATIC_DRAW);
2504
2981
  const edgeCorner = createBuffer(gl, cornerEdge, gl.STATIC_DRAW);
2982
+ const curveCorner = createBuffer(gl, cornerCurve, gl.STATIC_DRAW);
2505
2983
  const labelCorner = createBuffer(gl, cornerLabel, gl.STATIC_DRAW);
2506
2984
  const nodeIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.STATIC_DRAW);
2507
2985
  const edgeIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.STATIC_DRAW);
2508
2986
  const nodeOverlayIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
2509
2987
  const edgeOverlayIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
2988
+ const curveIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
2510
2989
  const labelMainBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
2511
2990
  const labelHighlightBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
2512
2991
  const warmIndex = createBuffer(gl, /* @__PURE__ */ new Uint32Array(1), gl.STATIC_DRAW);
@@ -2514,11 +2993,13 @@ function createWebglRenderer(canvas) {
2514
2993
  const buffers = [
2515
2994
  nodeCorner,
2516
2995
  edgeCorner,
2996
+ curveCorner,
2517
2997
  labelCorner,
2518
2998
  nodeIndices,
2519
2999
  edgeIndices,
2520
3000
  nodeOverlayIndices,
2521
3001
  edgeOverlayIndices,
3002
+ curveIndices,
2522
3003
  labelMainBuffer,
2523
3004
  labelHighlightBuffer,
2524
3005
  warmIndex,
@@ -2595,10 +3076,26 @@ function createWebglRenderer(canvas) {
2595
3076
  [labelProgram, cornerLabel.length / 2]
2596
3077
  ]);
2597
3078
  let warmed = false;
3079
+ let curvePass = null;
3080
+ function curves() {
3081
+ if (curvePass) return curvePass;
3082
+ const program = createProgram(gl, curveVertex, edgeFragment, blocks);
3083
+ prepare(program);
3084
+ programs.push(program);
3085
+ vertexCounts.set(program, cornerCurve.length / 3);
3086
+ curvePass = {
3087
+ program,
3088
+ main: createVertexArray(gl, program, [corner(curveCorner, 3), instance(curveIndices)]),
3089
+ overlay: createVertexArray(gl, program, [corner(curveCorner, 3), instance(edgeOverlayIndices)])
3090
+ };
3091
+ return curvePass;
3092
+ }
2598
3093
  let nodeCapacity = 0;
2599
3094
  let edgeCapacity = 0;
2600
3095
  let nodeOverlayCount = 0;
2601
3096
  let edgeOverlayCount = 0;
3097
+ const curved = /* @__PURE__ */ new Set();
3098
+ let curvedChanged = false;
2602
3099
  let input = null;
2603
3100
  const atlas = new GlyphAtlas("system-ui, sans-serif");
2604
3101
  const labels = new LabelLayer();
@@ -2629,6 +3126,14 @@ function createWebglRenderer(canvas) {
2629
3126
  edgeTopology.data[at + 3] = (edges.dash[index] ?? 0) | (edges.arrow[index] ?? 0) << 2 | (edges.alive[index] ?? 0) << 4 | (edges.slot[index] ?? 0) << 5;
2630
3127
  edgeStyle.data[at] = edges.width[index] ?? 0;
2631
3128
  edgeStyle.data[at + 1] = edges.opacity[index] ?? 1;
3129
+ const bend = edges.bend[index] ?? 0;
3130
+ edgeStyle.data[at + 2] = bend;
3131
+ const isCurved = edges.alive[index] === 1 && (bend !== 0 || edges.source[index] === edges.target[index]);
3132
+ if (isCurved !== curved.has(index)) {
3133
+ if (isCurved) curved.add(index);
3134
+ else curved.delete(index);
3135
+ curvedChanged = true;
3136
+ }
2632
3137
  }
2633
3138
  function syncNodes(next) {
2634
3139
  const nodes = next.scene.nodes;
@@ -2688,6 +3193,11 @@ function createWebglRenderer(canvas) {
2688
3193
  }
2689
3194
  edgeTopology.upload(first, last);
2690
3195
  edgeStyle.upload(first, last);
3196
+ if (curvedChanged) {
3197
+ curvedChanged = false;
3198
+ gl.bindBuffer(gl.ARRAY_BUFFER, curveIndices);
3199
+ gl.bufferData(gl.ARRAY_BUFFER, Uint32Array.from(curved), gl.DYNAMIC_DRAW);
3200
+ }
2691
3201
  const states = next.states;
2692
3202
  const range = states.edgeDirty.take();
2693
3203
  if (next.full || grown) {
@@ -2849,10 +3359,16 @@ function createWebglRenderer(canvas) {
2849
3359
  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
2850
3360
  bind();
2851
3361
  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);
3362
+ for (const layer of input?.scene.layers ?? []) if (layer.kind === "edges") {
3363
+ draw(edgeProgram, vertexArrays.edges, edgeCapacity, layer.slot, 0);
3364
+ if (curved.size > 0) {
3365
+ const pass = curves();
3366
+ draw(pass.program, pass.main, curved.size, layer.slot, 0);
3367
+ }
3368
+ } else draw(nodeProgram, vertexArrays.nodes, nodeCapacity, layer.slot, 0);
2854
3369
  draw(labelProgram, vertexArrays.labels, labels.main.count, 0, 0);
2855
3370
  draw(edgeProgram, vertexArrays.edgeOverlay, edgeOverlayCount, 0, 1);
3371
+ if (curvePass && curved.size > 0) draw(curvePass.program, curvePass.overlay, edgeOverlayCount, 0, 1);
2856
3372
  draw(nodeProgram, vertexArrays.nodeOverlay, nodeOverlayCount, 0, 1);
2857
3373
  draw(labelProgram, vertexArrays.labelHighlight, labels.highlight.count, 0, 1);
2858
3374
  gl.bindVertexArray(null);
@@ -2868,6 +3384,10 @@ function createWebglRenderer(canvas) {
2868
3384
  statesBuffer
2869
3385
  ]) gl.deleteBuffer(buffer);
2870
3386
  for (const vao of Object.values(vertexArrays)) gl.deleteVertexArray(vao);
3387
+ if (curvePass) {
3388
+ gl.deleteVertexArray(curvePass.main);
3389
+ gl.deleteVertexArray(curvePass.overlay);
3390
+ }
2871
3391
  for (const program of programs) gl.deleteProgram(program);
2872
3392
  gl.getExtension("WEBGL_lose_context")?.loseContext();
2873
3393
  }
@@ -3820,4 +4340,4 @@ function mountGraph(container, options) {
3820
4340
  }
3821
4341
  export { freeStateNames as n, maxFreeStates as r, mountGraph as t };
3822
4342
 
3823
- //# sourceMappingURL=mount-graph-BHwq3LOr.js.map
4343
+ //# sourceMappingURL=mount-graph-DYOgD_ql.js.map