@bruno00o/graph 0.3.0 → 0.4.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.
@@ -835,10 +835,13 @@ function compileNodeMark(mark) {
835
835
  y: channel(options.y),
836
836
  label: channel(typeof label === "object" ? label.text : label),
837
837
  secondary: channel(typeof label === "object" ? label.secondary : void 0),
838
+ labelBackground: style(typeof label === "object" ? label.background : void 0),
838
839
  shape: style(options.shape),
839
840
  fill: style(options.fill),
840
841
  stroke: style(options.stroke),
841
842
  strokeWidth: style(options.strokeWidth),
843
+ outline: style(options.outline),
844
+ outlineWidth: style(options.outlineWidth),
842
845
  opacity: style(options.opacity),
843
846
  image: style(options.image),
844
847
  badges,
@@ -1001,6 +1004,7 @@ const lightTheme = {
1001
1004
  "focus-ring": "#c93700",
1002
1005
  halo: "#c9370047",
1003
1006
  "label-halo": "#f7f7f4",
1007
+ "label-background": "#ffffff",
1004
1008
  "node-ring": "#f7f7f4",
1005
1009
  "badge-fill": "#191d22",
1006
1010
  "badge-ink": "#f7f7f4",
@@ -1025,6 +1029,9 @@ const defaults = {
1025
1029
  nodeFill: themeColor("node"),
1026
1030
  nodeStroke: themeColor("node-ring"),
1027
1031
  nodeStrokeWidth: 1,
1032
+ nodeOutline: themeColor("node-ring"),
1033
+ nodeOutlineWidth: 2,
1034
+ labelBackground: themeColor("label-background"),
1028
1035
  edgeWidth: 1.5,
1029
1036
  edgeWidthRange: [1, 6],
1030
1037
  edgeColor: themeColor("edge"),
@@ -1212,6 +1219,11 @@ function grow$1(array, length) {
1212
1219
  next.set(array);
1213
1220
  return next;
1214
1221
  }
1222
+ function growColor(array, length) {
1223
+ const next = new Uint32Array(length).fill(noColor);
1224
+ next.set(array);
1225
+ return next;
1226
+ }
1215
1227
  function nextCapacity(capacity, index) {
1216
1228
  return Math.max(64, capacity * 2, index + 1);
1217
1229
  }
@@ -1225,10 +1237,13 @@ var NodeColumns = class {
1225
1237
  fill = /* @__PURE__ */ new Uint32Array(0);
1226
1238
  stroke = /* @__PURE__ */ new Uint32Array(0);
1227
1239
  strokeWidth = /* @__PURE__ */ new Float32Array(0);
1240
+ outline = /* @__PURE__ */ new Uint32Array(0);
1241
+ outlineWidth = /* @__PURE__ */ new Float32Array(0);
1228
1242
  opacity = /* @__PURE__ */ new Float32Array(0);
1229
1243
  screenUnits = /* @__PURE__ */ new Uint8Array(0);
1230
1244
  label = [];
1231
1245
  secondary = [];
1246
+ labelBackground = /* @__PURE__ */ new Uint32Array(0);
1232
1247
  image = [];
1233
1248
  imageScale = /* @__PURE__ */ new Float32Array(0);
1234
1249
  imageColor = /* @__PURE__ */ new Uint32Array(0);
@@ -1243,10 +1258,13 @@ var NodeColumns = class {
1243
1258
  this.fill = grow$1(this.fill, capacity);
1244
1259
  this.stroke = grow$1(this.stroke, capacity);
1245
1260
  this.strokeWidth = grow$1(this.strokeWidth, capacity);
1261
+ this.outline = grow$1(this.outline, capacity);
1262
+ this.outlineWidth = grow$1(this.outlineWidth, capacity);
1246
1263
  this.opacity = grow$1(this.opacity, capacity);
1247
1264
  this.screenUnits = grow$1(this.screenUnits, capacity);
1265
+ this.labelBackground = growColor(this.labelBackground, capacity);
1248
1266
  this.imageScale = grow$1(this.imageScale, capacity);
1249
- this.imageColor = grow$1(this.imageColor, capacity);
1267
+ this.imageColor = growColor(this.imageColor, capacity);
1250
1268
  this.capacity = capacity;
1251
1269
  }
1252
1270
  clear(index) {
@@ -1532,6 +1550,11 @@ function readBadges(mark, datum, palette) {
1532
1550
  }
1533
1551
  return badges.length === 0 ? null : badges;
1534
1552
  }
1553
+ function readBackground(value, palette) {
1554
+ if (value === true) return palette.id(defaults.labelBackground);
1555
+ const color = toColor(value);
1556
+ return color ? palette.id(color) : noColor;
1557
+ }
1535
1558
  function readImage(value) {
1536
1559
  if (typeof value === "string") return value === "" ? null : {
1537
1560
  src: value,
@@ -1631,10 +1654,14 @@ function createScene() {
1631
1654
  nodes.fill[index] = palette.id(fill);
1632
1655
  nodes.stroke[index] = palette.id(toColor(mark.stroke?.(datum)) ?? defaults.nodeStroke);
1633
1656
  nodes.strokeWidth[index] = finiteOr(mark.strokeWidth?.(datum), defaults.nodeStrokeWidth);
1657
+ const outline = mark.outline ? toColor(mark.outline(datum)) : null;
1658
+ nodes.outline[index] = palette.id(outline ?? defaults.nodeOutline);
1659
+ nodes.outlineWidth[index] = mark.outline && !outline ? 0 : Math.max(0, finiteOr(mark.outlineWidth?.(datum), outline ? defaults.nodeOutlineWidth : 0));
1634
1660
  nodes.opacity[index] = Math.min(1, Math.max(0, finiteOr(mark.opacity?.(datum), 1)));
1635
1661
  nodes.screenUnits[index] = mark.screenUnits ? 1 : 0;
1636
1662
  nodes.label[index] = toText(mark.label?.(datum));
1637
1663
  nodes.secondary[index] = toText(mark.secondary?.(datum));
1664
+ nodes.labelBackground[index] = readBackground(mark.labelBackground?.(datum), palette);
1638
1665
  const image = readImage(mark.image?.(datum));
1639
1666
  nodes.image[index] = image?.src ?? null;
1640
1667
  nodes.imageScale[index] = image?.scale ?? 1;
@@ -1840,6 +1867,12 @@ var DirtyRange = class {
1840
1867
  return range;
1841
1868
  }
1842
1869
  };
1870
+ const bits = /* @__PURE__ */ new Float32Array(1);
1871
+ const word = new Uint32Array(bits.buffer);
1872
+ function floatWord(value) {
1873
+ bits[0] = value;
1874
+ return word[0] ?? 0;
1875
+ }
1843
1876
  function grow(array, capacity) {
1844
1877
  if (capacity <= array.length) return array;
1845
1878
  const next = new Uint32Array(Math.max(capacity, array.length * 2));
@@ -1882,6 +1915,8 @@ var StateBuffers = class {
1882
1915
  this.numbers[at + 3] = style?.label === void 0 ? -1 : style.label ? 1 : 0;
1883
1916
  this.paint[at] = style?.fill ?? 4294967295;
1884
1917
  this.paint[at + 1] = style?.stroke ?? 4294967295;
1918
+ this.paint[at + 2] = style?.outline ?? 4294967295;
1919
+ this.paint[at + 3] = style?.outlineWidth === void 0 ? noPaint : floatWord(Math.max(0, style.outlineWidth));
1885
1920
  if (kind === "nodes") this.labelFlags[bit] = this.numbers[at + 3] ?? -1;
1886
1921
  this.tableVersion++;
1887
1922
  }
@@ -2450,149 +2485,12 @@ var ImageAtlas = class {
2450
2485
  return true;
2451
2486
  }
2452
2487
  };
2453
- const primarySize = 12;
2454
- const lineHeight = 1.25;
2455
- const labelMin = 8;
2456
- const cell = 8;
2457
- const maxLabels = 4e3;
2458
- const cullMargin = 400;
2459
- const forcedStates = 6;
2460
- var GlyphBuffer = class {
2461
- data = /* @__PURE__ */ new ArrayBuffer(8192);
2462
- count = 0;
2463
- #u32 = new Uint32Array(this.data);
2464
- #f32 = new Float32Array(this.data);
2465
- #u16 = new Uint16Array(this.data);
2466
- clear() {
2467
- this.count = 0;
2468
- }
2469
- push(node, kind, line, atlasHeight) {
2470
- for (const { glyph, x } of line.glyphs) {
2471
- if ((this.count + 1) * 32 > this.data.byteLength) this.#grow();
2472
- const at = this.count * 8;
2473
- this.#u32[at] = node;
2474
- this.#u32[at + 1] = kind;
2475
- this.#f32[at + 2] = x;
2476
- this.#f32[at + 3] = glyph.top;
2477
- this.#f32[at + 4] = glyph.width;
2478
- this.#f32[at + 5] = glyph.height;
2479
- const uv = at * 2 + 12;
2480
- this.#u16[uv] = Math.round(glyph.x / atlasWidth * 65535);
2481
- this.#u16[uv + 1] = Math.round(glyph.y / atlasHeight * 65535);
2482
- this.#u16[uv + 2] = Math.round((glyph.x + glyph.width) / atlasWidth * 65535);
2483
- this.#u16[uv + 3] = Math.round((glyph.y + glyph.height) / atlasHeight * 65535);
2484
- this.count++;
2485
- }
2486
- }
2487
- #grow() {
2488
- const data = /* @__PURE__ */ new ArrayBuffer(this.data.byteLength * 2);
2489
- new Uint8Array(data).set(new Uint8Array(this.data));
2490
- this.data = data;
2491
- this.#u32 = new Uint32Array(data);
2492
- this.#f32 = new Float32Array(data);
2493
- this.#u16 = new Uint16Array(data);
2494
- }
2495
- };
2496
- function labelVisible(mask, flags) {
2497
- let bits = mask >>> 0;
2498
- while (bits !== 0) {
2499
- const bit = 31 - Math.clz32(bits & -bits);
2500
- bits &= bits - 1;
2501
- const flag = flags[bit] ?? -1;
2502
- if (flag >= 0) return flag === 1;
2503
- }
2504
- return true;
2505
- }
2506
- var LabelLayer = class {
2507
- main = new GlyphBuffer();
2508
- highlight = new GlyphBuffer();
2509
- #occupancy = /* @__PURE__ */ new Uint8Array(0);
2510
- select(frame, atlas) {
2511
- this.main.clear();
2512
- this.highlight.clear();
2513
- const { nodes, positions, view, width, height } = frame;
2514
- const cols = Math.ceil(width / cell) + 1;
2515
- const rows = Math.ceil(height / cell) + 1;
2516
- if (this.#occupancy.length < cols * rows) this.#occupancy = new Uint8Array(cols * rows);
2517
- else this.#occupancy.fill(0, 0, cols * rows);
2518
- const occupancy = this.#occupancy;
2519
- const highlighted = new Set(frame.highlighted);
2520
- const worldPrimary = primarySize * Math.min(1, view.zoom);
2521
- let placed = 0;
2522
- const forced = (index) => ((frame.nodeMask[index] ?? 0) & forcedStates) !== 0;
2523
- const place = (index, highlight) => {
2524
- const force = highlight && forced(index);
2525
- const label = nodes.label[index];
2526
- if (!label || nodes.alive[index] !== 1) return;
2527
- if (!labelVisible(frame.nodeMask[index] ?? 0, frame.labelFlags)) return;
2528
- const screenUnits = nodes.screenUnits[index] === 1;
2529
- const primary = screenUnits ? primarySize : worldPrimary;
2530
- if (primary < labelMin) return;
2531
- const diameter = screenUnits ? nodes.size[index] ?? 0 : (nodes.size[index] ?? 0) * view.zoom;
2532
- const radius = Math.max(diameter / 2, 1.25);
2533
- const x = (positions.x(index) - view.x) * view.zoom + width / 2;
2534
- const y = (positions.y(index) - view.y) * view.zoom + height / 2;
2535
- if (x < -400 || x > width + cullMargin) return;
2536
- if (y + radius > height || y + radius + primary * lineHeight * 2 < -3) return;
2537
- const line = atlas.line(label, "500");
2538
- const secondaryText = nodes.secondary[index];
2539
- const secondary = secondaryText ? atlas.line(secondaryText, "400") : null;
2540
- const scale = primary / 24;
2541
- const textWidth = Math.max(line.width * scale, secondary ? secondary.width * scale * 11 / 12 : 0);
2542
- const top = y + radius + 3;
2543
- const bottom = top + primary * lineHeight * (secondary ? 2 : 1);
2544
- const left = x - textWidth / 2;
2545
- const right = x + textWidth / 2;
2546
- if (right < 0 || left > width || bottom < 0 || top > height) return;
2547
- const c0 = Math.max(0, Math.floor(left / cell));
2548
- const c1 = Math.min(cols - 1, Math.floor(right / cell));
2549
- const r0 = Math.max(0, Math.floor(top / cell));
2550
- const r1 = Math.min(rows - 1, Math.floor(bottom / cell));
2551
- if (!force) {
2552
- for (let row = r0; row <= r1; row++) for (let col = c0; col <= c1; col++) if (occupancy[row * cols + col] === 1) return;
2553
- }
2554
- for (let row = r0; row <= r1; row++) occupancy.fill(1, row * cols + c0, row * cols + c1 + 1);
2555
- const target = highlight ? this.highlight : this.main;
2556
- target.push(index, 0, line, atlas.height);
2557
- if (secondary) target.push(index, 1, secondary, atlas.height);
2558
- placed++;
2559
- };
2560
- for (const index of highlighted) if (forced(index)) place(index, true);
2561
- for (const index of highlighted) if (!forced(index)) place(index, true);
2562
- if (worldPrimary < labelMin && !frame.screenLabels) return;
2563
- const xy = positions.xy;
2564
- const order = frame.order;
2565
- const left = view.x - (width / 2 + cullMargin) / view.zoom;
2566
- const right = view.x + (width / 2 + cullMargin) / view.zoom;
2567
- const largest = nodes.size[order[0] ?? 0] ?? 0;
2568
- const reach = cullMargin + (frame.screenLabels ? largest : largest * view.zoom) / 2;
2569
- const top = view.y - (height / 2 + reach) / view.zoom;
2570
- const bottom = view.y + height / 2 / view.zoom;
2571
- for (let at = 0; at < frame.orderLength && placed < maxLabels; at++) {
2572
- const index = order[at] ?? 0;
2573
- const x = xy[index * 2] ?? 0;
2574
- const y = xy[index * 2 + 1] ?? 0;
2575
- if (x < left || x > right || y < top || y > bottom) continue;
2576
- if (!highlighted.has(index)) place(index, false);
2577
- }
2578
- }
2579
- };
2580
- function sortBySize(nodes, order) {
2581
- let length = 0;
2582
- let screenLabels = false;
2583
- for (let index = 0; index < nodes.capacity; index++) if (nodes.alive[index] === 1 && nodes.label[index]) {
2584
- order[length++] = index;
2585
- if (nodes.screenUnits[index] === 1) screenLabels = true;
2586
- }
2587
- const sizes = nodes.size;
2588
- order.subarray(0, length).sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
2589
- return {
2590
- length,
2591
- screenLabels
2592
- };
2593
- }
2594
2488
  const badgeFont = .66;
2595
2489
  const badgeRing = 1.5;
2490
+ const platePadX = .45;
2491
+ const plateHalfOne = .75;
2492
+ const plateHalfTwo = 1.32;
2493
+ const plateRadius = .25;
2596
2494
  const header = `#version 300 es
2597
2495
  precision highp float;
2598
2496
  precision highp int;
@@ -2627,7 +2525,7 @@ vec4 toClip(vec2 screen) {
2627
2525
  }
2628
2526
  ivec2 texel(uint index) { return ivec2(int(index & 1023u), int(index >> 10u)); }
2629
2527
  vec4 paletteColor(uint id) { return texelFetch(palette, ivec2(int(id & 255u), int(id >> 8u)), 0); }
2630
- struct Look { float opacity; float strokeWidth; float halo; float label; uint fill; uint stroke; };
2528
+ struct Look { float opacity; float strokeWidth; float halo; float label; uint fill; uint stroke; uint outline; float outlineWidth; };
2631
2529
  Look resolve(uint mask, Look look, int table) {
2632
2530
  bool opacity = false;
2633
2531
  bool strokeWidth = false;
@@ -2635,6 +2533,8 @@ Look resolve(uint mask, Look look, int table) {
2635
2533
  bool label = false;
2636
2534
  bool fill = false;
2637
2535
  bool stroke = false;
2536
+ bool outline = false;
2537
+ bool outlineWidth = false;
2638
2538
  float base = look.opacity;
2639
2539
  for (int bit = 0; bit < 32; bit++) {
2640
2540
  if (mask == 0u) break;
@@ -2649,6 +2549,8 @@ Look resolve(uint mask, Look look, int table) {
2649
2549
  if (!label && numbers.w >= 0.0) { look.label = numbers.w; label = true; }
2650
2550
  if (!fill && paint.x != none) { look.fill = paint.x; fill = true; }
2651
2551
  if (!stroke && paint.y != none) { look.stroke = paint.y; stroke = true; }
2552
+ if (!outline && paint.z != none) { look.outline = paint.z; outline = true; }
2553
+ if (!outlineWidth && paint.w != none) { look.outlineWidth = uintBitsToFloat(paint.w); outlineWidth = true; }
2652
2554
  }
2653
2555
  return look;
2654
2556
  }
@@ -2658,6 +2560,11 @@ uniform sampler2D nodePosition;
2658
2560
  uniform sampler2D nodeStyle;
2659
2561
  uniform usampler2D nodePaint;
2660
2562
  uniform usampler2D nodeState;
2563
+ uniform usampler2D nodeDecor;
2564
+ Look nodeLook(ivec2 t, vec4 style, uvec4 paint, uint mask) {
2565
+ float outlineWidth = uintBitsToFloat(texelFetch(nodeDecor, t, 0).w);
2566
+ return resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y, paint.w, outlineWidth), 0);
2567
+ }
2661
2568
  float nodeDiameter(vec4 style) {
2662
2569
  uint flags = uint(style.w + 0.5);
2663
2570
  return (flags & 4u) != 0u ? style.x : style.x * zoom;
@@ -2677,7 +2584,6 @@ uniform sampler2D edgeStyle;
2677
2584
  uniform usampler2D edgeState;
2678
2585
  `;
2679
2586
  const nodeVertex = `${header}${nodeData}
2680
- uniform usampler2D nodeImage;
2681
2587
  in vec2 corner;
2682
2588
  in uint instance;
2683
2589
  out vec2 vLocal;
@@ -2687,9 +2593,11 @@ flat out vec4 vImageColor;
2687
2593
  out float vRadius;
2688
2594
  out float vStrokeWidth;
2689
2595
  out float vHaloWidth;
2596
+ out float vOutlineWidth;
2690
2597
  flat out uint vShape;
2691
2598
  flat out vec4 vFill;
2692
2599
  flat out vec4 vStroke;
2600
+ flat out vec4 vOutline;
2693
2601
  flat out float vOpacity;
2694
2602
  void main() {
2695
2603
  ivec2 t = texel(instance);
@@ -2702,19 +2610,22 @@ void main() {
2702
2610
  gl_Position = hidden;
2703
2611
  return;
2704
2612
  }
2705
- Look look = resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y), 0);
2613
+ Look look = nodeLook(t, style, paint, mask);
2706
2614
  vec2 xy = texelFetch(nodePosition, t, 0).xy;
2707
2615
  float radius = nodeRadius(style);
2708
2616
  float halo = look.halo > 0.5 ? haloWidth : 0.0;
2709
- vLocal = corner * (radius + halo + 1.5);
2617
+ float outline = max(look.outlineWidth, 0.0);
2618
+ vLocal = corner * (radius + outline + halo + 1.5);
2710
2619
  vRadius = radius;
2711
2620
  vStrokeWidth = min(look.strokeWidth, radius * 0.5);
2712
2621
  vHaloWidth = halo;
2622
+ vOutlineWidth = outline;
2623
+ vOutline = paletteColor(look.outline);
2713
2624
  vShape = flags & 3u;
2714
2625
  vFill = paletteColor(look.fill);
2715
2626
  vStroke = paletteColor(look.stroke);
2716
2627
  vOpacity = look.opacity;
2717
- uvec4 image = texelFetch(nodeImage, t, 0);
2628
+ uvec4 image = texelFetch(nodeDecor, t, 0);
2718
2629
  vImageLayer = float(image.x) - 1.0;
2719
2630
  vImageScale = max(uintBitsToFloat(image.z), 0.001);
2720
2631
  vImageColor = image.y == none ? vec4(-1.0) : paletteColor(image.y);
@@ -2730,9 +2641,11 @@ flat in vec4 vImageColor;
2730
2641
  in float vRadius;
2731
2642
  in float vStrokeWidth;
2732
2643
  in float vHaloWidth;
2644
+ in float vOutlineWidth;
2733
2645
  flat in uint vShape;
2734
2646
  flat in vec4 vFill;
2735
2647
  flat in vec4 vStroke;
2648
+ flat in vec4 vOutline;
2736
2649
  flat in float vOpacity;
2737
2650
  out vec4 fragColor;
2738
2651
  float shapeDistance(vec2 p, float r) {
@@ -2759,8 +2672,12 @@ void main() {
2759
2672
  }
2760
2673
  vec4 color = mix(fill, vec4(vStroke.rgb * vStroke.a, vStroke.a), ring);
2761
2674
  vec4 result = color * inside * vOpacity;
2675
+ if (vOutlineWidth > 0.0) {
2676
+ float outer = 1.0 - smoothstep(vOutlineWidth - aa, vOutlineWidth + aa, d);
2677
+ result = mix(vec4(vOutline.rgb * vOutline.a, vOutline.a), color, inside) * outer * vOpacity;
2678
+ }
2762
2679
  if (vHaloWidth > 0.0) {
2763
- float h = haloColor.a * (1.0 - smoothstep(0.0, vHaloWidth, d)) * vOpacity;
2680
+ float h = haloColor.a * (1.0 - smoothstep(0.0, vHaloWidth, d - vOutlineWidth)) * vOpacity;
2764
2681
  result += vec4(haloColor.rgb * h, h) * (1.0 - result.a);
2765
2682
  }
2766
2683
  if (result.a < 0.002) discard;
@@ -2796,7 +2713,7 @@ void main() {
2796
2713
  gl_Position = hidden;
2797
2714
  return;
2798
2715
  }
2799
- Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z), 32);
2716
+ Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z, none, 0.0), 32);
2800
2717
  ivec2 s = texel(topology.x);
2801
2718
  ivec2 e = texel(topology.y);
2802
2719
  vec2 ps = toScreen(texelFetch(nodePosition, s, 0).xy);
@@ -2898,7 +2815,7 @@ void main() {
2898
2815
  gl_Position = hidden;
2899
2816
  return;
2900
2817
  }
2901
- Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z), 32);
2818
+ Look look = resolve(mask, Look(style.y, style.x, 0.0, 1.0, none, topology.z, none, 0.0), 32);
2902
2819
  ivec2 s = texel(topology.x);
2903
2820
  ivec2 e = texel(topology.y);
2904
2821
  vec2 a = toScreen(texelFetch(nodePosition, s, 0).xy);
@@ -3043,14 +2960,16 @@ out vec2 vUv;
3043
2960
  out float vScale;
3044
2961
  flat out vec4 vInk;
3045
2962
  flat out float vHalo;
2963
+ flat out vec4 vPlate;
3046
2964
  flat out float vOpacity;
3047
2965
  void main() {
2966
+ vPlate = vec4(0.0);
3048
2967
  ivec2 t = texel(glyphNode);
3049
2968
  vec4 style = texelFetch(nodeStyle, t, 0);
3050
2969
  uvec4 paint = texelFetch(nodePaint, t, 0);
3051
2970
  uint flags = uint(style.w + 0.5);
3052
2971
  uint mask = texelFetch(nodeState, t, 0).r;
3053
- Look look = resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y), 0);
2972
+ Look look = nodeLook(t, style, paint, mask);
3054
2973
  vUv = mix(glyphUv.xy, glyphUv.zw, corner);
3055
2974
  vOpacity = look.opacity;
3056
2975
  uint kind = glyphKind & 3u;
@@ -3074,13 +2993,28 @@ void main() {
3074
2993
  gl_Position = hidden;
3075
2994
  return;
3076
2995
  }
2996
+ float below = nodeRadius(style) + max(look.outlineWidth, 0.0) + 3.0;
2997
+ vec2 top = toScreen(texelFetch(nodePosition, t, 0).xy) + vec2(0.0, below);
2998
+ if (kind == 3u) {
2999
+ bool two = glyphRect.y > 1.5;
3000
+ vec2 extent = vec2(
3001
+ glyphRect.x * primary / atlasFontSize * 0.5 + primary * ${platePadX.toFixed(2)},
3002
+ primary * (two ? ${plateHalfTwo.toFixed(2)} : ${plateHalfOne.toFixed(2)})
3003
+ );
3004
+ vUv = (corner * 2.0 - 1.0) * (extent + 1.0);
3005
+ vPlate = vec4(extent, primary * ${plateRadius.toFixed(2)}, 1.0);
3006
+ vScale = 1.0;
3007
+ vInk = paletteColor(glyphKind >> 4u);
3008
+ vHalo = 0.0;
3009
+ gl_Position = toClip(top + vec2(0.0, primary * (two ? ${plateHalfTwo.toFixed(2)} : ${plateHalfOne.toFixed(2)})) + vUv);
3010
+ return;
3011
+ }
3077
3012
  float size = kind == 0u ? primary : primary * 11.0 / 12.0;
3078
3013
  float scale = size / atlasFontSize;
3079
- float below = nodeRadius(style) + 3.0 + (kind == 1u ? primary * 1.25 : 0.0);
3080
- vec2 anchor = toScreen(texelFetch(nodePosition, t, 0).xy) + vec2(0.0, below);
3014
+ vec2 anchor = top + vec2(0.0, kind == 1u ? primary * 1.25 : 0.0);
3081
3015
  vScale = scale;
3082
3016
  vInk = kind == 0u ? labelInk : labelMuted;
3083
- vHalo = 1.0;
3017
+ vHalo = (glyphKind & 4u) != 0u ? 0.0 : 1.0;
3084
3018
  gl_Position = toClip(anchor + (glyphRect.xy + corner * glyphRect.zw) * scale);
3085
3019
  }
3086
3020
  `;
@@ -3090,9 +3024,19 @@ in vec2 vUv;
3090
3024
  in float vScale;
3091
3025
  flat in vec4 vInk;
3092
3026
  flat in float vHalo;
3027
+ flat in vec4 vPlate;
3093
3028
  flat in float vOpacity;
3094
3029
  out vec4 fragColor;
3095
3030
  void main() {
3031
+ if (vPlate.w > 0.5) {
3032
+ vec2 q = abs(vUv) - vPlate.xy + vPlate.z;
3033
+ float edge = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - vPlate.z;
3034
+ float aa = 0.5 / pixelRatio + 0.25;
3035
+ float cover = vInk.a * (1.0 - smoothstep(-aa, aa, edge)) * vOpacity;
3036
+ if (cover < 0.002) discard;
3037
+ fragColor = vec4(vInk.rgb * cover, cover);
3038
+ return;
3039
+ }
3096
3040
  float d = texture(atlas, vUv).r;
3097
3041
  float gamma = 0.08 / max(vScale * pixelRatio, 0.05);
3098
3042
  float ink = smoothstep(0.75 - gamma, 0.75 + gamma, d);
@@ -3127,7 +3071,7 @@ void main() {
3127
3071
  gl_Position = hidden;
3128
3072
  return;
3129
3073
  }
3130
- Look look = resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y), 0);
3074
+ Look look = nodeLook(t, style, paint, mask);
3131
3075
  bool text = (badgeInfo & 4u) != 0u && height >= ${12 .toFixed(1)};
3132
3076
  float font = height * ${badgeFont.toFixed(2)};
3133
3077
  float width = text ? max(height, badgeText * font / ${24 .toFixed(1)} + height * 0.6) : height;
@@ -3239,6 +3183,166 @@ const cornerLabel = new Float32Array([
3239
3183
  1,
3240
3184
  1
3241
3185
  ]);
3186
+ const primarySize = 12;
3187
+ const lineHeight = 1.25;
3188
+ const labelMin = 8;
3189
+ const cell = 8;
3190
+ const maxLabels = 4e3;
3191
+ const cullMargin = 400;
3192
+ const forcedStates = 6;
3193
+ var GlyphBuffer = class {
3194
+ data = /* @__PURE__ */ new ArrayBuffer(8192);
3195
+ count = 0;
3196
+ #u32 = new Uint32Array(this.data);
3197
+ #f32 = new Float32Array(this.data);
3198
+ #u16 = new Uint16Array(this.data);
3199
+ clear() {
3200
+ this.count = 0;
3201
+ }
3202
+ push(node, kind, line, atlasHeight) {
3203
+ for (const { glyph, x } of line.glyphs) {
3204
+ if ((this.count + 1) * 32 > this.data.byteLength) this.#grow();
3205
+ const at = this.count * 8;
3206
+ this.#u32[at] = node;
3207
+ this.#u32[at + 1] = kind;
3208
+ this.#f32[at + 2] = x;
3209
+ this.#f32[at + 3] = glyph.top;
3210
+ this.#f32[at + 4] = glyph.width;
3211
+ this.#f32[at + 5] = glyph.height;
3212
+ const uv = at * 2 + 12;
3213
+ this.#u16[uv] = Math.round(glyph.x / atlasWidth * 65535);
3214
+ this.#u16[uv + 1] = Math.round(glyph.y / atlasHeight * 65535);
3215
+ this.#u16[uv + 2] = Math.round((glyph.x + glyph.width) / atlasWidth * 65535);
3216
+ this.#u16[uv + 3] = Math.round((glyph.y + glyph.height) / atlasHeight * 65535);
3217
+ this.count++;
3218
+ }
3219
+ }
3220
+ pushPlate(node, color, width, lines) {
3221
+ if ((this.count + 1) * 32 > this.data.byteLength) this.#grow();
3222
+ const at = this.count * 8;
3223
+ this.#u32[at] = node;
3224
+ this.#u32[at + 1] = 3 | color << 4;
3225
+ this.#f32[at + 2] = width;
3226
+ this.#f32[at + 3] = lines;
3227
+ this.#f32[at + 4] = 0;
3228
+ this.#f32[at + 5] = 0;
3229
+ this.#u32[at + 6] = 0;
3230
+ this.#u32[at + 7] = 0;
3231
+ this.count++;
3232
+ }
3233
+ #grow() {
3234
+ const data = /* @__PURE__ */ new ArrayBuffer(this.data.byteLength * 2);
3235
+ new Uint8Array(data).set(new Uint8Array(this.data));
3236
+ this.data = data;
3237
+ this.#u32 = new Uint32Array(data);
3238
+ this.#f32 = new Float32Array(data);
3239
+ this.#u16 = new Uint16Array(data);
3240
+ }
3241
+ };
3242
+ function labelVisible(mask, flags) {
3243
+ let bits = mask >>> 0;
3244
+ while (bits !== 0) {
3245
+ const bit = 31 - Math.clz32(bits & -bits);
3246
+ bits &= bits - 1;
3247
+ const flag = flags[bit] ?? -1;
3248
+ if (flag >= 0) return flag === 1;
3249
+ }
3250
+ return true;
3251
+ }
3252
+ var LabelLayer = class {
3253
+ main = new GlyphBuffer();
3254
+ highlight = new GlyphBuffer();
3255
+ #occupancy = /* @__PURE__ */ new Uint8Array(0);
3256
+ select(frame, atlas) {
3257
+ this.main.clear();
3258
+ this.highlight.clear();
3259
+ const { nodes, positions, view, width, height } = frame;
3260
+ const cols = Math.ceil(width / cell) + 1;
3261
+ const rows = Math.ceil(height / cell) + 1;
3262
+ if (this.#occupancy.length < cols * rows) this.#occupancy = new Uint8Array(cols * rows);
3263
+ else this.#occupancy.fill(0, 0, cols * rows);
3264
+ const occupancy = this.#occupancy;
3265
+ const highlighted = new Set(frame.highlighted);
3266
+ const worldPrimary = primarySize * Math.min(1, view.zoom);
3267
+ let placed = 0;
3268
+ const forced = (index) => ((frame.nodeMask[index] ?? 0) & forcedStates) !== 0;
3269
+ const place = (index, highlight) => {
3270
+ const force = highlight && forced(index);
3271
+ const label = nodes.label[index];
3272
+ if (!label || nodes.alive[index] !== 1) return;
3273
+ if (!labelVisible(frame.nodeMask[index] ?? 0, frame.labelFlags)) return;
3274
+ const screenUnits = nodes.screenUnits[index] === 1;
3275
+ const primary = screenUnits ? primarySize : worldPrimary;
3276
+ if (primary < labelMin) return;
3277
+ const diameter = screenUnits ? nodes.size[index] ?? 0 : (nodes.size[index] ?? 0) * view.zoom;
3278
+ const radius = Math.max(diameter / 2, 1.25) + (nodes.outlineWidth[index] ?? 0);
3279
+ const x = (positions.x(index) - view.x) * view.zoom + width / 2;
3280
+ const y = (positions.y(index) - view.y) * view.zoom + height / 2;
3281
+ if (x < -400 || x > width + cullMargin) return;
3282
+ if (y + radius > height || y + radius + primary * lineHeight * 3 < -3) return;
3283
+ const line = atlas.line(label, "500");
3284
+ const secondaryText = nodes.secondary[index];
3285
+ const secondary = secondaryText ? atlas.line(secondaryText, "400") : null;
3286
+ const scale = primary / 24;
3287
+ const textUnits = Math.max(line.width, secondary ? secondary.width * 11 / 12 : 0);
3288
+ const background = nodes.labelBackground[index] ?? 4294967295;
3289
+ const plated = background !== noColor;
3290
+ const pad = plated ? primary * platePadX : 0;
3291
+ const lines = secondary ? 2 : 1;
3292
+ const top = y + radius + 3;
3293
+ const bottom = top + (plated ? primary * 2 * (secondary ? plateHalfTwo : plateHalfOne) : primary * lineHeight * lines);
3294
+ const left = x - textUnits * scale / 2 - pad;
3295
+ const right = x + textUnits * scale / 2 + pad;
3296
+ if (right < 0 || left > width || bottom < 0 || top > height) return;
3297
+ const c0 = Math.max(0, Math.floor(left / cell));
3298
+ const c1 = Math.min(cols - 1, Math.floor(right / cell));
3299
+ const r0 = Math.max(0, Math.floor(top / cell));
3300
+ const r1 = Math.min(rows - 1, Math.floor(bottom / cell));
3301
+ if (!force) {
3302
+ for (let row = r0; row <= r1; row++) for (let col = c0; col <= c1; col++) if (occupancy[row * cols + col] === 1) return;
3303
+ }
3304
+ for (let row = r0; row <= r1; row++) occupancy.fill(1, row * cols + c0, row * cols + c1 + 1);
3305
+ const target = highlight ? this.highlight : this.main;
3306
+ const plate = plated ? 4 : 0;
3307
+ if (plated) target.pushPlate(index, background, textUnits, lines);
3308
+ target.push(index, plate, line, atlas.height);
3309
+ if (secondary) target.push(index, 1 | plate, secondary, atlas.height);
3310
+ placed++;
3311
+ };
3312
+ for (const index of highlighted) if (forced(index)) place(index, true);
3313
+ for (const index of highlighted) if (!forced(index)) place(index, true);
3314
+ if (worldPrimary < labelMin && !frame.screenLabels) return;
3315
+ const xy = positions.xy;
3316
+ const order = frame.order;
3317
+ const left = view.x - (width / 2 + cullMargin) / view.zoom;
3318
+ const right = view.x + (width / 2 + cullMargin) / view.zoom;
3319
+ const largest = nodes.size[order[0] ?? 0] ?? 0;
3320
+ const reach = cullMargin + (frame.screenLabels ? largest : largest * view.zoom) / 2;
3321
+ const top = view.y - (height / 2 + reach) / view.zoom;
3322
+ const bottom = view.y + height / 2 / view.zoom;
3323
+ for (let at = 0; at < frame.orderLength && placed < maxLabels; at++) {
3324
+ const index = order[at] ?? 0;
3325
+ const x = xy[index * 2] ?? 0;
3326
+ const y = xy[index * 2 + 1] ?? 0;
3327
+ if (x < left || x > right || y < top || y > bottom) continue;
3328
+ if (!highlighted.has(index)) place(index, false);
3329
+ }
3330
+ }
3331
+ };
3332
+ function sortBySize(nodes, order) {
3333
+ let length = 0;
3334
+ let screenLabels = false;
3335
+ for (let index = 0; index < nodes.capacity; index++) if (nodes.alive[index] === 1 && nodes.label[index]) {
3336
+ order[length++] = index;
3337
+ if (nodes.screenUnits[index] === 1) screenLabels = true;
3338
+ }
3339
+ const sizes = nodes.size;
3340
+ order.subarray(0, length).sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
3341
+ return {
3342
+ length,
3343
+ screenLabels
3344
+ };
3345
+ }
3242
3346
  // src/webgl/renderer.ts
3243
3347
  const blocks = {
3244
3348
  Frame: 0,
@@ -3258,7 +3362,7 @@ const units = {
3258
3362
  edgeState: 7,
3259
3363
  atlas: 8,
3260
3364
  images: 9,
3261
- nodeImage: 10
3365
+ nodeDecor: 10
3262
3366
  };
3263
3367
  function iota(length) {
3264
3368
  const values = new Uint32Array(length);
@@ -3285,7 +3389,7 @@ function createWebglRenderer(canvas, invalidate = () => {}) {
3285
3389
  const nodeStyle = new DataTexture(gl, format.rgba32f, f32);
3286
3390
  const nodePaint = new DataTexture(gl, format.rgba32ui, u32);
3287
3391
  const nodeState = new DataTexture(gl, format.r32ui, u32);
3288
- const nodeImage = new DataTexture(gl, format.rgba32ui, u32);
3392
+ const nodeDecor = new DataTexture(gl, format.rgba32ui, u32);
3289
3393
  const edgeTopology = new DataTexture(gl, format.rgba32ui, u32);
3290
3394
  const edgeStyle = new DataTexture(gl, format.rgba32f, f32);
3291
3395
  const edgeState = new DataTexture(gl, format.r32ui, u32);
@@ -3294,11 +3398,11 @@ function createWebglRenderer(canvas, invalidate = () => {}) {
3294
3398
  nodeStyle,
3295
3399
  nodePaint,
3296
3400
  nodeState,
3297
- nodeImage
3401
+ nodeDecor
3298
3402
  ];
3299
3403
  const images = new ImageAtlas(gl, invalidate);
3300
- const scaleBits = /* @__PURE__ */ new Float32Array(1);
3301
- const scaleWord = new Uint32Array(scaleBits.buffer);
3404
+ const floatBits = /* @__PURE__ */ new Float32Array(1);
3405
+ const floatWord = new Uint32Array(floatBits.buffer);
3302
3406
  const edgeTextures = [
3303
3407
  edgeTopology,
3304
3408
  edgeStyle,
@@ -3528,6 +3632,7 @@ function createWebglRenderer(canvas, invalidate = () => {}) {
3528
3632
  nodePaint.data[at] = nodes.fill[index] ?? 0;
3529
3633
  nodePaint.data[at + 1] = nodes.stroke[index] ?? 0;
3530
3634
  nodePaint.data[at + 2] = nodes.slot[index] ?? 0;
3635
+ nodePaint.data[at + 3] = nodes.outline[index] ?? 4294967295;
3531
3636
  const src = nodes.alive[index] === 1 ? nodes.image[index] ?? null : null;
3532
3637
  const previous = imageOf[index] ?? null;
3533
3638
  if (src !== previous) {
@@ -3535,10 +3640,12 @@ function createWebglRenderer(canvas, invalidate = () => {}) {
3535
3640
  if (src) images.acquire(src);
3536
3641
  imageOf[index] = src;
3537
3642
  }
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;
3643
+ floatBits[0] = nodes.imageScale[index] ?? 1;
3644
+ nodeDecor.data[at] = src ? images.layer(src) + 1 : 0;
3645
+ nodeDecor.data[at + 1] = nodes.imageColor[index] ?? 4294967295;
3646
+ nodeDecor.data[at + 2] = floatWord[0] ?? 0;
3647
+ floatBits[0] = nodes.outlineWidth[index] ?? 0;
3648
+ nodeDecor.data[at + 3] = floatWord[0] ?? 0;
3542
3649
  const badges = nodes.alive[index] === 1 ? nodes.badges[index] : null;
3543
3650
  if (badges || badged.has(index)) badgesStale = true;
3544
3651
  if (badges) badged.add(index);
@@ -3552,11 +3659,11 @@ function createWebglRenderer(canvas, invalidate = () => {}) {
3552
3659
  for (let index = 0; index < nodes.capacity; index++) {
3553
3660
  const src = imageOf[index];
3554
3661
  if (!src || !loaded.has(src)) continue;
3555
- nodeImage.data[index * 4] = images.layer(src) + 1;
3662
+ nodeDecor.data[index * 4] = images.layer(src) + 1;
3556
3663
  first = Math.min(first, index);
3557
3664
  last = Math.max(last, index);
3558
3665
  }
3559
- nodeImage.upload(first, last);
3666
+ nodeDecor.upload(first, last);
3560
3667
  }
3561
3668
  function rebuildBadges() {
3562
3669
  if (!input) return;
@@ -3634,7 +3741,7 @@ function createWebglRenderer(canvas, invalidate = () => {}) {
3634
3741
  }
3635
3742
  nodeStyle.upload(first, last);
3636
3743
  nodePaint.upload(first, last);
3637
- nodeImage.upload(first, last);
3744
+ nodeDecor.upload(first, last);
3638
3745
  if (next.positionsChanged || next.full || grown) {
3639
3746
  const xy = next.positions.xy;
3640
3747
  nodePosition.data.set(xy.subarray(0, Math.min(xy.length, nodePosition.data.length)));
@@ -3896,7 +4003,7 @@ function createWebglRenderer(canvas, invalidate = () => {}) {
3896
4003
  [units.edgeStyle, edgeStyle.texture],
3897
4004
  [units.edgeState, edgeState.texture],
3898
4005
  [units.atlas, atlasTexture],
3899
- [units.nodeImage, nodeImage.texture]
4006
+ [units.nodeDecor, nodeDecor.texture]
3900
4007
  ];
3901
4008
  for (const [unit, texture] of textures) {
3902
4009
  gl.activeTexture(gl.TEXTURE0 + unit);
@@ -4194,7 +4301,9 @@ function resolveStyle(style, id) {
4194
4301
  ...style.halo !== void 0 ? { halo: style.halo } : {},
4195
4302
  ...style.label !== void 0 ? { label: style.label } : {},
4196
4303
  ...style.fill !== void 0 ? { fill: id(style.fill) } : {},
4197
- ...style.stroke !== void 0 ? { stroke: id(style.stroke) } : {}
4304
+ ...style.stroke !== void 0 ? { stroke: id(style.stroke) } : {},
4305
+ ...style.outline !== void 0 ? { outline: id(style.outline) } : {},
4306
+ ...style.outlineWidth !== void 0 || style.outline !== void 0 ? { outlineWidth: style.outlineWidth ?? defaults.nodeOutlineWidth } : {}
4198
4307
  };
4199
4308
  }
4200
4309
  function createGraphHost(container, options, createRenderer) {
@@ -5142,4 +5251,4 @@ function mountGraph(container, options) {
5142
5251
  }
5143
5252
  export { freeStateNames as n, maxFreeStates as r, mountGraph as t };
5144
5253
 
5145
- //# sourceMappingURL=mount-graph-CzVfiIte.js.map
5254
+ //# sourceMappingURL=mount-graph-mObhwJ3e.js.map