@bruno00o/graph 0.2.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.
- package/README.md +3 -2
- package/dist/db.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{mount-graph-B88WGOQu.d.ts → mount-graph-Br5IAZbP.d.ts} +13 -3
- package/dist/{mount-graph-DYOgD_ql.js → mount-graph-mObhwJ3e.js} +1097 -186
- package/dist/mount-graph-mObhwJ3e.js.map +1 -0
- package/dist/query.d.ts +1 -1
- package/dist/react.d.ts +3 -2
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/dist/theme.css +3 -0
- package/dist/{types-D6b86Rt1.d.ts → types-DJEdpHUo.d.ts} +16 -4
- package/package.json +1 -1
- package/dist/mount-graph-DYOgD_ql.js.map +0 -1
|
@@ -449,6 +449,39 @@ 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
|
+
}
|
|
452
485
|
// src/core/edge-curve.ts
|
|
453
486
|
const loopTurn = .6;
|
|
454
487
|
const loopGrowth = .5;
|
|
@@ -802,10 +835,13 @@ function compileNodeMark(mark) {
|
|
|
802
835
|
y: channel(options.y),
|
|
803
836
|
label: channel(typeof label === "object" ? label.text : label),
|
|
804
837
|
secondary: channel(typeof label === "object" ? label.secondary : void 0),
|
|
838
|
+
labelBackground: style(typeof label === "object" ? label.background : void 0),
|
|
805
839
|
shape: style(options.shape),
|
|
806
840
|
fill: style(options.fill),
|
|
807
841
|
stroke: style(options.stroke),
|
|
808
842
|
strokeWidth: style(options.strokeWidth),
|
|
843
|
+
outline: style(options.outline),
|
|
844
|
+
outlineWidth: style(options.outlineWidth),
|
|
809
845
|
opacity: style(options.opacity),
|
|
810
846
|
image: style(options.image),
|
|
811
847
|
badges,
|
|
@@ -968,6 +1004,7 @@ const lightTheme = {
|
|
|
968
1004
|
"focus-ring": "#c93700",
|
|
969
1005
|
halo: "#c9370047",
|
|
970
1006
|
"label-halo": "#f7f7f4",
|
|
1007
|
+
"label-background": "#ffffff",
|
|
971
1008
|
"node-ring": "#f7f7f4",
|
|
972
1009
|
"badge-fill": "#191d22",
|
|
973
1010
|
"badge-ink": "#f7f7f4",
|
|
@@ -992,9 +1029,15 @@ const defaults = {
|
|
|
992
1029
|
nodeFill: themeColor("node"),
|
|
993
1030
|
nodeStroke: themeColor("node-ring"),
|
|
994
1031
|
nodeStrokeWidth: 1,
|
|
1032
|
+
nodeOutline: themeColor("node-ring"),
|
|
1033
|
+
nodeOutlineWidth: 2,
|
|
1034
|
+
labelBackground: themeColor("label-background"),
|
|
995
1035
|
edgeWidth: 1.5,
|
|
996
1036
|
edgeWidthRange: [1, 6],
|
|
997
|
-
edgeColor: themeColor("edge")
|
|
1037
|
+
edgeColor: themeColor("edge"),
|
|
1038
|
+
badgeFill: themeColor("badge-fill"),
|
|
1039
|
+
badgeInk: themeColor("badge-ink"),
|
|
1040
|
+
badgeRing: themeColor("node-ring")
|
|
998
1041
|
};
|
|
999
1042
|
const builtInStyles = {
|
|
1000
1043
|
nodes: {
|
|
@@ -1148,6 +1191,13 @@ function resolveNumberScale(options, fallback) {
|
|
|
1148
1191
|
};
|
|
1149
1192
|
}
|
|
1150
1193
|
// src/core/columns.ts
|
|
1194
|
+
const noColor = 4294967295;
|
|
1195
|
+
const cornerCodes = {
|
|
1196
|
+
topLeft: 0,
|
|
1197
|
+
topRight: 1,
|
|
1198
|
+
bottomLeft: 2,
|
|
1199
|
+
bottomRight: 3
|
|
1200
|
+
};
|
|
1151
1201
|
const shapeCodes = {
|
|
1152
1202
|
circle: 0,
|
|
1153
1203
|
square: 1,
|
|
@@ -1169,6 +1219,11 @@ function grow$1(array, length) {
|
|
|
1169
1219
|
next.set(array);
|
|
1170
1220
|
return next;
|
|
1171
1221
|
}
|
|
1222
|
+
function growColor(array, length) {
|
|
1223
|
+
const next = new Uint32Array(length).fill(noColor);
|
|
1224
|
+
next.set(array);
|
|
1225
|
+
return next;
|
|
1226
|
+
}
|
|
1172
1227
|
function nextCapacity(capacity, index) {
|
|
1173
1228
|
return Math.max(64, capacity * 2, index + 1);
|
|
1174
1229
|
}
|
|
@@ -1182,11 +1237,16 @@ var NodeColumns = class {
|
|
|
1182
1237
|
fill = /* @__PURE__ */ new Uint32Array(0);
|
|
1183
1238
|
stroke = /* @__PURE__ */ new Uint32Array(0);
|
|
1184
1239
|
strokeWidth = /* @__PURE__ */ new Float32Array(0);
|
|
1240
|
+
outline = /* @__PURE__ */ new Uint32Array(0);
|
|
1241
|
+
outlineWidth = /* @__PURE__ */ new Float32Array(0);
|
|
1185
1242
|
opacity = /* @__PURE__ */ new Float32Array(0);
|
|
1186
1243
|
screenUnits = /* @__PURE__ */ new Uint8Array(0);
|
|
1187
1244
|
label = [];
|
|
1188
1245
|
secondary = [];
|
|
1246
|
+
labelBackground = /* @__PURE__ */ new Uint32Array(0);
|
|
1189
1247
|
image = [];
|
|
1248
|
+
imageScale = /* @__PURE__ */ new Float32Array(0);
|
|
1249
|
+
imageColor = /* @__PURE__ */ new Uint32Array(0);
|
|
1190
1250
|
badges = [];
|
|
1191
1251
|
reserve(index) {
|
|
1192
1252
|
if (index < this.capacity) return;
|
|
@@ -1198,8 +1258,13 @@ var NodeColumns = class {
|
|
|
1198
1258
|
this.fill = grow$1(this.fill, capacity);
|
|
1199
1259
|
this.stroke = grow$1(this.stroke, capacity);
|
|
1200
1260
|
this.strokeWidth = grow$1(this.strokeWidth, capacity);
|
|
1261
|
+
this.outline = grow$1(this.outline, capacity);
|
|
1262
|
+
this.outlineWidth = grow$1(this.outlineWidth, capacity);
|
|
1201
1263
|
this.opacity = grow$1(this.opacity, capacity);
|
|
1202
1264
|
this.screenUnits = grow$1(this.screenUnits, capacity);
|
|
1265
|
+
this.labelBackground = growColor(this.labelBackground, capacity);
|
|
1266
|
+
this.imageScale = grow$1(this.imageScale, capacity);
|
|
1267
|
+
this.imageColor = growColor(this.imageColor, capacity);
|
|
1203
1268
|
this.capacity = capacity;
|
|
1204
1269
|
}
|
|
1205
1270
|
clear(index) {
|
|
@@ -1349,8 +1414,8 @@ function createEdgeLanes() {
|
|
|
1349
1414
|
}
|
|
1350
1415
|
function link(index, source, target) {
|
|
1351
1416
|
reserve(index);
|
|
1352
|
-
const low = source
|
|
1353
|
-
const high = source
|
|
1417
|
+
const low = Math.min(source, target);
|
|
1418
|
+
const high = Math.max(source, target);
|
|
1354
1419
|
const current = slots[index] ?? -1;
|
|
1355
1420
|
if (current >= 0) {
|
|
1356
1421
|
if (lows[current] === low && highs[current] === high) return;
|
|
@@ -1468,18 +1533,42 @@ function finiteOr(value, fallback) {
|
|
|
1468
1533
|
function codeOf(codes, value) {
|
|
1469
1534
|
return typeof value === "string" && Object.hasOwn(codes, value) ? codes[value] : 0;
|
|
1470
1535
|
}
|
|
1471
|
-
function readBadges(mark, datum) {
|
|
1536
|
+
function readBadges(mark, datum, palette) {
|
|
1472
1537
|
if (mark.badges.length === 0) return null;
|
|
1473
|
-
const badges =
|
|
1474
|
-
let count = 0;
|
|
1538
|
+
const badges = [];
|
|
1475
1539
|
for (const [corner, read] of mark.badges) {
|
|
1476
1540
|
const badge = read(datum);
|
|
1477
|
-
if (typeof badge
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1541
|
+
if (typeof badge !== "object" || badge === null) continue;
|
|
1542
|
+
const { text, fill, color } = badge;
|
|
1543
|
+
badges.push({
|
|
1544
|
+
corner: cornerCodes[corner],
|
|
1545
|
+
text: typeof text === "number" ? String(text) : toText(text) || null,
|
|
1546
|
+
fill: palette.id(toColor(fill) ?? defaults.badgeFill),
|
|
1547
|
+
color: palette.id(toColor(color) ?? defaults.badgeInk),
|
|
1548
|
+
ring: palette.id(defaults.badgeRing)
|
|
1549
|
+
});
|
|
1481
1550
|
}
|
|
1482
|
-
return
|
|
1551
|
+
return badges.length === 0 ? null : badges;
|
|
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
|
+
}
|
|
1558
|
+
function readImage(value) {
|
|
1559
|
+
if (typeof value === "string") return value === "" ? null : {
|
|
1560
|
+
src: value,
|
|
1561
|
+
scale: 1,
|
|
1562
|
+
color: null
|
|
1563
|
+
};
|
|
1564
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1565
|
+
const { src, scale, color } = value;
|
|
1566
|
+
if (typeof src !== "string" || src === "") return null;
|
|
1567
|
+
return {
|
|
1568
|
+
src,
|
|
1569
|
+
scale: Math.max(0, finiteOr(scale, 1)),
|
|
1570
|
+
color: toColor(color)
|
|
1571
|
+
};
|
|
1483
1572
|
}
|
|
1484
1573
|
function layersOf(marks) {
|
|
1485
1574
|
const slots = {
|
|
@@ -1565,12 +1654,19 @@ function createScene() {
|
|
|
1565
1654
|
nodes.fill[index] = palette.id(fill);
|
|
1566
1655
|
nodes.stroke[index] = palette.id(toColor(mark.stroke?.(datum)) ?? defaults.nodeStroke);
|
|
1567
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));
|
|
1568
1660
|
nodes.opacity[index] = Math.min(1, Math.max(0, finiteOr(mark.opacity?.(datum), 1)));
|
|
1569
1661
|
nodes.screenUnits[index] = mark.screenUnits ? 1 : 0;
|
|
1570
1662
|
nodes.label[index] = toText(mark.label?.(datum));
|
|
1571
1663
|
nodes.secondary[index] = toText(mark.secondary?.(datum));
|
|
1572
|
-
nodes.
|
|
1573
|
-
|
|
1664
|
+
nodes.labelBackground[index] = readBackground(mark.labelBackground?.(datum), palette);
|
|
1665
|
+
const image = readImage(mark.image?.(datum));
|
|
1666
|
+
nodes.image[index] = image?.src ?? null;
|
|
1667
|
+
nodes.imageScale[index] = image?.scale ?? 1;
|
|
1668
|
+
nodes.imageColor[index] = image?.color ? palette.id(image.color) : noColor;
|
|
1669
|
+
nodes.badges[index] = readBadges(mark, datum, palette);
|
|
1574
1670
|
dirtyNodes.add(index);
|
|
1575
1671
|
}
|
|
1576
1672
|
function writeEdge(model, record) {
|
|
@@ -1710,6 +1806,47 @@ function createScene() {
|
|
|
1710
1806
|
}
|
|
1711
1807
|
};
|
|
1712
1808
|
}
|
|
1809
|
+
// src/core/spatial.ts
|
|
1810
|
+
const sideWeight = 2;
|
|
1811
|
+
const cone = 2;
|
|
1812
|
+
function nearestNode(point, nodes, positions) {
|
|
1813
|
+
let best = -1;
|
|
1814
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1815
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
1816
|
+
if (nodes.alive[index] !== 1) continue;
|
|
1817
|
+
const distance = Math.hypot(positions.x(index) - point.x, positions.y(index) - point.y);
|
|
1818
|
+
if (distance < bestDistance) {
|
|
1819
|
+
best = index;
|
|
1820
|
+
bestDistance = distance;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return best;
|
|
1824
|
+
}
|
|
1825
|
+
function nodeToward(from, direction, nodes, positions) {
|
|
1826
|
+
const x = positions.x(from);
|
|
1827
|
+
const y = positions.y(from);
|
|
1828
|
+
const length = Math.hypot(direction.x, direction.y);
|
|
1829
|
+
if (length === 0) return -1;
|
|
1830
|
+
const dx = direction.x / length;
|
|
1831
|
+
const dy = direction.y / length;
|
|
1832
|
+
let best = -1;
|
|
1833
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1834
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
1835
|
+
if (index === from || nodes.alive[index] !== 1) continue;
|
|
1836
|
+
const vx = positions.x(index) - x;
|
|
1837
|
+
const vy = positions.y(index) - y;
|
|
1838
|
+
const along = vx * dx + vy * dy;
|
|
1839
|
+
if (along <= 0) continue;
|
|
1840
|
+
const side = Math.abs(vx * dy - vy * dx);
|
|
1841
|
+
if (side > along * cone) continue;
|
|
1842
|
+
const score = along + side * sideWeight;
|
|
1843
|
+
if (score < bestScore) {
|
|
1844
|
+
best = index;
|
|
1845
|
+
bestScore = score;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return best;
|
|
1849
|
+
}
|
|
1713
1850
|
const noPaint = 4294967295;
|
|
1714
1851
|
const tableOffset = {
|
|
1715
1852
|
nodes: 0,
|
|
@@ -1730,6 +1867,12 @@ var DirtyRange = class {
|
|
|
1730
1867
|
return range;
|
|
1731
1868
|
}
|
|
1732
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
|
+
}
|
|
1733
1876
|
function grow(array, capacity) {
|
|
1734
1877
|
if (capacity <= array.length) return array;
|
|
1735
1878
|
const next = new Uint32Array(Math.max(capacity, array.length * 2));
|
|
@@ -1772,6 +1915,8 @@ var StateBuffers = class {
|
|
|
1772
1915
|
this.numbers[at + 3] = style?.label === void 0 ? -1 : style.label ? 1 : 0;
|
|
1773
1916
|
this.paint[at] = style?.fill ?? 4294967295;
|
|
1774
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));
|
|
1775
1920
|
if (kind === "nodes") this.labelFlags[bit] = this.numbers[at + 3] ?? -1;
|
|
1776
1921
|
this.tableVersion++;
|
|
1777
1922
|
}
|
|
@@ -2191,149 +2336,167 @@ var GlyphAtlas = class {
|
|
|
2191
2336
|
this.generation++;
|
|
2192
2337
|
}
|
|
2193
2338
|
};
|
|
2194
|
-
const
|
|
2195
|
-
const
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
#
|
|
2204
|
-
#
|
|
2205
|
-
#
|
|
2206
|
-
|
|
2207
|
-
|
|
2339
|
+
const levels = Math.log2(256) + 1;
|
|
2340
|
+
const maxLayers = 256;
|
|
2341
|
+
var ImageAtlas = class {
|
|
2342
|
+
#gl;
|
|
2343
|
+
#invalidate;
|
|
2344
|
+
#entries = /* @__PURE__ */ new Map();
|
|
2345
|
+
#free = [];
|
|
2346
|
+
#placeholder;
|
|
2347
|
+
#texture = null;
|
|
2348
|
+
#capacity = 0;
|
|
2349
|
+
#next = 0;
|
|
2350
|
+
#loaded = /* @__PURE__ */ new Set();
|
|
2351
|
+
#mipsStale = false;
|
|
2352
|
+
#canvas = null;
|
|
2353
|
+
#warnedFull = false;
|
|
2354
|
+
#destroyed = false;
|
|
2355
|
+
constructor(gl, invalidate) {
|
|
2356
|
+
this.#gl = gl;
|
|
2357
|
+
this.#invalidate = invalidate;
|
|
2358
|
+
this.#placeholder = gl.createTexture();
|
|
2359
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#placeholder);
|
|
2360
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, 1, 1, 1);
|
|
2208
2361
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
if ((this.count + 1) * 32 > this.data.byteLength) this.#grow();
|
|
2212
|
-
const at = this.count * 8;
|
|
2213
|
-
this.#u32[at] = node;
|
|
2214
|
-
this.#u32[at + 1] = kind;
|
|
2215
|
-
this.#f32[at + 2] = x;
|
|
2216
|
-
this.#f32[at + 3] = glyph.top;
|
|
2217
|
-
this.#f32[at + 4] = glyph.width;
|
|
2218
|
-
this.#f32[at + 5] = glyph.height;
|
|
2219
|
-
const uv = at * 2 + 12;
|
|
2220
|
-
this.#u16[uv] = Math.round(glyph.x / atlasWidth * 65535);
|
|
2221
|
-
this.#u16[uv + 1] = Math.round(glyph.y / atlasHeight * 65535);
|
|
2222
|
-
this.#u16[uv + 2] = Math.round((glyph.x + glyph.width) / atlasWidth * 65535);
|
|
2223
|
-
this.#u16[uv + 3] = Math.round((glyph.y + glyph.height) / atlasHeight * 65535);
|
|
2224
|
-
this.count++;
|
|
2225
|
-
}
|
|
2362
|
+
get texture() {
|
|
2363
|
+
return this.#texture ?? this.#placeholder;
|
|
2226
2364
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
new Uint8Array(data).set(new Uint8Array(this.data));
|
|
2230
|
-
this.data = data;
|
|
2231
|
-
this.#u32 = new Uint32Array(data);
|
|
2232
|
-
this.#f32 = new Float32Array(data);
|
|
2233
|
-
this.#u16 = new Uint16Array(data);
|
|
2365
|
+
layer(src) {
|
|
2366
|
+
return this.#entries.get(src)?.layer ?? -1;
|
|
2234
2367
|
}
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2368
|
+
acquire(src) {
|
|
2369
|
+
const entry = this.#entries.get(src);
|
|
2370
|
+
if (entry) {
|
|
2371
|
+
entry.count++;
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
this.#entries.set(src, {
|
|
2375
|
+
count: 1,
|
|
2376
|
+
layer: -1
|
|
2377
|
+
});
|
|
2378
|
+
const image = new Image();
|
|
2379
|
+
image.crossOrigin = "anonymous";
|
|
2380
|
+
image.decoding = "async";
|
|
2381
|
+
image.src = src;
|
|
2382
|
+
image.decode().then(() => this.#store(src, image), () => this.#fail(src));
|
|
2383
|
+
}
|
|
2384
|
+
release(src) {
|
|
2385
|
+
const entry = this.#entries.get(src);
|
|
2386
|
+
if (!entry || --entry.count > 0) return;
|
|
2387
|
+
this.#entries.delete(src);
|
|
2388
|
+
if (entry.layer >= 0) this.#free.push(entry.layer);
|
|
2389
|
+
}
|
|
2390
|
+
takeLoaded() {
|
|
2391
|
+
if (this.#loaded.size === 0) return null;
|
|
2392
|
+
const loaded = this.#loaded;
|
|
2393
|
+
this.#loaded = /* @__PURE__ */ new Set();
|
|
2394
|
+
return loaded;
|
|
2395
|
+
}
|
|
2396
|
+
prepare() {
|
|
2397
|
+
if (!this.#mipsStale || !this.#texture) return;
|
|
2398
|
+
this.#mipsStale = false;
|
|
2399
|
+
const gl = this.#gl;
|
|
2400
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#texture);
|
|
2401
|
+
gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
|
|
2243
2402
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
#
|
|
2250
|
-
|
|
2251
|
-
this.
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
const
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
let placed = 0;
|
|
2262
|
-
const place = (index, force) => {
|
|
2263
|
-
const label = nodes.label[index];
|
|
2264
|
-
if (!label || nodes.alive[index] !== 1) return;
|
|
2265
|
-
if (!labelVisible(frame.nodeMask[index] ?? 0, frame.labelFlags)) return;
|
|
2266
|
-
const screenUnits = nodes.screenUnits[index] === 1;
|
|
2267
|
-
const primary = screenUnits ? primarySize : worldPrimary;
|
|
2268
|
-
if (primary < labelMin) return;
|
|
2269
|
-
const diameter = screenUnits ? nodes.size[index] ?? 0 : (nodes.size[index] ?? 0) * view.zoom;
|
|
2270
|
-
const radius = Math.max(diameter / 2, 1.25);
|
|
2271
|
-
const x = (positions.x(index) - view.x) * view.zoom + width / 2;
|
|
2272
|
-
const y = (positions.y(index) - view.y) * view.zoom + height / 2;
|
|
2273
|
-
if (x < -400 || x > width + cullMargin) return;
|
|
2274
|
-
if (y + radius > height || y + radius + primary * lineHeight * 2 < -3) return;
|
|
2275
|
-
const line = atlas.line(label, "500");
|
|
2276
|
-
const secondaryText = nodes.secondary[index];
|
|
2277
|
-
const secondary = secondaryText ? atlas.line(secondaryText, "400") : null;
|
|
2278
|
-
const scale = primary / 24;
|
|
2279
|
-
const textWidth = Math.max(line.width * scale, secondary ? secondary.width * scale * 11 / 12 : 0);
|
|
2280
|
-
const top = y + radius + 3;
|
|
2281
|
-
const bottom = top + primary * lineHeight * (secondary ? 2 : 1);
|
|
2282
|
-
const left = x - textWidth / 2;
|
|
2283
|
-
const right = x + textWidth / 2;
|
|
2284
|
-
if (right < 0 || left > width || bottom < 0 || top > height) return;
|
|
2285
|
-
const c0 = Math.max(0, Math.floor(left / cell));
|
|
2286
|
-
const c1 = Math.min(cols - 1, Math.floor(right / cell));
|
|
2287
|
-
const r0 = Math.max(0, Math.floor(top / cell));
|
|
2288
|
-
const r1 = Math.min(rows - 1, Math.floor(bottom / cell));
|
|
2289
|
-
if (!force) {
|
|
2290
|
-
for (let row = r0; row <= r1; row++) for (let col = c0; col <= c1; col++) if (occupancy[row * cols + col] === 1) return;
|
|
2403
|
+
destroy() {
|
|
2404
|
+
this.#destroyed = true;
|
|
2405
|
+
this.#gl.deleteTexture(this.#placeholder);
|
|
2406
|
+
if (this.#texture) this.#gl.deleteTexture(this.#texture);
|
|
2407
|
+
}
|
|
2408
|
+
#fail(src) {
|
|
2409
|
+
if (this.#destroyed) return;
|
|
2410
|
+
if (dev && this.#entries.has(src)) console.warn(`graph: cannot load the image ${src}`);
|
|
2411
|
+
}
|
|
2412
|
+
#store(src, image) {
|
|
2413
|
+
const entry = this.#entries.get(src);
|
|
2414
|
+
if (this.#destroyed || !entry || entry.layer >= 0) return;
|
|
2415
|
+
const layer = this.#free.pop() ?? this.#next;
|
|
2416
|
+
if (layer >= this.#capacity && !this.#grow()) {
|
|
2417
|
+
if (dev && !this.#warnedFull) {
|
|
2418
|
+
this.#warnedFull = true;
|
|
2419
|
+
console.warn(`graph: more than ${maxLayers} distinct images, the others are not drawn`);
|
|
2291
2420
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
if (layer === this.#next) this.#next++;
|
|
2424
|
+
const canvas = this.#draw(image);
|
|
2425
|
+
const gl = this.#gl;
|
|
2426
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#texture);
|
|
2427
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
2428
|
+
try {
|
|
2429
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, 256, 256, 1, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
|
2430
|
+
} catch {
|
|
2431
|
+
this.#free.push(layer);
|
|
2432
|
+
this.#fail(src);
|
|
2433
|
+
return;
|
|
2434
|
+
} finally {
|
|
2435
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
2436
|
+
}
|
|
2437
|
+
entry.layer = layer;
|
|
2438
|
+
this.#mipsStale = true;
|
|
2439
|
+
this.#loaded.add(src);
|
|
2440
|
+
this.#invalidate();
|
|
2441
|
+
}
|
|
2442
|
+
#draw(image) {
|
|
2443
|
+
this.#canvas ??= Object.assign(document.createElement("canvas"), {
|
|
2444
|
+
width: 256,
|
|
2445
|
+
height: 256
|
|
2446
|
+
});
|
|
2447
|
+
const context = this.#canvas.getContext("2d");
|
|
2448
|
+
if (context) {
|
|
2449
|
+
context.clearRect(0, 0, 256, 256);
|
|
2450
|
+
const width = image.naturalWidth;
|
|
2451
|
+
const height = image.naturalHeight;
|
|
2452
|
+
const scale = width > 0 && height > 0 ? 256 / Math.min(width, height) : 0;
|
|
2453
|
+
const drawnWidth = scale > 0 ? width * scale : 256;
|
|
2454
|
+
const drawnHeight = scale > 0 ? height * scale : 256;
|
|
2455
|
+
context.drawImage(image, (256 - drawnWidth) / 2, (256 - drawnHeight) / 2, drawnWidth, drawnHeight);
|
|
2314
2456
|
}
|
|
2457
|
+
return this.#canvas;
|
|
2315
2458
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2459
|
+
#grow() {
|
|
2460
|
+
if (this.#capacity >= maxLayers) return false;
|
|
2461
|
+
const gl = this.#gl;
|
|
2462
|
+
const capacity = Math.min(maxLayers, Math.max(4, this.#capacity * 2));
|
|
2463
|
+
const texture = gl.createTexture();
|
|
2464
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
|
|
2465
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, levels, gl.RGBA8, 256, 256, capacity);
|
|
2466
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
2467
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
2468
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2469
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2470
|
+
const previous = this.#texture;
|
|
2471
|
+
if (previous) {
|
|
2472
|
+
const framebuffer = gl.createFramebuffer();
|
|
2473
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer);
|
|
2474
|
+
for (let layer = 0; layer < this.#capacity; layer++) {
|
|
2475
|
+
gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, previous, 0, layer);
|
|
2476
|
+
gl.copyTexSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, 0, 0, 256, 256);
|
|
2477
|
+
}
|
|
2478
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
2479
|
+
gl.deleteFramebuffer(framebuffer);
|
|
2480
|
+
gl.deleteTexture(previous);
|
|
2481
|
+
}
|
|
2482
|
+
this.#texture = texture;
|
|
2483
|
+
this.#capacity = capacity;
|
|
2484
|
+
this.#mipsStale = true;
|
|
2485
|
+
return true;
|
|
2323
2486
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
// src/webgl/shaders.ts
|
|
2487
|
+
};
|
|
2488
|
+
const badgeFont = .66;
|
|
2489
|
+
const badgeRing = 1.5;
|
|
2490
|
+
const platePadX = .45;
|
|
2491
|
+
const plateHalfOne = .75;
|
|
2492
|
+
const plateHalfTwo = 1.32;
|
|
2493
|
+
const plateRadius = .25;
|
|
2332
2494
|
const header = `#version 300 es
|
|
2333
2495
|
precision highp float;
|
|
2334
2496
|
precision highp int;
|
|
2335
2497
|
precision highp sampler2D;
|
|
2336
2498
|
precision highp usampler2D;
|
|
2499
|
+
precision highp sampler2DArray;
|
|
2337
2500
|
layout(std140) uniform Frame {
|
|
2338
2501
|
vec2 center;
|
|
2339
2502
|
vec2 viewport;
|
|
@@ -2353,7 +2516,7 @@ layout(std140) uniform States {
|
|
|
2353
2516
|
uniform int layerSlot;
|
|
2354
2517
|
uniform int overlay;
|
|
2355
2518
|
uniform sampler2D palette;
|
|
2356
|
-
const uint highlightMask =
|
|
2519
|
+
const uint highlightMask = 7u;
|
|
2357
2520
|
const uint none = 0xffffffffu;
|
|
2358
2521
|
const vec4 hidden = vec4(2.0, 2.0, 2.0, 1.0);
|
|
2359
2522
|
vec2 toScreen(vec2 world) { return (world - center) * zoom + viewport * 0.5; }
|
|
@@ -2362,7 +2525,7 @@ vec4 toClip(vec2 screen) {
|
|
|
2362
2525
|
}
|
|
2363
2526
|
ivec2 texel(uint index) { return ivec2(int(index & 1023u), int(index >> 10u)); }
|
|
2364
2527
|
vec4 paletteColor(uint id) { return texelFetch(palette, ivec2(int(id & 255u), int(id >> 8u)), 0); }
|
|
2365
|
-
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; };
|
|
2366
2529
|
Look resolve(uint mask, Look look, int table) {
|
|
2367
2530
|
bool opacity = false;
|
|
2368
2531
|
bool strokeWidth = false;
|
|
@@ -2370,6 +2533,8 @@ Look resolve(uint mask, Look look, int table) {
|
|
|
2370
2533
|
bool label = false;
|
|
2371
2534
|
bool fill = false;
|
|
2372
2535
|
bool stroke = false;
|
|
2536
|
+
bool outline = false;
|
|
2537
|
+
bool outlineWidth = false;
|
|
2373
2538
|
float base = look.opacity;
|
|
2374
2539
|
for (int bit = 0; bit < 32; bit++) {
|
|
2375
2540
|
if (mask == 0u) break;
|
|
@@ -2384,6 +2549,8 @@ Look resolve(uint mask, Look look, int table) {
|
|
|
2384
2549
|
if (!label && numbers.w >= 0.0) { look.label = numbers.w; label = true; }
|
|
2385
2550
|
if (!fill && paint.x != none) { look.fill = paint.x; fill = true; }
|
|
2386
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; }
|
|
2387
2554
|
}
|
|
2388
2555
|
return look;
|
|
2389
2556
|
}
|
|
@@ -2393,11 +2560,23 @@ uniform sampler2D nodePosition;
|
|
|
2393
2560
|
uniform sampler2D nodeStyle;
|
|
2394
2561
|
uniform usampler2D nodePaint;
|
|
2395
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
|
+
}
|
|
2396
2568
|
float nodeDiameter(vec4 style) {
|
|
2397
2569
|
uint flags = uint(style.w + 0.5);
|
|
2398
2570
|
return (flags & 4u) != 0u ? style.x : style.x * zoom;
|
|
2399
2571
|
}
|
|
2400
2572
|
float nodeRadius(vec4 style) { return max(nodeDiameter(style) * 0.5, 1.25); }
|
|
2573
|
+
float badgeHeight(vec4 style) { return min(nodeDiameter(style) * 0.55, ${18 .toFixed(1)}); }
|
|
2574
|
+
vec2 badgeCenter(vec2 xy, vec4 style, uint corner) {
|
|
2575
|
+
uint shape = uint(style.w + 0.5) & 3u;
|
|
2576
|
+
float reach = shape == 0u ? 0.70710678 : shape == 1u ? 0.86 : 0.6;
|
|
2577
|
+
vec2 side = vec2((corner & 1u) != 0u ? 1.0 : -1.0, (corner & 2u) != 0u ? 1.0 : -1.0);
|
|
2578
|
+
return toScreen(xy) + side * reach * nodeRadius(style);
|
|
2579
|
+
}
|
|
2401
2580
|
`;
|
|
2402
2581
|
const edgeData = `
|
|
2403
2582
|
uniform usampler2D edgeTopology;
|
|
@@ -2408,12 +2587,17 @@ const nodeVertex = `${header}${nodeData}
|
|
|
2408
2587
|
in vec2 corner;
|
|
2409
2588
|
in uint instance;
|
|
2410
2589
|
out vec2 vLocal;
|
|
2590
|
+
flat out float vImageLayer;
|
|
2591
|
+
flat out float vImageScale;
|
|
2592
|
+
flat out vec4 vImageColor;
|
|
2411
2593
|
out float vRadius;
|
|
2412
2594
|
out float vStrokeWidth;
|
|
2413
2595
|
out float vHaloWidth;
|
|
2596
|
+
out float vOutlineWidth;
|
|
2414
2597
|
flat out uint vShape;
|
|
2415
2598
|
flat out vec4 vFill;
|
|
2416
2599
|
flat out vec4 vStroke;
|
|
2600
|
+
flat out vec4 vOutline;
|
|
2417
2601
|
flat out float vOpacity;
|
|
2418
2602
|
void main() {
|
|
2419
2603
|
ivec2 t = texel(instance);
|
|
@@ -2426,29 +2610,42 @@ void main() {
|
|
|
2426
2610
|
gl_Position = hidden;
|
|
2427
2611
|
return;
|
|
2428
2612
|
}
|
|
2429
|
-
Look look =
|
|
2613
|
+
Look look = nodeLook(t, style, paint, mask);
|
|
2430
2614
|
vec2 xy = texelFetch(nodePosition, t, 0).xy;
|
|
2431
2615
|
float radius = nodeRadius(style);
|
|
2432
2616
|
float halo = look.halo > 0.5 ? haloWidth : 0.0;
|
|
2433
|
-
|
|
2617
|
+
float outline = max(look.outlineWidth, 0.0);
|
|
2618
|
+
vLocal = corner * (radius + outline + halo + 1.5);
|
|
2434
2619
|
vRadius = radius;
|
|
2435
2620
|
vStrokeWidth = min(look.strokeWidth, radius * 0.5);
|
|
2436
2621
|
vHaloWidth = halo;
|
|
2622
|
+
vOutlineWidth = outline;
|
|
2623
|
+
vOutline = paletteColor(look.outline);
|
|
2437
2624
|
vShape = flags & 3u;
|
|
2438
2625
|
vFill = paletteColor(look.fill);
|
|
2439
2626
|
vStroke = paletteColor(look.stroke);
|
|
2440
2627
|
vOpacity = look.opacity;
|
|
2628
|
+
uvec4 image = texelFetch(nodeDecor, t, 0);
|
|
2629
|
+
vImageLayer = float(image.x) - 1.0;
|
|
2630
|
+
vImageScale = max(uintBitsToFloat(image.z), 0.001);
|
|
2631
|
+
vImageColor = image.y == none ? vec4(-1.0) : paletteColor(image.y);
|
|
2441
2632
|
gl_Position = toClip(toScreen(xy) + vLocal);
|
|
2442
2633
|
}
|
|
2443
2634
|
`;
|
|
2444
2635
|
const nodeFragment = `${header}
|
|
2636
|
+
uniform sampler2DArray images;
|
|
2445
2637
|
in vec2 vLocal;
|
|
2638
|
+
flat in float vImageLayer;
|
|
2639
|
+
flat in float vImageScale;
|
|
2640
|
+
flat in vec4 vImageColor;
|
|
2446
2641
|
in float vRadius;
|
|
2447
2642
|
in float vStrokeWidth;
|
|
2448
2643
|
in float vHaloWidth;
|
|
2644
|
+
in float vOutlineWidth;
|
|
2449
2645
|
flat in uint vShape;
|
|
2450
2646
|
flat in vec4 vFill;
|
|
2451
2647
|
flat in vec4 vStroke;
|
|
2648
|
+
flat in vec4 vOutline;
|
|
2452
2649
|
flat in float vOpacity;
|
|
2453
2650
|
out vec4 fragColor;
|
|
2454
2651
|
float shapeDistance(vec2 p, float r) {
|
|
@@ -2464,11 +2661,23 @@ void main() {
|
|
|
2464
2661
|
float d = shapeDistance(vLocal, vRadius);
|
|
2465
2662
|
float inside = 1.0 - smoothstep(-aa, aa, d);
|
|
2466
2663
|
float ring = vStrokeWidth > 0.0 ? smoothstep(-vStrokeWidth - aa, -vStrokeWidth + aa, d) : 0.0;
|
|
2467
|
-
vec4
|
|
2468
|
-
|
|
2469
|
-
|
|
2664
|
+
vec4 fill = vec4(vFill.rgb * vFill.a, vFill.a);
|
|
2665
|
+
if (vImageLayer >= 0.0) {
|
|
2666
|
+
vec2 uv = vLocal / (vRadius * 2.0 * vImageScale) + 0.5;
|
|
2667
|
+
vec4 picture = texture(images, vec3(uv, vImageLayer));
|
|
2668
|
+
vec2 within = step(vec2(0.0), uv) * step(uv, vec2(1.0));
|
|
2669
|
+
picture *= within.x * within.y;
|
|
2670
|
+
if (vImageColor.a >= 0.0) picture = vec4(vImageColor.rgb * vImageColor.a, vImageColor.a) * picture.a;
|
|
2671
|
+
fill = picture + fill * (1.0 - picture.a);
|
|
2672
|
+
}
|
|
2673
|
+
vec4 color = mix(fill, vec4(vStroke.rgb * vStroke.a, vStroke.a), ring);
|
|
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
|
+
}
|
|
2470
2679
|
if (vHaloWidth > 0.0) {
|
|
2471
|
-
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;
|
|
2472
2681
|
result += vec4(haloColor.rgb * h, h) * (1.0 - result.a);
|
|
2473
2682
|
}
|
|
2474
2683
|
if (result.a < 0.002) discard;
|
|
@@ -2504,7 +2713,7 @@ void main() {
|
|
|
2504
2713
|
gl_Position = hidden;
|
|
2505
2714
|
return;
|
|
2506
2715
|
}
|
|
2507
|
-
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);
|
|
2508
2717
|
ivec2 s = texel(topology.x);
|
|
2509
2718
|
ivec2 e = texel(topology.y);
|
|
2510
2719
|
vec2 ps = toScreen(texelFetch(nodePosition, s, 0).xy);
|
|
@@ -2606,7 +2815,7 @@ void main() {
|
|
|
2606
2815
|
gl_Position = hidden;
|
|
2607
2816
|
return;
|
|
2608
2817
|
}
|
|
2609
|
-
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);
|
|
2610
2819
|
ivec2 s = texel(topology.x);
|
|
2611
2820
|
ivec2 e = texel(topology.y);
|
|
2612
2821
|
vec2 a = toScreen(texelFetch(nodePosition, s, 0).xy);
|
|
@@ -2749,28 +2958,63 @@ in vec4 glyphRect;
|
|
|
2749
2958
|
in vec4 glyphUv;
|
|
2750
2959
|
out vec2 vUv;
|
|
2751
2960
|
out float vScale;
|
|
2752
|
-
flat out
|
|
2961
|
+
flat out vec4 vInk;
|
|
2962
|
+
flat out float vHalo;
|
|
2963
|
+
flat out vec4 vPlate;
|
|
2753
2964
|
flat out float vOpacity;
|
|
2754
2965
|
void main() {
|
|
2966
|
+
vPlate = vec4(0.0);
|
|
2755
2967
|
ivec2 t = texel(glyphNode);
|
|
2756
2968
|
vec4 style = texelFetch(nodeStyle, t, 0);
|
|
2757
2969
|
uvec4 paint = texelFetch(nodePaint, t, 0);
|
|
2758
2970
|
uint flags = uint(style.w + 0.5);
|
|
2759
2971
|
uint mask = texelFetch(nodeState, t, 0).r;
|
|
2760
|
-
Look look =
|
|
2972
|
+
Look look = nodeLook(t, style, paint, mask);
|
|
2973
|
+
vUv = mix(glyphUv.xy, glyphUv.zw, corner);
|
|
2974
|
+
vOpacity = look.opacity;
|
|
2975
|
+
uint kind = glyphKind & 3u;
|
|
2976
|
+
if (kind == 2u) {
|
|
2977
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
2978
|
+
float height = badgeHeight(style);
|
|
2979
|
+
if ((flags & 8u) == 0u || highlighted != (overlay == 1) || height < ${12 .toFixed(1)}) {
|
|
2980
|
+
gl_Position = hidden;
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
float font = height * ${badgeFont.toFixed(2)};
|
|
2984
|
+
vec2 center = badgeCenter(texelFetch(nodePosition, t, 0).xy, style, (glyphKind >> 2u) & 3u);
|
|
2985
|
+
vScale = font / atlasFontSize;
|
|
2986
|
+
vInk = paletteColor(glyphKind >> 4u);
|
|
2987
|
+
vHalo = 0.0;
|
|
2988
|
+
gl_Position = toClip(center - vec2(0.0, font * 0.65) + (glyphRect.xy + corner * glyphRect.zw) * vScale);
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2761
2991
|
float primary = (flags & 4u) != 0u ? 12.0 : 12.0 * min(1.0, zoom);
|
|
2762
2992
|
if ((flags & 8u) == 0u || look.label < 0.5 || primary < labelMin) {
|
|
2763
2993
|
gl_Position = hidden;
|
|
2764
2994
|
return;
|
|
2765
2995
|
}
|
|
2766
|
-
float
|
|
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
|
+
}
|
|
3012
|
+
float size = kind == 0u ? primary : primary * 11.0 / 12.0;
|
|
2767
3013
|
float scale = size / atlasFontSize;
|
|
2768
|
-
|
|
2769
|
-
vec2 anchor = toScreen(texelFetch(nodePosition, t, 0).xy) + vec2(0.0, below);
|
|
2770
|
-
vUv = mix(glyphUv.xy, glyphUv.zw, corner);
|
|
3014
|
+
vec2 anchor = top + vec2(0.0, kind == 1u ? primary * 1.25 : 0.0);
|
|
2771
3015
|
vScale = scale;
|
|
2772
|
-
|
|
2773
|
-
|
|
3016
|
+
vInk = kind == 0u ? labelInk : labelMuted;
|
|
3017
|
+
vHalo = (glyphKind & 4u) != 0u ? 0.0 : 1.0;
|
|
2774
3018
|
gl_Position = toClip(anchor + (glyphRect.xy + corner * glyphRect.zw) * scale);
|
|
2775
3019
|
}
|
|
2776
3020
|
`;
|
|
@@ -2778,21 +3022,88 @@ const labelFragment = `${header}
|
|
|
2778
3022
|
uniform sampler2D atlas;
|
|
2779
3023
|
in vec2 vUv;
|
|
2780
3024
|
in float vScale;
|
|
2781
|
-
flat in
|
|
3025
|
+
flat in vec4 vInk;
|
|
3026
|
+
flat in float vHalo;
|
|
3027
|
+
flat in vec4 vPlate;
|
|
2782
3028
|
flat in float vOpacity;
|
|
2783
3029
|
out vec4 fragColor;
|
|
2784
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
|
+
}
|
|
2785
3040
|
float d = texture(atlas, vUv).r;
|
|
2786
3041
|
float gamma = 0.08 / max(vScale * pixelRatio, 0.05);
|
|
2787
3042
|
float ink = smoothstep(0.75 - gamma, 0.75 + gamma, d);
|
|
2788
|
-
float halo = smoothstep(0.55 - gamma, 0.55 + gamma, d);
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
float alpha = max(halo * labelHalo.a, ink * inkColor.a) * vOpacity;
|
|
3043
|
+
float halo = smoothstep(0.55 - gamma, 0.55 + gamma, d) * vHalo;
|
|
3044
|
+
vec3 rgb = vHalo > 0.5 ? mix(labelHalo.rgb, vInk.rgb, ink) : vInk.rgb;
|
|
3045
|
+
float alpha = max(halo * labelHalo.a, ink * vInk.a) * vOpacity;
|
|
2792
3046
|
if (alpha < 0.002) discard;
|
|
2793
3047
|
fragColor = vec4(rgb * alpha, alpha);
|
|
2794
3048
|
}
|
|
2795
3049
|
`;
|
|
3050
|
+
const badgeVertex = `${header}${nodeData}
|
|
3051
|
+
in vec2 corner;
|
|
3052
|
+
in uint badgeNode;
|
|
3053
|
+
in uint badgeInfo;
|
|
3054
|
+
in uint badgeFill;
|
|
3055
|
+
in uint badgeRingColor;
|
|
3056
|
+
in float badgeText;
|
|
3057
|
+
out vec2 vLocal;
|
|
3058
|
+
flat out vec2 vHalf;
|
|
3059
|
+
flat out vec4 vFill;
|
|
3060
|
+
flat out vec4 vRing;
|
|
3061
|
+
flat out float vOpacity;
|
|
3062
|
+
void main() {
|
|
3063
|
+
ivec2 t = texel(badgeNode);
|
|
3064
|
+
vec4 style = texelFetch(nodeStyle, t, 0);
|
|
3065
|
+
uvec4 paint = texelFetch(nodePaint, t, 0);
|
|
3066
|
+
uint flags = uint(style.w + 0.5);
|
|
3067
|
+
uint mask = texelFetch(nodeState, t, 0).r;
|
|
3068
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
3069
|
+
float height = badgeHeight(style);
|
|
3070
|
+
if ((flags & 8u) == 0u || highlighted != (overlay == 1) || height < ${10 .toFixed(1)}) {
|
|
3071
|
+
gl_Position = hidden;
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
Look look = nodeLook(t, style, paint, mask);
|
|
3075
|
+
bool text = (badgeInfo & 4u) != 0u && height >= ${12 .toFixed(1)};
|
|
3076
|
+
float font = height * ${badgeFont.toFixed(2)};
|
|
3077
|
+
float width = text ? max(height, badgeText * font / ${24 .toFixed(1)} + height * 0.6) : height;
|
|
3078
|
+
vec2 center = badgeCenter(texelFetch(nodePosition, t, 0).xy, style, badgeInfo & 3u);
|
|
3079
|
+
vHalf = vec2(width, height) * 0.5;
|
|
3080
|
+
vLocal = corner * (vHalf + ${badgeRing.toFixed(1)} + 1.0);
|
|
3081
|
+
vFill = paletteColor(badgeFill);
|
|
3082
|
+
vRing = paletteColor(badgeRingColor);
|
|
3083
|
+
vOpacity = look.opacity;
|
|
3084
|
+
gl_Position = toClip(center + vLocal);
|
|
3085
|
+
}
|
|
3086
|
+
`;
|
|
3087
|
+
const badgeFragment = `${header}
|
|
3088
|
+
in vec2 vLocal;
|
|
3089
|
+
flat in vec2 vHalf;
|
|
3090
|
+
flat in vec4 vFill;
|
|
3091
|
+
flat in vec4 vRing;
|
|
3092
|
+
flat in float vOpacity;
|
|
3093
|
+
out vec4 fragColor;
|
|
3094
|
+
void main() {
|
|
3095
|
+
float aa = 0.5 / pixelRatio + 0.25;
|
|
3096
|
+
vec2 q = vec2(max(abs(vLocal.x) - (vHalf.x - vHalf.y), 0.0), vLocal.y);
|
|
3097
|
+
float d = length(q) - vHalf.y;
|
|
3098
|
+
float inner = 1.0 - smoothstep(-aa, aa, d);
|
|
3099
|
+
float outer = 1.0 - smoothstep(${badgeRing.toFixed(1)} - aa, ${badgeRing.toFixed(1)} + aa, d);
|
|
3100
|
+
vec4 fill = vec4(vFill.rgb * vFill.a, vFill.a);
|
|
3101
|
+
vec4 ring = vec4(vRing.rgb * vRing.a, vRing.a);
|
|
3102
|
+
vec4 result = mix(ring, fill, inner) * outer * vOpacity;
|
|
3103
|
+
if (result.a < 0.002) discard;
|
|
3104
|
+
fragColor = result;
|
|
3105
|
+
}
|
|
3106
|
+
`;
|
|
2796
3107
|
const cornerNode = new Float32Array([
|
|
2797
3108
|
-1,
|
|
2798
3109
|
-1,
|
|
@@ -2872,6 +3183,166 @@ const cornerLabel = new Float32Array([
|
|
|
2872
3183
|
1,
|
|
2873
3184
|
1
|
|
2874
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
|
+
}
|
|
2875
3346
|
// src/webgl/renderer.ts
|
|
2876
3347
|
const blocks = {
|
|
2877
3348
|
Frame: 0,
|
|
@@ -2879,6 +3350,7 @@ const blocks = {
|
|
|
2879
3350
|
};
|
|
2880
3351
|
const paletteWidth = 256;
|
|
2881
3352
|
const labelInterval = 120;
|
|
3353
|
+
const badgeStride = 20;
|
|
2882
3354
|
const units = {
|
|
2883
3355
|
palette: 0,
|
|
2884
3356
|
nodePosition: 1,
|
|
@@ -2888,7 +3360,9 @@ const units = {
|
|
|
2888
3360
|
edgeTopology: 5,
|
|
2889
3361
|
edgeStyle: 6,
|
|
2890
3362
|
edgeState: 7,
|
|
2891
|
-
atlas: 8
|
|
3363
|
+
atlas: 8,
|
|
3364
|
+
images: 9,
|
|
3365
|
+
nodeDecor: 10
|
|
2892
3366
|
};
|
|
2893
3367
|
function iota(length) {
|
|
2894
3368
|
const values = new Uint32Array(length);
|
|
@@ -2898,7 +3372,7 @@ function iota(length) {
|
|
|
2898
3372
|
function rgba(color) {
|
|
2899
3373
|
return color.map((channel) => channel / 255);
|
|
2900
3374
|
}
|
|
2901
|
-
function createWebglRenderer(canvas) {
|
|
3375
|
+
function createWebglRenderer(canvas, invalidate = () => {}) {
|
|
2902
3376
|
const context = canvas.getContext("webgl2", {
|
|
2903
3377
|
alpha: true,
|
|
2904
3378
|
antialias: false,
|
|
@@ -2915,6 +3389,7 @@ function createWebglRenderer(canvas) {
|
|
|
2915
3389
|
const nodeStyle = new DataTexture(gl, format.rgba32f, f32);
|
|
2916
3390
|
const nodePaint = new DataTexture(gl, format.rgba32ui, u32);
|
|
2917
3391
|
const nodeState = new DataTexture(gl, format.r32ui, u32);
|
|
3392
|
+
const nodeDecor = new DataTexture(gl, format.rgba32ui, u32);
|
|
2918
3393
|
const edgeTopology = new DataTexture(gl, format.rgba32ui, u32);
|
|
2919
3394
|
const edgeStyle = new DataTexture(gl, format.rgba32f, f32);
|
|
2920
3395
|
const edgeState = new DataTexture(gl, format.r32ui, u32);
|
|
@@ -2922,8 +3397,12 @@ function createWebglRenderer(canvas) {
|
|
|
2922
3397
|
nodePosition,
|
|
2923
3398
|
nodeStyle,
|
|
2924
3399
|
nodePaint,
|
|
2925
|
-
nodeState
|
|
3400
|
+
nodeState,
|
|
3401
|
+
nodeDecor
|
|
2926
3402
|
];
|
|
3403
|
+
const images = new ImageAtlas(gl, invalidate);
|
|
3404
|
+
const floatBits = /* @__PURE__ */ new Float32Array(1);
|
|
3405
|
+
const floatWord = new Uint32Array(floatBits.buffer);
|
|
2927
3406
|
const edgeTextures = [
|
|
2928
3407
|
edgeTopology,
|
|
2929
3408
|
edgeStyle,
|
|
@@ -2957,10 +3436,12 @@ function createWebglRenderer(canvas) {
|
|
|
2957
3436
|
const nodeProgram = createProgram(gl, nodeVertex, nodeFragment, blocks);
|
|
2958
3437
|
const edgeProgram = createProgram(gl, edgeVertex, edgeFragment, blocks);
|
|
2959
3438
|
const labelProgram = createProgram(gl, labelVertex, labelFragment, blocks);
|
|
3439
|
+
const badgeProgram = createProgram(gl, badgeVertex, badgeFragment, blocks);
|
|
2960
3440
|
const programs = [
|
|
2961
3441
|
nodeProgram,
|
|
2962
3442
|
edgeProgram,
|
|
2963
|
-
labelProgram
|
|
3443
|
+
labelProgram,
|
|
3444
|
+
badgeProgram
|
|
2964
3445
|
];
|
|
2965
3446
|
const locations = /* @__PURE__ */ new Map();
|
|
2966
3447
|
function prepare(program) {
|
|
@@ -2988,8 +3469,11 @@ function createWebglRenderer(canvas) {
|
|
|
2988
3469
|
const curveIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
|
|
2989
3470
|
const labelMainBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
2990
3471
|
const labelHighlightBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
3472
|
+
const badgeBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
3473
|
+
const badgeGlyphBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
2991
3474
|
const warmIndex = createBuffer(gl, /* @__PURE__ */ new Uint32Array(1), gl.STATIC_DRAW);
|
|
2992
3475
|
const warmGlyph = createBuffer(gl, /* @__PURE__ */ new Uint8Array(32), gl.STATIC_DRAW);
|
|
3476
|
+
const warmBadge = createBuffer(gl, new Uint8Array(badgeStride), gl.STATIC_DRAW);
|
|
2993
3477
|
const buffers = [
|
|
2994
3478
|
nodeCorner,
|
|
2995
3479
|
edgeCorner,
|
|
@@ -3002,8 +3486,11 @@ function createWebglRenderer(canvas) {
|
|
|
3002
3486
|
curveIndices,
|
|
3003
3487
|
labelMainBuffer,
|
|
3004
3488
|
labelHighlightBuffer,
|
|
3489
|
+
badgeBuffer,
|
|
3490
|
+
badgeGlyphBuffer,
|
|
3005
3491
|
warmIndex,
|
|
3006
|
-
warmGlyph
|
|
3492
|
+
warmGlyph,
|
|
3493
|
+
warmBadge
|
|
3007
3494
|
];
|
|
3008
3495
|
const corner = (buffer, size = 2) => ({
|
|
3009
3496
|
name: "corner",
|
|
@@ -3062,18 +3549,39 @@ function createWebglRenderer(canvas) {
|
|
|
3062
3549
|
offset: 24
|
|
3063
3550
|
}
|
|
3064
3551
|
];
|
|
3552
|
+
const badgeField = (buffer, name, offset, type = gl.UNSIGNED_INT) => ({
|
|
3553
|
+
name,
|
|
3554
|
+
buffer,
|
|
3555
|
+
size: 1,
|
|
3556
|
+
type,
|
|
3557
|
+
integer: type === gl.UNSIGNED_INT,
|
|
3558
|
+
divisor: 1,
|
|
3559
|
+
stride: badgeStride,
|
|
3560
|
+
offset
|
|
3561
|
+
});
|
|
3562
|
+
const badgeAttributes = (buffer) => [
|
|
3563
|
+
corner(nodeCorner),
|
|
3564
|
+
badgeField(buffer, "badgeNode", 0),
|
|
3565
|
+
badgeField(buffer, "badgeInfo", 4),
|
|
3566
|
+
badgeField(buffer, "badgeFill", 8),
|
|
3567
|
+
badgeField(buffer, "badgeRingColor", 12),
|
|
3568
|
+
badgeField(buffer, "badgeText", 16, gl.FLOAT)
|
|
3569
|
+
];
|
|
3065
3570
|
const vertexArrays = {
|
|
3066
3571
|
nodes: createVertexArray(gl, nodeProgram, [corner(nodeCorner), instance(nodeIndices)]),
|
|
3067
3572
|
nodeOverlay: createVertexArray(gl, nodeProgram, [corner(nodeCorner), instance(nodeOverlayIndices)]),
|
|
3068
3573
|
edges: createVertexArray(gl, edgeProgram, [corner(edgeCorner, 3), instance(edgeIndices)]),
|
|
3069
3574
|
edgeOverlay: createVertexArray(gl, edgeProgram, [corner(edgeCorner, 3), instance(edgeOverlayIndices)]),
|
|
3070
3575
|
labels: createVertexArray(gl, labelProgram, glyphs(labelMainBuffer)),
|
|
3071
|
-
labelHighlight: createVertexArray(gl, labelProgram, glyphs(labelHighlightBuffer))
|
|
3576
|
+
labelHighlight: createVertexArray(gl, labelProgram, glyphs(labelHighlightBuffer)),
|
|
3577
|
+
badges: createVertexArray(gl, badgeProgram, badgeAttributes(badgeBuffer)),
|
|
3578
|
+
badgeGlyphs: createVertexArray(gl, labelProgram, glyphs(badgeGlyphBuffer))
|
|
3072
3579
|
};
|
|
3073
3580
|
const vertexCounts = /* @__PURE__ */ new Map([
|
|
3074
3581
|
[nodeProgram, cornerNode.length / 2],
|
|
3075
3582
|
[edgeProgram, cornerEdge.length / 3],
|
|
3076
|
-
[labelProgram, cornerLabel.length / 2]
|
|
3583
|
+
[labelProgram, cornerLabel.length / 2],
|
|
3584
|
+
[badgeProgram, cornerNode.length / 2]
|
|
3077
3585
|
]);
|
|
3078
3586
|
let warmed = false;
|
|
3079
3587
|
let curvePass = null;
|
|
@@ -3107,6 +3615,13 @@ function createWebglRenderer(canvas) {
|
|
|
3107
3615
|
let orderStale = true;
|
|
3108
3616
|
let lastLabelTime = Number.NEGATIVE_INFINITY;
|
|
3109
3617
|
let lastLabelView = "";
|
|
3618
|
+
const imageOf = [];
|
|
3619
|
+
const badged = /* @__PURE__ */ new Set();
|
|
3620
|
+
const badgeGlyphs = new GlyphBuffer();
|
|
3621
|
+
let badgeData = /* @__PURE__ */ new ArrayBuffer(1280);
|
|
3622
|
+
let badgeCount = 0;
|
|
3623
|
+
let badgesStale = false;
|
|
3624
|
+
let badgeGeneration = -1;
|
|
3110
3625
|
function packNode(nodes, index) {
|
|
3111
3626
|
const at = index * 4;
|
|
3112
3627
|
const flags = (nodes.shape[index] ?? 0) | (nodes.screenUnits[index] ?? 0) << 2 | (nodes.alive[index] ?? 0) << 3;
|
|
@@ -3117,6 +3632,76 @@ function createWebglRenderer(canvas) {
|
|
|
3117
3632
|
nodePaint.data[at] = nodes.fill[index] ?? 0;
|
|
3118
3633
|
nodePaint.data[at + 1] = nodes.stroke[index] ?? 0;
|
|
3119
3634
|
nodePaint.data[at + 2] = nodes.slot[index] ?? 0;
|
|
3635
|
+
nodePaint.data[at + 3] = nodes.outline[index] ?? 4294967295;
|
|
3636
|
+
const src = nodes.alive[index] === 1 ? nodes.image[index] ?? null : null;
|
|
3637
|
+
const previous = imageOf[index] ?? null;
|
|
3638
|
+
if (src !== previous) {
|
|
3639
|
+
if (previous) images.release(previous);
|
|
3640
|
+
if (src) images.acquire(src);
|
|
3641
|
+
imageOf[index] = src;
|
|
3642
|
+
}
|
|
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;
|
|
3649
|
+
const badges = nodes.alive[index] === 1 ? nodes.badges[index] : null;
|
|
3650
|
+
if (badges || badged.has(index)) badgesStale = true;
|
|
3651
|
+
if (badges) badged.add(index);
|
|
3652
|
+
else badged.delete(index);
|
|
3653
|
+
}
|
|
3654
|
+
function refreshImages(nodes) {
|
|
3655
|
+
const loaded = images.takeLoaded();
|
|
3656
|
+
if (!loaded) return;
|
|
3657
|
+
let first = Number.POSITIVE_INFINITY;
|
|
3658
|
+
let last = -1;
|
|
3659
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
3660
|
+
const src = imageOf[index];
|
|
3661
|
+
if (!src || !loaded.has(src)) continue;
|
|
3662
|
+
nodeDecor.data[index * 4] = images.layer(src) + 1;
|
|
3663
|
+
first = Math.min(first, index);
|
|
3664
|
+
last = Math.max(last, index);
|
|
3665
|
+
}
|
|
3666
|
+
nodeDecor.upload(first, last);
|
|
3667
|
+
}
|
|
3668
|
+
function rebuildBadges() {
|
|
3669
|
+
if (!input) return;
|
|
3670
|
+
const nodes = input.scene.nodes;
|
|
3671
|
+
const generation = atlas.generation;
|
|
3672
|
+
badgeGlyphs.clear();
|
|
3673
|
+
badgeCount = 0;
|
|
3674
|
+
let words = new Uint32Array(badgeData);
|
|
3675
|
+
let floats = new Float32Array(badgeData);
|
|
3676
|
+
for (const index of badged) for (const badge of nodes.badges[index] ?? []) {
|
|
3677
|
+
if ((badgeCount + 1) * badgeStride > badgeData.byteLength) {
|
|
3678
|
+
const grown = /* @__PURE__ */ new ArrayBuffer(badgeData.byteLength * 2);
|
|
3679
|
+
new Uint8Array(grown).set(new Uint8Array(badgeData));
|
|
3680
|
+
badgeData = grown;
|
|
3681
|
+
words = new Uint32Array(badgeData);
|
|
3682
|
+
floats = new Float32Array(badgeData);
|
|
3683
|
+
}
|
|
3684
|
+
const line = badge.text ? atlas.line(badge.text, "500") : null;
|
|
3685
|
+
const at = badgeCount * (badgeStride / 4);
|
|
3686
|
+
words[at] = index;
|
|
3687
|
+
words[at + 1] = badge.corner | (line ? 4 : 0);
|
|
3688
|
+
words[at + 2] = badge.fill;
|
|
3689
|
+
words[at + 3] = badge.ring;
|
|
3690
|
+
floats[at + 4] = line?.width ?? 0;
|
|
3691
|
+
if (line) badgeGlyphs.push(index, 2 | badge.corner << 2 | badge.color << 4, line, atlas.height);
|
|
3692
|
+
badgeCount++;
|
|
3693
|
+
}
|
|
3694
|
+
if (atlas.generation !== generation) {
|
|
3695
|
+
labelsStale = true;
|
|
3696
|
+
rebuildBadges();
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
badgesStale = false;
|
|
3700
|
+
badgeGeneration = atlas.generation;
|
|
3701
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, badgeBuffer);
|
|
3702
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(badgeData, 0, badgeCount * badgeStride), gl.DYNAMIC_DRAW);
|
|
3703
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, badgeGlyphBuffer);
|
|
3704
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(badgeGlyphs.data, 0, badgeGlyphs.count * 32), gl.DYNAMIC_DRAW);
|
|
3120
3705
|
}
|
|
3121
3706
|
function packEdge(edges, index) {
|
|
3122
3707
|
const at = index * 4;
|
|
@@ -3156,6 +3741,7 @@ function createWebglRenderer(canvas) {
|
|
|
3156
3741
|
}
|
|
3157
3742
|
nodeStyle.upload(first, last);
|
|
3158
3743
|
nodePaint.upload(first, last);
|
|
3744
|
+
nodeDecor.upload(first, last);
|
|
3159
3745
|
if (next.positionsChanged || next.full || grown) {
|
|
3160
3746
|
const xy = next.positions.xy;
|
|
3161
3747
|
nodePosition.data.set(xy.subarray(0, Math.min(xy.length, nodePosition.data.length)));
|
|
@@ -3298,7 +3884,8 @@ function createWebglRenderer(canvas) {
|
|
|
3298
3884
|
const passes = [
|
|
3299
3885
|
[nodeProgram, [corner(nodeCorner), instance(warmIndex)]],
|
|
3300
3886
|
[edgeProgram, [corner(edgeCorner, 3), instance(warmIndex)]],
|
|
3301
|
-
[labelProgram, glyphs(warmGlyph)]
|
|
3887
|
+
[labelProgram, glyphs(warmGlyph)],
|
|
3888
|
+
[badgeProgram, badgeAttributes(warmBadge)]
|
|
3302
3889
|
];
|
|
3303
3890
|
gl.enable(gl.SCISSOR_TEST);
|
|
3304
3891
|
gl.scissor(0, 0, 0, 0);
|
|
@@ -3315,6 +3902,7 @@ function createWebglRenderer(canvas) {
|
|
|
3315
3902
|
sync(next) {
|
|
3316
3903
|
input = next;
|
|
3317
3904
|
syncNodes(next);
|
|
3905
|
+
refreshImages(next.scene.nodes);
|
|
3318
3906
|
syncEdges(next);
|
|
3319
3907
|
syncStates(next);
|
|
3320
3908
|
if (next.dirty.nodes.size > 0 || next.full) labelsStale = true;
|
|
@@ -3351,7 +3939,12 @@ function createWebglRenderer(canvas) {
|
|
|
3351
3939
|
frameBlock[7] = 8;
|
|
3352
3940
|
gl.bindBuffer(gl.UNIFORM_BUFFER, frameBuffer);
|
|
3353
3941
|
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, frameBlock);
|
|
3942
|
+
if (badgesStale || badgeGeneration !== atlas.generation) rebuildBadges();
|
|
3943
|
+
const generation = atlas.generation;
|
|
3354
3944
|
const wait = selectLabels(frame, performance.now());
|
|
3945
|
+
if (atlas.generation !== generation) rebuildBadges();
|
|
3946
|
+
syncAtlas();
|
|
3947
|
+
images.prepare();
|
|
3355
3948
|
gl.viewport(0, 0, width, height);
|
|
3356
3949
|
gl.clearColor(0, 0, 0, 0);
|
|
3357
3950
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
@@ -3366,10 +3959,16 @@ function createWebglRenderer(canvas) {
|
|
|
3366
3959
|
draw(pass.program, pass.main, curved.size, layer.slot, 0);
|
|
3367
3960
|
}
|
|
3368
3961
|
} else draw(nodeProgram, vertexArrays.nodes, nodeCapacity, layer.slot, 0);
|
|
3962
|
+
draw(badgeProgram, vertexArrays.badges, badgeCount, 0, 0);
|
|
3963
|
+
draw(labelProgram, vertexArrays.badgeGlyphs, badgeGlyphs.count, 0, 0);
|
|
3369
3964
|
draw(labelProgram, vertexArrays.labels, labels.main.count, 0, 0);
|
|
3370
3965
|
draw(edgeProgram, vertexArrays.edgeOverlay, edgeOverlayCount, 0, 1);
|
|
3371
3966
|
if (curvePass && curved.size > 0) draw(curvePass.program, curvePass.overlay, edgeOverlayCount, 0, 1);
|
|
3372
3967
|
draw(nodeProgram, vertexArrays.nodeOverlay, nodeOverlayCount, 0, 1);
|
|
3968
|
+
if (nodeOverlayCount > 0) {
|
|
3969
|
+
draw(badgeProgram, vertexArrays.badges, badgeCount, 0, 1);
|
|
3970
|
+
draw(labelProgram, vertexArrays.badgeGlyphs, badgeGlyphs.count, 0, 1);
|
|
3971
|
+
}
|
|
3373
3972
|
draw(labelProgram, vertexArrays.labelHighlight, labels.highlight.count, 0, 1);
|
|
3374
3973
|
gl.bindVertexArray(null);
|
|
3375
3974
|
return wait;
|
|
@@ -3378,6 +3977,7 @@ function createWebglRenderer(canvas) {
|
|
|
3378
3977
|
for (const texture of [...nodeTextures, ...edgeTextures]) texture.destroy();
|
|
3379
3978
|
gl.deleteTexture(palette);
|
|
3380
3979
|
gl.deleteTexture(atlasTexture);
|
|
3980
|
+
images.destroy();
|
|
3381
3981
|
for (const buffer of [
|
|
3382
3982
|
...buffers,
|
|
3383
3983
|
frameBuffer,
|
|
@@ -3402,12 +4002,15 @@ function createWebglRenderer(canvas) {
|
|
|
3402
4002
|
[units.edgeTopology, edgeTopology.texture],
|
|
3403
4003
|
[units.edgeStyle, edgeStyle.texture],
|
|
3404
4004
|
[units.edgeState, edgeState.texture],
|
|
3405
|
-
[units.atlas, atlasTexture]
|
|
4005
|
+
[units.atlas, atlasTexture],
|
|
4006
|
+
[units.nodeDecor, nodeDecor.texture]
|
|
3406
4007
|
];
|
|
3407
4008
|
for (const [unit, texture] of textures) {
|
|
3408
4009
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
3409
4010
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
3410
4011
|
}
|
|
4012
|
+
gl.activeTexture(gl.TEXTURE0 + units.images);
|
|
4013
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, images.texture);
|
|
3411
4014
|
}
|
|
3412
4015
|
}
|
|
3413
4016
|
// src/host/events.ts
|
|
@@ -3437,8 +4040,71 @@ var Emitter = class {
|
|
|
3437
4040
|
this.#handlers.clear();
|
|
3438
4041
|
}
|
|
3439
4042
|
};
|
|
4043
|
+
// src/host/keyboard.ts
|
|
4044
|
+
const arrows = {
|
|
4045
|
+
ArrowLeft: {
|
|
4046
|
+
x: -1,
|
|
4047
|
+
y: 0
|
|
4048
|
+
},
|
|
4049
|
+
ArrowRight: {
|
|
4050
|
+
x: 1,
|
|
4051
|
+
y: 0
|
|
4052
|
+
},
|
|
4053
|
+
ArrowUp: {
|
|
4054
|
+
x: 0,
|
|
4055
|
+
y: -1
|
|
4056
|
+
},
|
|
4057
|
+
ArrowDown: {
|
|
4058
|
+
x: 0,
|
|
4059
|
+
y: 1
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
const keyZoom = 1.5;
|
|
4063
|
+
function keyAction(event) {
|
|
4064
|
+
if (event.altKey) return null;
|
|
4065
|
+
const command = event.ctrlKey || event.metaKey;
|
|
4066
|
+
const direction = arrows[event.key];
|
|
4067
|
+
if (direction) {
|
|
4068
|
+
if (command) return null;
|
|
4069
|
+
return event.shiftKey ? {
|
|
4070
|
+
type: "pan",
|
|
4071
|
+
direction
|
|
4072
|
+
} : {
|
|
4073
|
+
type: "move",
|
|
4074
|
+
direction
|
|
4075
|
+
};
|
|
4076
|
+
}
|
|
4077
|
+
switch (event.key) {
|
|
4078
|
+
case "Enter":
|
|
4079
|
+
case " ": return {
|
|
4080
|
+
type: "activate",
|
|
4081
|
+
additive: event.shiftKey || command
|
|
4082
|
+
};
|
|
4083
|
+
case "Escape": return { type: "escape" };
|
|
4084
|
+
}
|
|
4085
|
+
if (command) return null;
|
|
4086
|
+
switch (event.key) {
|
|
4087
|
+
case "+":
|
|
4088
|
+
case "=": return {
|
|
4089
|
+
type: "zoom",
|
|
4090
|
+
factor: keyZoom
|
|
4091
|
+
};
|
|
4092
|
+
case "-":
|
|
4093
|
+
case "_": return {
|
|
4094
|
+
type: "zoom",
|
|
4095
|
+
factor: 1 / keyZoom
|
|
4096
|
+
};
|
|
4097
|
+
case "0": return { type: "fit" };
|
|
4098
|
+
}
|
|
4099
|
+
return null;
|
|
4100
|
+
}
|
|
3440
4101
|
// src/host/state-registry.ts
|
|
3441
|
-
const highlightBits =
|
|
4102
|
+
const highlightBits = 7;
|
|
4103
|
+
const highlightStates = [
|
|
4104
|
+
"selected",
|
|
4105
|
+
"hovered",
|
|
4106
|
+
"focused"
|
|
4107
|
+
];
|
|
3442
4108
|
const kinds = ["nodes", "edges"];
|
|
3443
4109
|
function members() {
|
|
3444
4110
|
return {
|
|
@@ -3510,6 +4176,7 @@ var StateRegistry = class {
|
|
|
3510
4176
|
apply(changes) {
|
|
3511
4177
|
let selection = false;
|
|
3512
4178
|
let hovered = false;
|
|
4179
|
+
let focused = false;
|
|
3513
4180
|
const forget = (kind, removed) => {
|
|
3514
4181
|
for (const { key, index } of removed) {
|
|
3515
4182
|
this.#setMask(kind, index, 0);
|
|
@@ -3517,6 +4184,7 @@ var StateRegistry = class {
|
|
|
3517
4184
|
if (!this.#members.get(name)?.[kind].delete(key)) continue;
|
|
3518
4185
|
if (name === "selected") selection = true;
|
|
3519
4186
|
if (name === "hovered") hovered = true;
|
|
4187
|
+
if (name === "focused") focused = true;
|
|
3520
4188
|
}
|
|
3521
4189
|
}
|
|
3522
4190
|
};
|
|
@@ -3529,7 +4197,8 @@ var StateRegistry = class {
|
|
|
3529
4197
|
learn("edges", changes.edges.added);
|
|
3530
4198
|
return {
|
|
3531
4199
|
selection,
|
|
3532
|
-
hovered
|
|
4200
|
+
hovered,
|
|
4201
|
+
focused
|
|
3533
4202
|
};
|
|
3534
4203
|
}
|
|
3535
4204
|
takeHighlighted() {
|
|
@@ -3537,7 +4206,7 @@ var StateRegistry = class {
|
|
|
3537
4206
|
this.#highlightDirty = false;
|
|
3538
4207
|
const indices = (kind) => {
|
|
3539
4208
|
const found = /* @__PURE__ */ new Set();
|
|
3540
|
-
for (const name of
|
|
4209
|
+
for (const name of highlightStates) for (const key of this.#members.get(name)?.[kind] ?? []) {
|
|
3541
4210
|
const index = this.#index(kind, key);
|
|
3542
4211
|
if (index >= 0) found.add(index);
|
|
3543
4212
|
}
|
|
@@ -3582,6 +4251,14 @@ var StateRegistry = class {
|
|
|
3582
4251
|
}
|
|
3583
4252
|
};
|
|
3584
4253
|
// src/host/mount-graph.ts
|
|
4254
|
+
const svgNamespace = "http://www.w3.org/2000/svg";
|
|
4255
|
+
const lassoStep = 3;
|
|
4256
|
+
const keyPan = .1;
|
|
4257
|
+
const followMargin = 48;
|
|
4258
|
+
const keyDuration = 200;
|
|
4259
|
+
const visuallyHidden = "position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap";
|
|
4260
|
+
const focusOutline = "2px solid var(--graph-focus-ring,#c93700)";
|
|
4261
|
+
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";
|
|
3585
4262
|
const defaultPadding = 32;
|
|
3586
4263
|
const clickSlop = 4;
|
|
3587
4264
|
const pickTolerance = 4;
|
|
@@ -3624,7 +4301,9 @@ function resolveStyle(style, id) {
|
|
|
3624
4301
|
...style.halo !== void 0 ? { halo: style.halo } : {},
|
|
3625
4302
|
...style.label !== void 0 ? { label: style.label } : {},
|
|
3626
4303
|
...style.fill !== void 0 ? { fill: id(style.fill) } : {},
|
|
3627
|
-
...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 } : {}
|
|
3628
4307
|
};
|
|
3629
4308
|
}
|
|
3630
4309
|
function createGraphHost(container, options, createRenderer) {
|
|
@@ -3636,11 +4315,15 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3636
4315
|
element.setAttribute("data-graph", "");
|
|
3637
4316
|
element.setAttribute("role", "img");
|
|
3638
4317
|
element.setAttribute("aria-label", options.ariaLabel);
|
|
3639
|
-
element.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden";
|
|
4318
|
+
element.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden;outline:none;outline-offset:-2px";
|
|
3640
4319
|
const canvas = document.createElement("canvas");
|
|
3641
4320
|
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;outline:none";
|
|
3642
|
-
|
|
4321
|
+
const announcer = document.createElement("div");
|
|
4322
|
+
announcer.setAttribute("aria-live", "polite");
|
|
4323
|
+
announcer.style.cssText = visuallyHidden;
|
|
4324
|
+
element.append(canvas, announcer);
|
|
3643
4325
|
container.append(element);
|
|
4326
|
+
let remembered = null;
|
|
3644
4327
|
let frameRequest = 0;
|
|
3645
4328
|
let laterRequest;
|
|
3646
4329
|
let needsFlush = false;
|
|
@@ -3668,7 +4351,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3668
4351
|
const emitter = new Emitter();
|
|
3669
4352
|
const pickIndex = new PickIndex();
|
|
3670
4353
|
const themeResolver = new ThemeResolver(element);
|
|
3671
|
-
let renderer = createRenderer(canvas);
|
|
4354
|
+
let renderer = createRenderer(canvas, () => request());
|
|
3672
4355
|
registry.define(freeStateNames(definition.states));
|
|
3673
4356
|
if (options.selection !== void 0) registry.set("selected", options.selection);
|
|
3674
4357
|
const store = createStore({
|
|
@@ -3680,6 +4363,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3680
4363
|
width: 0,
|
|
3681
4364
|
height: 0,
|
|
3682
4365
|
hovered: null,
|
|
4366
|
+
focused: null,
|
|
3683
4367
|
selection: controlled ? registry.members("selected") : noKeys,
|
|
3684
4368
|
states: freeStates(),
|
|
3685
4369
|
positionsVersion: 0
|
|
@@ -3872,16 +4556,197 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3872
4556
|
updateCursor();
|
|
3873
4557
|
request();
|
|
3874
4558
|
}
|
|
4559
|
+
function nodePoint(key) {
|
|
4560
|
+
const record = model.node(key);
|
|
4561
|
+
return record ? {
|
|
4562
|
+
x: positions.x(record.index),
|
|
4563
|
+
y: positions.y(record.index)
|
|
4564
|
+
} : null;
|
|
4565
|
+
}
|
|
4566
|
+
function focusPoint(focus) {
|
|
4567
|
+
if (focus.kind === "node") return nodePoint(focus.key);
|
|
4568
|
+
const ends = model.endpoints(focus.key);
|
|
4569
|
+
const a = ends ? nodePoint(ends.source) : null;
|
|
4570
|
+
const b = ends ? nodePoint(ends.target) : null;
|
|
4571
|
+
return a && b ? {
|
|
4572
|
+
x: (a.x + b.x) / 2,
|
|
4573
|
+
y: (a.y + b.y) / 2
|
|
4574
|
+
} : null;
|
|
4575
|
+
}
|
|
4576
|
+
function labelOf(focus) {
|
|
4577
|
+
const record = focus.kind === "node" ? model.node(focus.key) : model.edge(focus.key);
|
|
4578
|
+
const labels = focus.kind === "node" ? scene.nodes.label : scene.edges.label;
|
|
4579
|
+
return record && labels[record.index] || String(focus.key);
|
|
4580
|
+
}
|
|
4581
|
+
function announce(text) {
|
|
4582
|
+
announcer.textContent = announcer.textContent === text ? `${text}\u00a0` : text;
|
|
4583
|
+
}
|
|
4584
|
+
function reveal(focus) {
|
|
4585
|
+
const point = focusPoint(focus);
|
|
4586
|
+
if (!point) return;
|
|
4587
|
+
const { view } = store.get();
|
|
4588
|
+
const size = viewport();
|
|
4589
|
+
const screen = toScreen(view, size, point);
|
|
4590
|
+
if (!(screen.x >= followMargin && screen.x <= size.width - followMargin && screen.y >= followMargin && screen.y <= size.height - followMargin)) animateTo({
|
|
4591
|
+
...view,
|
|
4592
|
+
x: point.x,
|
|
4593
|
+
y: point.y
|
|
4594
|
+
}, keyDuration);
|
|
4595
|
+
}
|
|
4596
|
+
function setFocused(next, follow = false) {
|
|
4597
|
+
if (sameTarget(store.get().focused, next)) return;
|
|
4598
|
+
registry.set("focused", next ? { [kindOf[next.kind]]: [next.key] } : {});
|
|
4599
|
+
patch({ focused: next });
|
|
4600
|
+
if (next) {
|
|
4601
|
+
element.style.outline = "none";
|
|
4602
|
+
announce(labelOf(next));
|
|
4603
|
+
if (follow) reveal(next);
|
|
4604
|
+
}
|
|
4605
|
+
emitter.emit("focuschange", { target: next });
|
|
4606
|
+
request();
|
|
4607
|
+
}
|
|
4608
|
+
function moveFocus(direction) {
|
|
4609
|
+
const current = store.get().focused;
|
|
4610
|
+
const from = current?.kind === "node" ? model.node(current.key)?.index ?? -1 : -1;
|
|
4611
|
+
let index = from >= 0 ? nodeToward(from, direction, scene.nodes, positions) : -1;
|
|
4612
|
+
if (from < 0) {
|
|
4613
|
+
const selected = registry.members("selected").nodes;
|
|
4614
|
+
const last = selected[selected.length - 1];
|
|
4615
|
+
const center = store.get().view;
|
|
4616
|
+
index = last !== void 0 ? model.node(last)?.index ?? -1 : nearestNode((current && focusPoint(current)) ?? center, scene.nodes, positions);
|
|
4617
|
+
}
|
|
4618
|
+
const key = index >= 0 ? scene.nodes.keys[index] : void 0;
|
|
4619
|
+
const next = key !== void 0 ? target("node", key) : null;
|
|
4620
|
+
if (next) setFocused(next, true);
|
|
4621
|
+
}
|
|
4622
|
+
function focusVisible() {
|
|
4623
|
+
try {
|
|
4624
|
+
return element.matches(":focus-visible");
|
|
4625
|
+
} catch {
|
|
4626
|
+
return true;
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
function onFocus() {
|
|
4630
|
+
if (!definition.interaction.keyboard || !focusVisible()) return;
|
|
4631
|
+
const restored = remembered ? target(remembered.kind, remembered.key) : null;
|
|
4632
|
+
remembered = null;
|
|
4633
|
+
if (restored) setFocused(restored, true);
|
|
4634
|
+
else if (!store.get().focused) element.style.outline = focusOutline;
|
|
4635
|
+
}
|
|
4636
|
+
function onBlur() {
|
|
4637
|
+
element.style.outline = "none";
|
|
4638
|
+
const current = store.get().focused;
|
|
4639
|
+
if (!current) return;
|
|
4640
|
+
remembered = current;
|
|
4641
|
+
setFocused(null);
|
|
4642
|
+
}
|
|
4643
|
+
function cancelLasso() {
|
|
4644
|
+
if (!gesture?.lasso) return false;
|
|
4645
|
+
if (canvas.hasPointerCapture?.(gesture.id)) canvas.releasePointerCapture(gesture.id);
|
|
4646
|
+
gesture = null;
|
|
4647
|
+
drawLasso(null);
|
|
4648
|
+
updateCursor();
|
|
4649
|
+
return true;
|
|
4650
|
+
}
|
|
4651
|
+
function runKey(event) {
|
|
4652
|
+
const action = keyAction(event);
|
|
4653
|
+
if (!action) return false;
|
|
4654
|
+
const { interaction } = definition;
|
|
4655
|
+
const { view, focused, selection } = store.get();
|
|
4656
|
+
const size = viewport();
|
|
4657
|
+
switch (action.type) {
|
|
4658
|
+
case "move":
|
|
4659
|
+
moveFocus(action.direction);
|
|
4660
|
+
return true;
|
|
4661
|
+
case "pan": {
|
|
4662
|
+
if (!interaction.pan) return false;
|
|
4663
|
+
const step = Math.min(size.width, size.height) * keyPan;
|
|
4664
|
+
commitView(panBy(view, -action.direction.x * step, -action.direction.y * step));
|
|
4665
|
+
return true;
|
|
4666
|
+
}
|
|
4667
|
+
case "zoom": {
|
|
4668
|
+
if (!interaction.zoom) return false;
|
|
4669
|
+
const point = focused ? focusPoint(focused) : null;
|
|
4670
|
+
animateTo(zoomAround(view, size, point ? toScreen(view, size, point) : {
|
|
4671
|
+
x: size.width / 2,
|
|
4672
|
+
y: size.height / 2
|
|
4673
|
+
}, action.factor), keyDuration);
|
|
4674
|
+
return true;
|
|
4675
|
+
}
|
|
4676
|
+
case "fit": return fitTo(void 0, { duration: keyDuration });
|
|
4677
|
+
case "activate":
|
|
4678
|
+
if (!focused || interaction.select === false) return false;
|
|
4679
|
+
selectTarget(focused, action.additive);
|
|
4680
|
+
return true;
|
|
4681
|
+
case "escape":
|
|
4682
|
+
if (cancelLasso()) return true;
|
|
4683
|
+
if (interaction.select !== false && selection.nodes.length + selection.edges.length > 0) {
|
|
4684
|
+
requestSelection(noKeys);
|
|
4685
|
+
return true;
|
|
4686
|
+
}
|
|
4687
|
+
if (!focused) return false;
|
|
4688
|
+
setFocused(null);
|
|
4689
|
+
return true;
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
function onKeyDown(event) {
|
|
4693
|
+
if (!definition.interaction.keyboard || event.target !== element) return;
|
|
4694
|
+
if (runKey(event)) event.preventDefault();
|
|
4695
|
+
}
|
|
4696
|
+
function applyKeyboard() {
|
|
4697
|
+
if (definition.interaction.keyboard) {
|
|
4698
|
+
element.tabIndex = 0;
|
|
4699
|
+
element.setAttribute("role", "application");
|
|
4700
|
+
return;
|
|
4701
|
+
}
|
|
4702
|
+
element.removeAttribute("tabindex");
|
|
4703
|
+
element.setAttribute("role", "img");
|
|
4704
|
+
element.style.outline = "none";
|
|
4705
|
+
setFocused(null);
|
|
4706
|
+
}
|
|
4707
|
+
let lassoLayer = null;
|
|
4708
|
+
function drawLasso(points) {
|
|
4709
|
+
if (!points) {
|
|
4710
|
+
lassoLayer?.remove();
|
|
4711
|
+
lassoLayer = null;
|
|
4712
|
+
return;
|
|
4713
|
+
}
|
|
4714
|
+
if (!lassoLayer) {
|
|
4715
|
+
lassoLayer = document.createElementNS(svgNamespace, "svg");
|
|
4716
|
+
lassoLayer.setAttribute("aria-hidden", "true");
|
|
4717
|
+
lassoLayer.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible";
|
|
4718
|
+
const path = document.createElementNS(svgNamespace, "path");
|
|
4719
|
+
path.style.cssText = lassoStyle;
|
|
4720
|
+
lassoLayer.append(path);
|
|
4721
|
+
element.append(lassoLayer);
|
|
4722
|
+
}
|
|
4723
|
+
lassoLayer.firstElementChild?.setAttribute("d", `M${points.map((point) => `${point.x} ${point.y}`).join("L")}Z`);
|
|
4724
|
+
}
|
|
4725
|
+
function selectInside(points) {
|
|
4726
|
+
const view = store.get().view;
|
|
4727
|
+
const size = viewport();
|
|
4728
|
+
const inside = nodesInside(points.map((point) => toWorld(view, size, point)), scene.nodes, positions).filter((key) => !registry.has("selected", "nodes", key));
|
|
4729
|
+
if (inside.length === 0) return;
|
|
4730
|
+
const current = registry.members("selected");
|
|
4731
|
+
requestSelection({
|
|
4732
|
+
...current,
|
|
4733
|
+
nodes: [...current.nodes, ...inside]
|
|
4734
|
+
});
|
|
4735
|
+
}
|
|
3875
4736
|
function updateCursor() {
|
|
3876
4737
|
const interactive = definition.interaction.select !== false || definition.interaction.drag;
|
|
3877
|
-
if (gesture?.
|
|
4738
|
+
if (gesture?.lasso) canvas.style.cursor = "crosshair";
|
|
4739
|
+
else if (gesture?.moved) canvas.style.cursor = "grabbing";
|
|
3878
4740
|
else if (store.get().hovered && interactive) canvas.style.cursor = "pointer";
|
|
3879
4741
|
else canvas.style.cursor = "";
|
|
3880
4742
|
}
|
|
3881
4743
|
function selectOnClick(clicked, event) {
|
|
4744
|
+
selectTarget(clicked, event.shiftKey || event.metaKey || event.ctrlKey);
|
|
4745
|
+
}
|
|
4746
|
+
function selectTarget(clicked, modifier) {
|
|
3882
4747
|
const mode = definition.interaction.select;
|
|
3883
4748
|
if (mode === false) return;
|
|
3884
|
-
const additive = mode === "multiple" &&
|
|
4749
|
+
const additive = mode === "multiple" && modifier;
|
|
3885
4750
|
if (!clicked) {
|
|
3886
4751
|
if (!additive) requestSelection(noKeys);
|
|
3887
4752
|
return;
|
|
@@ -3920,6 +4785,14 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3920
4785
|
const fresh = target(hovered.kind, hovered.key);
|
|
3921
4786
|
if (fresh && fresh.datum !== hovered.datum) patch({ hovered: fresh });
|
|
3922
4787
|
}
|
|
4788
|
+
const focused = store.get().focused;
|
|
4789
|
+
if (result.focused) {
|
|
4790
|
+
patch({ focused: null });
|
|
4791
|
+
emitter.emit("focuschange", { target: null });
|
|
4792
|
+
} else if (focused) {
|
|
4793
|
+
const fresh = target(focused.kind, focused.key);
|
|
4794
|
+
if (fresh && fresh.datum !== focused.datum) patch({ focused: fresh });
|
|
4795
|
+
}
|
|
3923
4796
|
}
|
|
3924
4797
|
function movePositions(indices, to, duration) {
|
|
3925
4798
|
if (indices.length === 0) return;
|
|
@@ -4101,18 +4974,24 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4101
4974
|
function onPointerDown(event) {
|
|
4102
4975
|
if (event.button !== 0 || gesture) return;
|
|
4103
4976
|
viewAnimation = null;
|
|
4977
|
+
remembered = null;
|
|
4978
|
+
setFocused(null);
|
|
4104
4979
|
const point = local(event);
|
|
4105
4980
|
const hit = pickAt(point);
|
|
4106
4981
|
const drag = hit ? startDrag(hit, point) : null;
|
|
4982
|
+
const { lasso, select } = definition.interaction;
|
|
4983
|
+
const lassoing = !hit && event.shiftKey && lasso && select === "multiple";
|
|
4107
4984
|
gesture = {
|
|
4108
4985
|
id: event.pointerId,
|
|
4109
4986
|
start: point,
|
|
4110
4987
|
last: point,
|
|
4111
4988
|
target: hit,
|
|
4112
4989
|
drag,
|
|
4990
|
+
lasso: lassoing ? [point] : null,
|
|
4113
4991
|
moved: false
|
|
4114
4992
|
};
|
|
4115
4993
|
canvas.setPointerCapture?.(event.pointerId);
|
|
4994
|
+
if (lassoing) updateCursor();
|
|
4116
4995
|
}
|
|
4117
4996
|
function onPointerMove(event) {
|
|
4118
4997
|
const point = local(event);
|
|
@@ -4125,6 +5004,16 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4125
5004
|
gesture.moved = true;
|
|
4126
5005
|
updateCursor();
|
|
4127
5006
|
}
|
|
5007
|
+
const lasso = gesture.lasso;
|
|
5008
|
+
if (lasso) {
|
|
5009
|
+
const last = lasso[lasso.length - 1] ?? gesture.start;
|
|
5010
|
+
if (gesture.moved && Math.hypot(point.x - last.x, point.y - last.y) >= lassoStep) {
|
|
5011
|
+
lasso.push(point);
|
|
5012
|
+
drawLasso(lasso);
|
|
5013
|
+
}
|
|
5014
|
+
gesture.last = point;
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
4128
5017
|
const drag = gesture.drag;
|
|
4129
5018
|
if (drag) {
|
|
4130
5019
|
if (!gesture.moved) return;
|
|
@@ -4161,6 +5050,10 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4161
5050
|
gesture = null;
|
|
4162
5051
|
if (canvas.hasPointerCapture?.(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
|
|
4163
5052
|
if (ended.drag?.started) emitter.emit("dragend", dragEvent(ended.drag, event));
|
|
5053
|
+
if (ended.lasso) {
|
|
5054
|
+
drawLasso(null);
|
|
5055
|
+
if (!cancelled && ended.moved) selectInside(ended.lasso);
|
|
5056
|
+
}
|
|
4164
5057
|
if (!cancelled && !ended.moved) {
|
|
4165
5058
|
emitter.emit("click", pointerEvent(event, ended.target));
|
|
4166
5059
|
selectOnClick(ended.target, event);
|
|
@@ -4198,7 +5091,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4198
5091
|
}
|
|
4199
5092
|
function onContextRestored() {
|
|
4200
5093
|
contextLost = false;
|
|
4201
|
-
renderer = createRenderer(canvas);
|
|
5094
|
+
renderer = createRenderer(canvas, () => request());
|
|
4202
5095
|
full = true;
|
|
4203
5096
|
refreshTheme();
|
|
4204
5097
|
}
|
|
@@ -4219,6 +5112,13 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4219
5112
|
["webglcontextrestored", onContextRestored]
|
|
4220
5113
|
];
|
|
4221
5114
|
for (const [name, listener, listenerOptions] of listeners) canvas.addEventListener(name, listener, listenerOptions);
|
|
5115
|
+
const elementListeners = [
|
|
5116
|
+
["keydown", onKeyDown],
|
|
5117
|
+
["focus", onFocus],
|
|
5118
|
+
["blur", onBlur]
|
|
5119
|
+
];
|
|
5120
|
+
for (const [name, listener] of elementListeners) element.addEventListener(name, listener);
|
|
5121
|
+
applyKeyboard();
|
|
4222
5122
|
model.setMarks(definition.marks);
|
|
4223
5123
|
scene.setDefinition(definition);
|
|
4224
5124
|
applyLayers();
|
|
@@ -4253,6 +5153,15 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4253
5153
|
set: (keys) => requestSelection(toKeys(keys)),
|
|
4254
5154
|
clear: () => requestSelection(noKeys)
|
|
4255
5155
|
},
|
|
5156
|
+
focus: {
|
|
5157
|
+
get: () => store.get().focused,
|
|
5158
|
+
set: (next) => {
|
|
5159
|
+
const resolved = next ? target(next.kind, next.key) : null;
|
|
5160
|
+
if (next && !resolved) return;
|
|
5161
|
+
if (resolved && definition.interaction.keyboard) element.focus({ preventScroll: true });
|
|
5162
|
+
setFocused(resolved, true);
|
|
5163
|
+
}
|
|
5164
|
+
},
|
|
4256
5165
|
states: {
|
|
4257
5166
|
set: (name, keys) => {
|
|
4258
5167
|
if (registry.set(declared(name), keys)) publishState(name);
|
|
@@ -4315,6 +5224,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4315
5224
|
applyLayers();
|
|
4316
5225
|
if (registry.define(freeStateNames(definition.states))) patch({ states: freeStates() });
|
|
4317
5226
|
applyStyles();
|
|
5227
|
+
applyKeyboard();
|
|
4318
5228
|
updateCursor();
|
|
4319
5229
|
request(true);
|
|
4320
5230
|
}
|
|
@@ -4327,6 +5237,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4327
5237
|
resizeObserver.disconnect();
|
|
4328
5238
|
stopTheme();
|
|
4329
5239
|
for (const [name, listener] of listeners) canvas.removeEventListener(name, listener);
|
|
5240
|
+
for (const [name, listener] of elementListeners) element.removeEventListener(name, listener);
|
|
4330
5241
|
emitter.clear();
|
|
4331
5242
|
model.destroy();
|
|
4332
5243
|
renderer.destroy();
|
|
@@ -4340,4 +5251,4 @@ function mountGraph(container, options) {
|
|
|
4340
5251
|
}
|
|
4341
5252
|
export { freeStateNames as n, maxFreeStates as r, mountGraph as t };
|
|
4342
5253
|
|
|
4343
|
-
//# sourceMappingURL=mount-graph-
|
|
5254
|
+
//# sourceMappingURL=mount-graph-mObhwJ3e.js.map
|