@bruno00o/graph 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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-DYOgD_ql.js → mount-graph-CzVfiIte.js} +853 -51
- package/dist/mount-graph-CzVfiIte.js.map +1 -0
- package/dist/{mount-graph-B88WGOQu.d.ts → mount-graph-DQLST9-_.d.ts} +13 -3
- package/dist/query.d.ts +1 -1
- package/dist/react.d.ts +3 -2
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/dist/{types-D6b86Rt1.d.ts → types-BaflHDaf.d.ts} +11 -4
- package/package.json +1 -1
- package/dist/mount-graph-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;
|
|
@@ -994,7 +1027,10 @@ const defaults = {
|
|
|
994
1027
|
nodeStrokeWidth: 1,
|
|
995
1028
|
edgeWidth: 1.5,
|
|
996
1029
|
edgeWidthRange: [1, 6],
|
|
997
|
-
edgeColor: themeColor("edge")
|
|
1030
|
+
edgeColor: themeColor("edge"),
|
|
1031
|
+
badgeFill: themeColor("badge-fill"),
|
|
1032
|
+
badgeInk: themeColor("badge-ink"),
|
|
1033
|
+
badgeRing: themeColor("node-ring")
|
|
998
1034
|
};
|
|
999
1035
|
const builtInStyles = {
|
|
1000
1036
|
nodes: {
|
|
@@ -1148,6 +1184,13 @@ function resolveNumberScale(options, fallback) {
|
|
|
1148
1184
|
};
|
|
1149
1185
|
}
|
|
1150
1186
|
// src/core/columns.ts
|
|
1187
|
+
const noColor = 4294967295;
|
|
1188
|
+
const cornerCodes = {
|
|
1189
|
+
topLeft: 0,
|
|
1190
|
+
topRight: 1,
|
|
1191
|
+
bottomLeft: 2,
|
|
1192
|
+
bottomRight: 3
|
|
1193
|
+
};
|
|
1151
1194
|
const shapeCodes = {
|
|
1152
1195
|
circle: 0,
|
|
1153
1196
|
square: 1,
|
|
@@ -1187,6 +1230,8 @@ var NodeColumns = class {
|
|
|
1187
1230
|
label = [];
|
|
1188
1231
|
secondary = [];
|
|
1189
1232
|
image = [];
|
|
1233
|
+
imageScale = /* @__PURE__ */ new Float32Array(0);
|
|
1234
|
+
imageColor = /* @__PURE__ */ new Uint32Array(0);
|
|
1190
1235
|
badges = [];
|
|
1191
1236
|
reserve(index) {
|
|
1192
1237
|
if (index < this.capacity) return;
|
|
@@ -1200,6 +1245,8 @@ var NodeColumns = class {
|
|
|
1200
1245
|
this.strokeWidth = grow$1(this.strokeWidth, capacity);
|
|
1201
1246
|
this.opacity = grow$1(this.opacity, capacity);
|
|
1202
1247
|
this.screenUnits = grow$1(this.screenUnits, capacity);
|
|
1248
|
+
this.imageScale = grow$1(this.imageScale, capacity);
|
|
1249
|
+
this.imageColor = grow$1(this.imageColor, capacity);
|
|
1203
1250
|
this.capacity = capacity;
|
|
1204
1251
|
}
|
|
1205
1252
|
clear(index) {
|
|
@@ -1349,8 +1396,8 @@ function createEdgeLanes() {
|
|
|
1349
1396
|
}
|
|
1350
1397
|
function link(index, source, target) {
|
|
1351
1398
|
reserve(index);
|
|
1352
|
-
const low = source
|
|
1353
|
-
const high = source
|
|
1399
|
+
const low = Math.min(source, target);
|
|
1400
|
+
const high = Math.max(source, target);
|
|
1354
1401
|
const current = slots[index] ?? -1;
|
|
1355
1402
|
if (current >= 0) {
|
|
1356
1403
|
if (lows[current] === low && highs[current] === high) return;
|
|
@@ -1468,18 +1515,37 @@ function finiteOr(value, fallback) {
|
|
|
1468
1515
|
function codeOf(codes, value) {
|
|
1469
1516
|
return typeof value === "string" && Object.hasOwn(codes, value) ? codes[value] : 0;
|
|
1470
1517
|
}
|
|
1471
|
-
function readBadges(mark, datum) {
|
|
1518
|
+
function readBadges(mark, datum, palette) {
|
|
1472
1519
|
if (mark.badges.length === 0) return null;
|
|
1473
|
-
const badges =
|
|
1474
|
-
let count = 0;
|
|
1520
|
+
const badges = [];
|
|
1475
1521
|
for (const [corner, read] of mark.badges) {
|
|
1476
1522
|
const badge = read(datum);
|
|
1477
|
-
if (typeof badge
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1523
|
+
if (typeof badge !== "object" || badge === null) continue;
|
|
1524
|
+
const { text, fill, color } = badge;
|
|
1525
|
+
badges.push({
|
|
1526
|
+
corner: cornerCodes[corner],
|
|
1527
|
+
text: typeof text === "number" ? String(text) : toText(text) || null,
|
|
1528
|
+
fill: palette.id(toColor(fill) ?? defaults.badgeFill),
|
|
1529
|
+
color: palette.id(toColor(color) ?? defaults.badgeInk),
|
|
1530
|
+
ring: palette.id(defaults.badgeRing)
|
|
1531
|
+
});
|
|
1481
1532
|
}
|
|
1482
|
-
return
|
|
1533
|
+
return badges.length === 0 ? null : badges;
|
|
1534
|
+
}
|
|
1535
|
+
function readImage(value) {
|
|
1536
|
+
if (typeof value === "string") return value === "" ? null : {
|
|
1537
|
+
src: value,
|
|
1538
|
+
scale: 1,
|
|
1539
|
+
color: null
|
|
1540
|
+
};
|
|
1541
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1542
|
+
const { src, scale, color } = value;
|
|
1543
|
+
if (typeof src !== "string" || src === "") return null;
|
|
1544
|
+
return {
|
|
1545
|
+
src,
|
|
1546
|
+
scale: Math.max(0, finiteOr(scale, 1)),
|
|
1547
|
+
color: toColor(color)
|
|
1548
|
+
};
|
|
1483
1549
|
}
|
|
1484
1550
|
function layersOf(marks) {
|
|
1485
1551
|
const slots = {
|
|
@@ -1569,8 +1635,11 @@ function createScene() {
|
|
|
1569
1635
|
nodes.screenUnits[index] = mark.screenUnits ? 1 : 0;
|
|
1570
1636
|
nodes.label[index] = toText(mark.label?.(datum));
|
|
1571
1637
|
nodes.secondary[index] = toText(mark.secondary?.(datum));
|
|
1572
|
-
|
|
1573
|
-
nodes.
|
|
1638
|
+
const image = readImage(mark.image?.(datum));
|
|
1639
|
+
nodes.image[index] = image?.src ?? null;
|
|
1640
|
+
nodes.imageScale[index] = image?.scale ?? 1;
|
|
1641
|
+
nodes.imageColor[index] = image?.color ? palette.id(image.color) : noColor;
|
|
1642
|
+
nodes.badges[index] = readBadges(mark, datum, palette);
|
|
1574
1643
|
dirtyNodes.add(index);
|
|
1575
1644
|
}
|
|
1576
1645
|
function writeEdge(model, record) {
|
|
@@ -1710,6 +1779,47 @@ function createScene() {
|
|
|
1710
1779
|
}
|
|
1711
1780
|
};
|
|
1712
1781
|
}
|
|
1782
|
+
// src/core/spatial.ts
|
|
1783
|
+
const sideWeight = 2;
|
|
1784
|
+
const cone = 2;
|
|
1785
|
+
function nearestNode(point, nodes, positions) {
|
|
1786
|
+
let best = -1;
|
|
1787
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1788
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
1789
|
+
if (nodes.alive[index] !== 1) continue;
|
|
1790
|
+
const distance = Math.hypot(positions.x(index) - point.x, positions.y(index) - point.y);
|
|
1791
|
+
if (distance < bestDistance) {
|
|
1792
|
+
best = index;
|
|
1793
|
+
bestDistance = distance;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return best;
|
|
1797
|
+
}
|
|
1798
|
+
function nodeToward(from, direction, nodes, positions) {
|
|
1799
|
+
const x = positions.x(from);
|
|
1800
|
+
const y = positions.y(from);
|
|
1801
|
+
const length = Math.hypot(direction.x, direction.y);
|
|
1802
|
+
if (length === 0) return -1;
|
|
1803
|
+
const dx = direction.x / length;
|
|
1804
|
+
const dy = direction.y / length;
|
|
1805
|
+
let best = -1;
|
|
1806
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1807
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
1808
|
+
if (index === from || nodes.alive[index] !== 1) continue;
|
|
1809
|
+
const vx = positions.x(index) - x;
|
|
1810
|
+
const vy = positions.y(index) - y;
|
|
1811
|
+
const along = vx * dx + vy * dy;
|
|
1812
|
+
if (along <= 0) continue;
|
|
1813
|
+
const side = Math.abs(vx * dy - vy * dx);
|
|
1814
|
+
if (side > along * cone) continue;
|
|
1815
|
+
const score = along + side * sideWeight;
|
|
1816
|
+
if (score < bestScore) {
|
|
1817
|
+
best = index;
|
|
1818
|
+
bestScore = score;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return best;
|
|
1822
|
+
}
|
|
1713
1823
|
const noPaint = 4294967295;
|
|
1714
1824
|
const tableOffset = {
|
|
1715
1825
|
nodes: 0,
|
|
@@ -2191,12 +2301,162 @@ var GlyphAtlas = class {
|
|
|
2191
2301
|
this.generation++;
|
|
2192
2302
|
}
|
|
2193
2303
|
};
|
|
2304
|
+
const levels = Math.log2(256) + 1;
|
|
2305
|
+
const maxLayers = 256;
|
|
2306
|
+
var ImageAtlas = class {
|
|
2307
|
+
#gl;
|
|
2308
|
+
#invalidate;
|
|
2309
|
+
#entries = /* @__PURE__ */ new Map();
|
|
2310
|
+
#free = [];
|
|
2311
|
+
#placeholder;
|
|
2312
|
+
#texture = null;
|
|
2313
|
+
#capacity = 0;
|
|
2314
|
+
#next = 0;
|
|
2315
|
+
#loaded = /* @__PURE__ */ new Set();
|
|
2316
|
+
#mipsStale = false;
|
|
2317
|
+
#canvas = null;
|
|
2318
|
+
#warnedFull = false;
|
|
2319
|
+
#destroyed = false;
|
|
2320
|
+
constructor(gl, invalidate) {
|
|
2321
|
+
this.#gl = gl;
|
|
2322
|
+
this.#invalidate = invalidate;
|
|
2323
|
+
this.#placeholder = gl.createTexture();
|
|
2324
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#placeholder);
|
|
2325
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, 1, 1, 1);
|
|
2326
|
+
}
|
|
2327
|
+
get texture() {
|
|
2328
|
+
return this.#texture ?? this.#placeholder;
|
|
2329
|
+
}
|
|
2330
|
+
layer(src) {
|
|
2331
|
+
return this.#entries.get(src)?.layer ?? -1;
|
|
2332
|
+
}
|
|
2333
|
+
acquire(src) {
|
|
2334
|
+
const entry = this.#entries.get(src);
|
|
2335
|
+
if (entry) {
|
|
2336
|
+
entry.count++;
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
this.#entries.set(src, {
|
|
2340
|
+
count: 1,
|
|
2341
|
+
layer: -1
|
|
2342
|
+
});
|
|
2343
|
+
const image = new Image();
|
|
2344
|
+
image.crossOrigin = "anonymous";
|
|
2345
|
+
image.decoding = "async";
|
|
2346
|
+
image.src = src;
|
|
2347
|
+
image.decode().then(() => this.#store(src, image), () => this.#fail(src));
|
|
2348
|
+
}
|
|
2349
|
+
release(src) {
|
|
2350
|
+
const entry = this.#entries.get(src);
|
|
2351
|
+
if (!entry || --entry.count > 0) return;
|
|
2352
|
+
this.#entries.delete(src);
|
|
2353
|
+
if (entry.layer >= 0) this.#free.push(entry.layer);
|
|
2354
|
+
}
|
|
2355
|
+
takeLoaded() {
|
|
2356
|
+
if (this.#loaded.size === 0) return null;
|
|
2357
|
+
const loaded = this.#loaded;
|
|
2358
|
+
this.#loaded = /* @__PURE__ */ new Set();
|
|
2359
|
+
return loaded;
|
|
2360
|
+
}
|
|
2361
|
+
prepare() {
|
|
2362
|
+
if (!this.#mipsStale || !this.#texture) return;
|
|
2363
|
+
this.#mipsStale = false;
|
|
2364
|
+
const gl = this.#gl;
|
|
2365
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#texture);
|
|
2366
|
+
gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
|
|
2367
|
+
}
|
|
2368
|
+
destroy() {
|
|
2369
|
+
this.#destroyed = true;
|
|
2370
|
+
this.#gl.deleteTexture(this.#placeholder);
|
|
2371
|
+
if (this.#texture) this.#gl.deleteTexture(this.#texture);
|
|
2372
|
+
}
|
|
2373
|
+
#fail(src) {
|
|
2374
|
+
if (this.#destroyed) return;
|
|
2375
|
+
if (dev && this.#entries.has(src)) console.warn(`graph: cannot load the image ${src}`);
|
|
2376
|
+
}
|
|
2377
|
+
#store(src, image) {
|
|
2378
|
+
const entry = this.#entries.get(src);
|
|
2379
|
+
if (this.#destroyed || !entry || entry.layer >= 0) return;
|
|
2380
|
+
const layer = this.#free.pop() ?? this.#next;
|
|
2381
|
+
if (layer >= this.#capacity && !this.#grow()) {
|
|
2382
|
+
if (dev && !this.#warnedFull) {
|
|
2383
|
+
this.#warnedFull = true;
|
|
2384
|
+
console.warn(`graph: more than ${maxLayers} distinct images, the others are not drawn`);
|
|
2385
|
+
}
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
if (layer === this.#next) this.#next++;
|
|
2389
|
+
const canvas = this.#draw(image);
|
|
2390
|
+
const gl = this.#gl;
|
|
2391
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#texture);
|
|
2392
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
2393
|
+
try {
|
|
2394
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, 256, 256, 1, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
|
2395
|
+
} catch {
|
|
2396
|
+
this.#free.push(layer);
|
|
2397
|
+
this.#fail(src);
|
|
2398
|
+
return;
|
|
2399
|
+
} finally {
|
|
2400
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
2401
|
+
}
|
|
2402
|
+
entry.layer = layer;
|
|
2403
|
+
this.#mipsStale = true;
|
|
2404
|
+
this.#loaded.add(src);
|
|
2405
|
+
this.#invalidate();
|
|
2406
|
+
}
|
|
2407
|
+
#draw(image) {
|
|
2408
|
+
this.#canvas ??= Object.assign(document.createElement("canvas"), {
|
|
2409
|
+
width: 256,
|
|
2410
|
+
height: 256
|
|
2411
|
+
});
|
|
2412
|
+
const context = this.#canvas.getContext("2d");
|
|
2413
|
+
if (context) {
|
|
2414
|
+
context.clearRect(0, 0, 256, 256);
|
|
2415
|
+
const width = image.naturalWidth;
|
|
2416
|
+
const height = image.naturalHeight;
|
|
2417
|
+
const scale = width > 0 && height > 0 ? 256 / Math.min(width, height) : 0;
|
|
2418
|
+
const drawnWidth = scale > 0 ? width * scale : 256;
|
|
2419
|
+
const drawnHeight = scale > 0 ? height * scale : 256;
|
|
2420
|
+
context.drawImage(image, (256 - drawnWidth) / 2, (256 - drawnHeight) / 2, drawnWidth, drawnHeight);
|
|
2421
|
+
}
|
|
2422
|
+
return this.#canvas;
|
|
2423
|
+
}
|
|
2424
|
+
#grow() {
|
|
2425
|
+
if (this.#capacity >= maxLayers) return false;
|
|
2426
|
+
const gl = this.#gl;
|
|
2427
|
+
const capacity = Math.min(maxLayers, Math.max(4, this.#capacity * 2));
|
|
2428
|
+
const texture = gl.createTexture();
|
|
2429
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
|
|
2430
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, levels, gl.RGBA8, 256, 256, capacity);
|
|
2431
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
2432
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
2433
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2434
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2435
|
+
const previous = this.#texture;
|
|
2436
|
+
if (previous) {
|
|
2437
|
+
const framebuffer = gl.createFramebuffer();
|
|
2438
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer);
|
|
2439
|
+
for (let layer = 0; layer < this.#capacity; layer++) {
|
|
2440
|
+
gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, previous, 0, layer);
|
|
2441
|
+
gl.copyTexSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, 0, 0, 256, 256);
|
|
2442
|
+
}
|
|
2443
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
2444
|
+
gl.deleteFramebuffer(framebuffer);
|
|
2445
|
+
gl.deleteTexture(previous);
|
|
2446
|
+
}
|
|
2447
|
+
this.#texture = texture;
|
|
2448
|
+
this.#capacity = capacity;
|
|
2449
|
+
this.#mipsStale = true;
|
|
2450
|
+
return true;
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2194
2453
|
const primarySize = 12;
|
|
2195
2454
|
const lineHeight = 1.25;
|
|
2196
2455
|
const labelMin = 8;
|
|
2197
2456
|
const cell = 8;
|
|
2198
2457
|
const maxLabels = 4e3;
|
|
2199
2458
|
const cullMargin = 400;
|
|
2459
|
+
const forcedStates = 6;
|
|
2200
2460
|
var GlyphBuffer = class {
|
|
2201
2461
|
data = /* @__PURE__ */ new ArrayBuffer(8192);
|
|
2202
2462
|
count = 0;
|
|
@@ -2259,7 +2519,9 @@ var LabelLayer = class {
|
|
|
2259
2519
|
const highlighted = new Set(frame.highlighted);
|
|
2260
2520
|
const worldPrimary = primarySize * Math.min(1, view.zoom);
|
|
2261
2521
|
let placed = 0;
|
|
2262
|
-
const
|
|
2522
|
+
const forced = (index) => ((frame.nodeMask[index] ?? 0) & forcedStates) !== 0;
|
|
2523
|
+
const place = (index, highlight) => {
|
|
2524
|
+
const force = highlight && forced(index);
|
|
2263
2525
|
const label = nodes.label[index];
|
|
2264
2526
|
if (!label || nodes.alive[index] !== 1) return;
|
|
2265
2527
|
if (!labelVisible(frame.nodeMask[index] ?? 0, frame.labelFlags)) return;
|
|
@@ -2290,12 +2552,13 @@ var LabelLayer = class {
|
|
|
2290
2552
|
for (let row = r0; row <= r1; row++) for (let col = c0; col <= c1; col++) if (occupancy[row * cols + col] === 1) return;
|
|
2291
2553
|
}
|
|
2292
2554
|
for (let row = r0; row <= r1; row++) occupancy.fill(1, row * cols + c0, row * cols + c1 + 1);
|
|
2293
|
-
const target =
|
|
2555
|
+
const target = highlight ? this.highlight : this.main;
|
|
2294
2556
|
target.push(index, 0, line, atlas.height);
|
|
2295
2557
|
if (secondary) target.push(index, 1, secondary, atlas.height);
|
|
2296
2558
|
placed++;
|
|
2297
2559
|
};
|
|
2298
|
-
for (const index of highlighted) place(index, true);
|
|
2560
|
+
for (const index of highlighted) if (forced(index)) place(index, true);
|
|
2561
|
+
for (const index of highlighted) if (!forced(index)) place(index, true);
|
|
2299
2562
|
if (worldPrimary < labelMin && !frame.screenLabels) return;
|
|
2300
2563
|
const xy = positions.xy;
|
|
2301
2564
|
const order = frame.order;
|
|
@@ -2328,12 +2591,14 @@ function sortBySize(nodes, order) {
|
|
|
2328
2591
|
screenLabels
|
|
2329
2592
|
};
|
|
2330
2593
|
}
|
|
2331
|
-
|
|
2594
|
+
const badgeFont = .66;
|
|
2595
|
+
const badgeRing = 1.5;
|
|
2332
2596
|
const header = `#version 300 es
|
|
2333
2597
|
precision highp float;
|
|
2334
2598
|
precision highp int;
|
|
2335
2599
|
precision highp sampler2D;
|
|
2336
2600
|
precision highp usampler2D;
|
|
2601
|
+
precision highp sampler2DArray;
|
|
2337
2602
|
layout(std140) uniform Frame {
|
|
2338
2603
|
vec2 center;
|
|
2339
2604
|
vec2 viewport;
|
|
@@ -2353,7 +2618,7 @@ layout(std140) uniform States {
|
|
|
2353
2618
|
uniform int layerSlot;
|
|
2354
2619
|
uniform int overlay;
|
|
2355
2620
|
uniform sampler2D palette;
|
|
2356
|
-
const uint highlightMask =
|
|
2621
|
+
const uint highlightMask = 7u;
|
|
2357
2622
|
const uint none = 0xffffffffu;
|
|
2358
2623
|
const vec4 hidden = vec4(2.0, 2.0, 2.0, 1.0);
|
|
2359
2624
|
vec2 toScreen(vec2 world) { return (world - center) * zoom + viewport * 0.5; }
|
|
@@ -2398,6 +2663,13 @@ float nodeDiameter(vec4 style) {
|
|
|
2398
2663
|
return (flags & 4u) != 0u ? style.x : style.x * zoom;
|
|
2399
2664
|
}
|
|
2400
2665
|
float nodeRadius(vec4 style) { return max(nodeDiameter(style) * 0.5, 1.25); }
|
|
2666
|
+
float badgeHeight(vec4 style) { return min(nodeDiameter(style) * 0.55, ${18 .toFixed(1)}); }
|
|
2667
|
+
vec2 badgeCenter(vec2 xy, vec4 style, uint corner) {
|
|
2668
|
+
uint shape = uint(style.w + 0.5) & 3u;
|
|
2669
|
+
float reach = shape == 0u ? 0.70710678 : shape == 1u ? 0.86 : 0.6;
|
|
2670
|
+
vec2 side = vec2((corner & 1u) != 0u ? 1.0 : -1.0, (corner & 2u) != 0u ? 1.0 : -1.0);
|
|
2671
|
+
return toScreen(xy) + side * reach * nodeRadius(style);
|
|
2672
|
+
}
|
|
2401
2673
|
`;
|
|
2402
2674
|
const edgeData = `
|
|
2403
2675
|
uniform usampler2D edgeTopology;
|
|
@@ -2405,9 +2677,13 @@ uniform sampler2D edgeStyle;
|
|
|
2405
2677
|
uniform usampler2D edgeState;
|
|
2406
2678
|
`;
|
|
2407
2679
|
const nodeVertex = `${header}${nodeData}
|
|
2680
|
+
uniform usampler2D nodeImage;
|
|
2408
2681
|
in vec2 corner;
|
|
2409
2682
|
in uint instance;
|
|
2410
2683
|
out vec2 vLocal;
|
|
2684
|
+
flat out float vImageLayer;
|
|
2685
|
+
flat out float vImageScale;
|
|
2686
|
+
flat out vec4 vImageColor;
|
|
2411
2687
|
out float vRadius;
|
|
2412
2688
|
out float vStrokeWidth;
|
|
2413
2689
|
out float vHaloWidth;
|
|
@@ -2438,11 +2714,19 @@ void main() {
|
|
|
2438
2714
|
vFill = paletteColor(look.fill);
|
|
2439
2715
|
vStroke = paletteColor(look.stroke);
|
|
2440
2716
|
vOpacity = look.opacity;
|
|
2717
|
+
uvec4 image = texelFetch(nodeImage, t, 0);
|
|
2718
|
+
vImageLayer = float(image.x) - 1.0;
|
|
2719
|
+
vImageScale = max(uintBitsToFloat(image.z), 0.001);
|
|
2720
|
+
vImageColor = image.y == none ? vec4(-1.0) : paletteColor(image.y);
|
|
2441
2721
|
gl_Position = toClip(toScreen(xy) + vLocal);
|
|
2442
2722
|
}
|
|
2443
2723
|
`;
|
|
2444
2724
|
const nodeFragment = `${header}
|
|
2725
|
+
uniform sampler2DArray images;
|
|
2445
2726
|
in vec2 vLocal;
|
|
2727
|
+
flat in float vImageLayer;
|
|
2728
|
+
flat in float vImageScale;
|
|
2729
|
+
flat in vec4 vImageColor;
|
|
2446
2730
|
in float vRadius;
|
|
2447
2731
|
in float vStrokeWidth;
|
|
2448
2732
|
in float vHaloWidth;
|
|
@@ -2464,9 +2748,17 @@ void main() {
|
|
|
2464
2748
|
float d = shapeDistance(vLocal, vRadius);
|
|
2465
2749
|
float inside = 1.0 - smoothstep(-aa, aa, d);
|
|
2466
2750
|
float ring = vStrokeWidth > 0.0 ? smoothstep(-vStrokeWidth - aa, -vStrokeWidth + aa, d) : 0.0;
|
|
2467
|
-
vec4
|
|
2468
|
-
|
|
2469
|
-
|
|
2751
|
+
vec4 fill = vec4(vFill.rgb * vFill.a, vFill.a);
|
|
2752
|
+
if (vImageLayer >= 0.0) {
|
|
2753
|
+
vec2 uv = vLocal / (vRadius * 2.0 * vImageScale) + 0.5;
|
|
2754
|
+
vec4 picture = texture(images, vec3(uv, vImageLayer));
|
|
2755
|
+
vec2 within = step(vec2(0.0), uv) * step(uv, vec2(1.0));
|
|
2756
|
+
picture *= within.x * within.y;
|
|
2757
|
+
if (vImageColor.a >= 0.0) picture = vec4(vImageColor.rgb * vImageColor.a, vImageColor.a) * picture.a;
|
|
2758
|
+
fill = picture + fill * (1.0 - picture.a);
|
|
2759
|
+
}
|
|
2760
|
+
vec4 color = mix(fill, vec4(vStroke.rgb * vStroke.a, vStroke.a), ring);
|
|
2761
|
+
vec4 result = color * inside * vOpacity;
|
|
2470
2762
|
if (vHaloWidth > 0.0) {
|
|
2471
2763
|
float h = haloColor.a * (1.0 - smoothstep(0.0, vHaloWidth, d)) * vOpacity;
|
|
2472
2764
|
result += vec4(haloColor.rgb * h, h) * (1.0 - result.a);
|
|
@@ -2749,7 +3041,8 @@ in vec4 glyphRect;
|
|
|
2749
3041
|
in vec4 glyphUv;
|
|
2750
3042
|
out vec2 vUv;
|
|
2751
3043
|
out float vScale;
|
|
2752
|
-
flat out
|
|
3044
|
+
flat out vec4 vInk;
|
|
3045
|
+
flat out float vHalo;
|
|
2753
3046
|
flat out float vOpacity;
|
|
2754
3047
|
void main() {
|
|
2755
3048
|
ivec2 t = texel(glyphNode);
|
|
@@ -2758,19 +3051,36 @@ void main() {
|
|
|
2758
3051
|
uint flags = uint(style.w + 0.5);
|
|
2759
3052
|
uint mask = texelFetch(nodeState, t, 0).r;
|
|
2760
3053
|
Look look = resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y), 0);
|
|
3054
|
+
vUv = mix(glyphUv.xy, glyphUv.zw, corner);
|
|
3055
|
+
vOpacity = look.opacity;
|
|
3056
|
+
uint kind = glyphKind & 3u;
|
|
3057
|
+
if (kind == 2u) {
|
|
3058
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
3059
|
+
float height = badgeHeight(style);
|
|
3060
|
+
if ((flags & 8u) == 0u || highlighted != (overlay == 1) || height < ${12 .toFixed(1)}) {
|
|
3061
|
+
gl_Position = hidden;
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
float font = height * ${badgeFont.toFixed(2)};
|
|
3065
|
+
vec2 center = badgeCenter(texelFetch(nodePosition, t, 0).xy, style, (glyphKind >> 2u) & 3u);
|
|
3066
|
+
vScale = font / atlasFontSize;
|
|
3067
|
+
vInk = paletteColor(glyphKind >> 4u);
|
|
3068
|
+
vHalo = 0.0;
|
|
3069
|
+
gl_Position = toClip(center - vec2(0.0, font * 0.65) + (glyphRect.xy + corner * glyphRect.zw) * vScale);
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
2761
3072
|
float primary = (flags & 4u) != 0u ? 12.0 : 12.0 * min(1.0, zoom);
|
|
2762
3073
|
if ((flags & 8u) == 0u || look.label < 0.5 || primary < labelMin) {
|
|
2763
3074
|
gl_Position = hidden;
|
|
2764
3075
|
return;
|
|
2765
3076
|
}
|
|
2766
|
-
float size =
|
|
3077
|
+
float size = kind == 0u ? primary : primary * 11.0 / 12.0;
|
|
2767
3078
|
float scale = size / atlasFontSize;
|
|
2768
|
-
float below = nodeRadius(style) + 3.0 + (
|
|
3079
|
+
float below = nodeRadius(style) + 3.0 + (kind == 1u ? primary * 1.25 : 0.0);
|
|
2769
3080
|
vec2 anchor = toScreen(texelFetch(nodePosition, t, 0).xy) + vec2(0.0, below);
|
|
2770
|
-
vUv = mix(glyphUv.xy, glyphUv.zw, corner);
|
|
2771
3081
|
vScale = scale;
|
|
2772
|
-
|
|
2773
|
-
|
|
3082
|
+
vInk = kind == 0u ? labelInk : labelMuted;
|
|
3083
|
+
vHalo = 1.0;
|
|
2774
3084
|
gl_Position = toClip(anchor + (glyphRect.xy + corner * glyphRect.zw) * scale);
|
|
2775
3085
|
}
|
|
2776
3086
|
`;
|
|
@@ -2778,21 +3088,78 @@ const labelFragment = `${header}
|
|
|
2778
3088
|
uniform sampler2D atlas;
|
|
2779
3089
|
in vec2 vUv;
|
|
2780
3090
|
in float vScale;
|
|
2781
|
-
flat in
|
|
3091
|
+
flat in vec4 vInk;
|
|
3092
|
+
flat in float vHalo;
|
|
2782
3093
|
flat in float vOpacity;
|
|
2783
3094
|
out vec4 fragColor;
|
|
2784
3095
|
void main() {
|
|
2785
3096
|
float d = texture(atlas, vUv).r;
|
|
2786
3097
|
float gamma = 0.08 / max(vScale * pixelRatio, 0.05);
|
|
2787
3098
|
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;
|
|
3099
|
+
float halo = smoothstep(0.55 - gamma, 0.55 + gamma, d) * vHalo;
|
|
3100
|
+
vec3 rgb = vHalo > 0.5 ? mix(labelHalo.rgb, vInk.rgb, ink) : vInk.rgb;
|
|
3101
|
+
float alpha = max(halo * labelHalo.a, ink * vInk.a) * vOpacity;
|
|
2792
3102
|
if (alpha < 0.002) discard;
|
|
2793
3103
|
fragColor = vec4(rgb * alpha, alpha);
|
|
2794
3104
|
}
|
|
2795
3105
|
`;
|
|
3106
|
+
const badgeVertex = `${header}${nodeData}
|
|
3107
|
+
in vec2 corner;
|
|
3108
|
+
in uint badgeNode;
|
|
3109
|
+
in uint badgeInfo;
|
|
3110
|
+
in uint badgeFill;
|
|
3111
|
+
in uint badgeRingColor;
|
|
3112
|
+
in float badgeText;
|
|
3113
|
+
out vec2 vLocal;
|
|
3114
|
+
flat out vec2 vHalf;
|
|
3115
|
+
flat out vec4 vFill;
|
|
3116
|
+
flat out vec4 vRing;
|
|
3117
|
+
flat out float vOpacity;
|
|
3118
|
+
void main() {
|
|
3119
|
+
ivec2 t = texel(badgeNode);
|
|
3120
|
+
vec4 style = texelFetch(nodeStyle, t, 0);
|
|
3121
|
+
uvec4 paint = texelFetch(nodePaint, t, 0);
|
|
3122
|
+
uint flags = uint(style.w + 0.5);
|
|
3123
|
+
uint mask = texelFetch(nodeState, t, 0).r;
|
|
3124
|
+
bool highlighted = (mask & highlightMask) != 0u;
|
|
3125
|
+
float height = badgeHeight(style);
|
|
3126
|
+
if ((flags & 8u) == 0u || highlighted != (overlay == 1) || height < ${10 .toFixed(1)}) {
|
|
3127
|
+
gl_Position = hidden;
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
Look look = resolve(mask, Look(style.z, style.y, 0.0, 1.0, paint.x, paint.y), 0);
|
|
3131
|
+
bool text = (badgeInfo & 4u) != 0u && height >= ${12 .toFixed(1)};
|
|
3132
|
+
float font = height * ${badgeFont.toFixed(2)};
|
|
3133
|
+
float width = text ? max(height, badgeText * font / ${24 .toFixed(1)} + height * 0.6) : height;
|
|
3134
|
+
vec2 center = badgeCenter(texelFetch(nodePosition, t, 0).xy, style, badgeInfo & 3u);
|
|
3135
|
+
vHalf = vec2(width, height) * 0.5;
|
|
3136
|
+
vLocal = corner * (vHalf + ${badgeRing.toFixed(1)} + 1.0);
|
|
3137
|
+
vFill = paletteColor(badgeFill);
|
|
3138
|
+
vRing = paletteColor(badgeRingColor);
|
|
3139
|
+
vOpacity = look.opacity;
|
|
3140
|
+
gl_Position = toClip(center + vLocal);
|
|
3141
|
+
}
|
|
3142
|
+
`;
|
|
3143
|
+
const badgeFragment = `${header}
|
|
3144
|
+
in vec2 vLocal;
|
|
3145
|
+
flat in vec2 vHalf;
|
|
3146
|
+
flat in vec4 vFill;
|
|
3147
|
+
flat in vec4 vRing;
|
|
3148
|
+
flat in float vOpacity;
|
|
3149
|
+
out vec4 fragColor;
|
|
3150
|
+
void main() {
|
|
3151
|
+
float aa = 0.5 / pixelRatio + 0.25;
|
|
3152
|
+
vec2 q = vec2(max(abs(vLocal.x) - (vHalf.x - vHalf.y), 0.0), vLocal.y);
|
|
3153
|
+
float d = length(q) - vHalf.y;
|
|
3154
|
+
float inner = 1.0 - smoothstep(-aa, aa, d);
|
|
3155
|
+
float outer = 1.0 - smoothstep(${badgeRing.toFixed(1)} - aa, ${badgeRing.toFixed(1)} + aa, d);
|
|
3156
|
+
vec4 fill = vec4(vFill.rgb * vFill.a, vFill.a);
|
|
3157
|
+
vec4 ring = vec4(vRing.rgb * vRing.a, vRing.a);
|
|
3158
|
+
vec4 result = mix(ring, fill, inner) * outer * vOpacity;
|
|
3159
|
+
if (result.a < 0.002) discard;
|
|
3160
|
+
fragColor = result;
|
|
3161
|
+
}
|
|
3162
|
+
`;
|
|
2796
3163
|
const cornerNode = new Float32Array([
|
|
2797
3164
|
-1,
|
|
2798
3165
|
-1,
|
|
@@ -2879,6 +3246,7 @@ const blocks = {
|
|
|
2879
3246
|
};
|
|
2880
3247
|
const paletteWidth = 256;
|
|
2881
3248
|
const labelInterval = 120;
|
|
3249
|
+
const badgeStride = 20;
|
|
2882
3250
|
const units = {
|
|
2883
3251
|
palette: 0,
|
|
2884
3252
|
nodePosition: 1,
|
|
@@ -2888,7 +3256,9 @@ const units = {
|
|
|
2888
3256
|
edgeTopology: 5,
|
|
2889
3257
|
edgeStyle: 6,
|
|
2890
3258
|
edgeState: 7,
|
|
2891
|
-
atlas: 8
|
|
3259
|
+
atlas: 8,
|
|
3260
|
+
images: 9,
|
|
3261
|
+
nodeImage: 10
|
|
2892
3262
|
};
|
|
2893
3263
|
function iota(length) {
|
|
2894
3264
|
const values = new Uint32Array(length);
|
|
@@ -2898,7 +3268,7 @@ function iota(length) {
|
|
|
2898
3268
|
function rgba(color) {
|
|
2899
3269
|
return color.map((channel) => channel / 255);
|
|
2900
3270
|
}
|
|
2901
|
-
function createWebglRenderer(canvas) {
|
|
3271
|
+
function createWebglRenderer(canvas, invalidate = () => {}) {
|
|
2902
3272
|
const context = canvas.getContext("webgl2", {
|
|
2903
3273
|
alpha: true,
|
|
2904
3274
|
antialias: false,
|
|
@@ -2915,6 +3285,7 @@ function createWebglRenderer(canvas) {
|
|
|
2915
3285
|
const nodeStyle = new DataTexture(gl, format.rgba32f, f32);
|
|
2916
3286
|
const nodePaint = new DataTexture(gl, format.rgba32ui, u32);
|
|
2917
3287
|
const nodeState = new DataTexture(gl, format.r32ui, u32);
|
|
3288
|
+
const nodeImage = new DataTexture(gl, format.rgba32ui, u32);
|
|
2918
3289
|
const edgeTopology = new DataTexture(gl, format.rgba32ui, u32);
|
|
2919
3290
|
const edgeStyle = new DataTexture(gl, format.rgba32f, f32);
|
|
2920
3291
|
const edgeState = new DataTexture(gl, format.r32ui, u32);
|
|
@@ -2922,8 +3293,12 @@ function createWebglRenderer(canvas) {
|
|
|
2922
3293
|
nodePosition,
|
|
2923
3294
|
nodeStyle,
|
|
2924
3295
|
nodePaint,
|
|
2925
|
-
nodeState
|
|
3296
|
+
nodeState,
|
|
3297
|
+
nodeImage
|
|
2926
3298
|
];
|
|
3299
|
+
const images = new ImageAtlas(gl, invalidate);
|
|
3300
|
+
const scaleBits = /* @__PURE__ */ new Float32Array(1);
|
|
3301
|
+
const scaleWord = new Uint32Array(scaleBits.buffer);
|
|
2927
3302
|
const edgeTextures = [
|
|
2928
3303
|
edgeTopology,
|
|
2929
3304
|
edgeStyle,
|
|
@@ -2957,10 +3332,12 @@ function createWebglRenderer(canvas) {
|
|
|
2957
3332
|
const nodeProgram = createProgram(gl, nodeVertex, nodeFragment, blocks);
|
|
2958
3333
|
const edgeProgram = createProgram(gl, edgeVertex, edgeFragment, blocks);
|
|
2959
3334
|
const labelProgram = createProgram(gl, labelVertex, labelFragment, blocks);
|
|
3335
|
+
const badgeProgram = createProgram(gl, badgeVertex, badgeFragment, blocks);
|
|
2960
3336
|
const programs = [
|
|
2961
3337
|
nodeProgram,
|
|
2962
3338
|
edgeProgram,
|
|
2963
|
-
labelProgram
|
|
3339
|
+
labelProgram,
|
|
3340
|
+
badgeProgram
|
|
2964
3341
|
];
|
|
2965
3342
|
const locations = /* @__PURE__ */ new Map();
|
|
2966
3343
|
function prepare(program) {
|
|
@@ -2988,8 +3365,11 @@ function createWebglRenderer(canvas) {
|
|
|
2988
3365
|
const curveIndices = createBuffer(gl, /* @__PURE__ */ new Uint32Array(0), gl.DYNAMIC_DRAW);
|
|
2989
3366
|
const labelMainBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
2990
3367
|
const labelHighlightBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
3368
|
+
const badgeBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
3369
|
+
const badgeGlyphBuffer = createBuffer(gl, /* @__PURE__ */ new Uint8Array(0), gl.DYNAMIC_DRAW);
|
|
2991
3370
|
const warmIndex = createBuffer(gl, /* @__PURE__ */ new Uint32Array(1), gl.STATIC_DRAW);
|
|
2992
3371
|
const warmGlyph = createBuffer(gl, /* @__PURE__ */ new Uint8Array(32), gl.STATIC_DRAW);
|
|
3372
|
+
const warmBadge = createBuffer(gl, new Uint8Array(badgeStride), gl.STATIC_DRAW);
|
|
2993
3373
|
const buffers = [
|
|
2994
3374
|
nodeCorner,
|
|
2995
3375
|
edgeCorner,
|
|
@@ -3002,8 +3382,11 @@ function createWebglRenderer(canvas) {
|
|
|
3002
3382
|
curveIndices,
|
|
3003
3383
|
labelMainBuffer,
|
|
3004
3384
|
labelHighlightBuffer,
|
|
3385
|
+
badgeBuffer,
|
|
3386
|
+
badgeGlyphBuffer,
|
|
3005
3387
|
warmIndex,
|
|
3006
|
-
warmGlyph
|
|
3388
|
+
warmGlyph,
|
|
3389
|
+
warmBadge
|
|
3007
3390
|
];
|
|
3008
3391
|
const corner = (buffer, size = 2) => ({
|
|
3009
3392
|
name: "corner",
|
|
@@ -3062,18 +3445,39 @@ function createWebglRenderer(canvas) {
|
|
|
3062
3445
|
offset: 24
|
|
3063
3446
|
}
|
|
3064
3447
|
];
|
|
3448
|
+
const badgeField = (buffer, name, offset, type = gl.UNSIGNED_INT) => ({
|
|
3449
|
+
name,
|
|
3450
|
+
buffer,
|
|
3451
|
+
size: 1,
|
|
3452
|
+
type,
|
|
3453
|
+
integer: type === gl.UNSIGNED_INT,
|
|
3454
|
+
divisor: 1,
|
|
3455
|
+
stride: badgeStride,
|
|
3456
|
+
offset
|
|
3457
|
+
});
|
|
3458
|
+
const badgeAttributes = (buffer) => [
|
|
3459
|
+
corner(nodeCorner),
|
|
3460
|
+
badgeField(buffer, "badgeNode", 0),
|
|
3461
|
+
badgeField(buffer, "badgeInfo", 4),
|
|
3462
|
+
badgeField(buffer, "badgeFill", 8),
|
|
3463
|
+
badgeField(buffer, "badgeRingColor", 12),
|
|
3464
|
+
badgeField(buffer, "badgeText", 16, gl.FLOAT)
|
|
3465
|
+
];
|
|
3065
3466
|
const vertexArrays = {
|
|
3066
3467
|
nodes: createVertexArray(gl, nodeProgram, [corner(nodeCorner), instance(nodeIndices)]),
|
|
3067
3468
|
nodeOverlay: createVertexArray(gl, nodeProgram, [corner(nodeCorner), instance(nodeOverlayIndices)]),
|
|
3068
3469
|
edges: createVertexArray(gl, edgeProgram, [corner(edgeCorner, 3), instance(edgeIndices)]),
|
|
3069
3470
|
edgeOverlay: createVertexArray(gl, edgeProgram, [corner(edgeCorner, 3), instance(edgeOverlayIndices)]),
|
|
3070
3471
|
labels: createVertexArray(gl, labelProgram, glyphs(labelMainBuffer)),
|
|
3071
|
-
labelHighlight: createVertexArray(gl, labelProgram, glyphs(labelHighlightBuffer))
|
|
3472
|
+
labelHighlight: createVertexArray(gl, labelProgram, glyphs(labelHighlightBuffer)),
|
|
3473
|
+
badges: createVertexArray(gl, badgeProgram, badgeAttributes(badgeBuffer)),
|
|
3474
|
+
badgeGlyphs: createVertexArray(gl, labelProgram, glyphs(badgeGlyphBuffer))
|
|
3072
3475
|
};
|
|
3073
3476
|
const vertexCounts = /* @__PURE__ */ new Map([
|
|
3074
3477
|
[nodeProgram, cornerNode.length / 2],
|
|
3075
3478
|
[edgeProgram, cornerEdge.length / 3],
|
|
3076
|
-
[labelProgram, cornerLabel.length / 2]
|
|
3479
|
+
[labelProgram, cornerLabel.length / 2],
|
|
3480
|
+
[badgeProgram, cornerNode.length / 2]
|
|
3077
3481
|
]);
|
|
3078
3482
|
let warmed = false;
|
|
3079
3483
|
let curvePass = null;
|
|
@@ -3107,6 +3511,13 @@ function createWebglRenderer(canvas) {
|
|
|
3107
3511
|
let orderStale = true;
|
|
3108
3512
|
let lastLabelTime = Number.NEGATIVE_INFINITY;
|
|
3109
3513
|
let lastLabelView = "";
|
|
3514
|
+
const imageOf = [];
|
|
3515
|
+
const badged = /* @__PURE__ */ new Set();
|
|
3516
|
+
const badgeGlyphs = new GlyphBuffer();
|
|
3517
|
+
let badgeData = /* @__PURE__ */ new ArrayBuffer(1280);
|
|
3518
|
+
let badgeCount = 0;
|
|
3519
|
+
let badgesStale = false;
|
|
3520
|
+
let badgeGeneration = -1;
|
|
3110
3521
|
function packNode(nodes, index) {
|
|
3111
3522
|
const at = index * 4;
|
|
3112
3523
|
const flags = (nodes.shape[index] ?? 0) | (nodes.screenUnits[index] ?? 0) << 2 | (nodes.alive[index] ?? 0) << 3;
|
|
@@ -3117,6 +3528,73 @@ function createWebglRenderer(canvas) {
|
|
|
3117
3528
|
nodePaint.data[at] = nodes.fill[index] ?? 0;
|
|
3118
3529
|
nodePaint.data[at + 1] = nodes.stroke[index] ?? 0;
|
|
3119
3530
|
nodePaint.data[at + 2] = nodes.slot[index] ?? 0;
|
|
3531
|
+
const src = nodes.alive[index] === 1 ? nodes.image[index] ?? null : null;
|
|
3532
|
+
const previous = imageOf[index] ?? null;
|
|
3533
|
+
if (src !== previous) {
|
|
3534
|
+
if (previous) images.release(previous);
|
|
3535
|
+
if (src) images.acquire(src);
|
|
3536
|
+
imageOf[index] = src;
|
|
3537
|
+
}
|
|
3538
|
+
scaleBits[0] = nodes.imageScale[index] ?? 1;
|
|
3539
|
+
nodeImage.data[at] = src ? images.layer(src) + 1 : 0;
|
|
3540
|
+
nodeImage.data[at + 1] = nodes.imageColor[index] ?? 4294967295;
|
|
3541
|
+
nodeImage.data[at + 2] = scaleWord[0] ?? 0;
|
|
3542
|
+
const badges = nodes.alive[index] === 1 ? nodes.badges[index] : null;
|
|
3543
|
+
if (badges || badged.has(index)) badgesStale = true;
|
|
3544
|
+
if (badges) badged.add(index);
|
|
3545
|
+
else badged.delete(index);
|
|
3546
|
+
}
|
|
3547
|
+
function refreshImages(nodes) {
|
|
3548
|
+
const loaded = images.takeLoaded();
|
|
3549
|
+
if (!loaded) return;
|
|
3550
|
+
let first = Number.POSITIVE_INFINITY;
|
|
3551
|
+
let last = -1;
|
|
3552
|
+
for (let index = 0; index < nodes.capacity; index++) {
|
|
3553
|
+
const src = imageOf[index];
|
|
3554
|
+
if (!src || !loaded.has(src)) continue;
|
|
3555
|
+
nodeImage.data[index * 4] = images.layer(src) + 1;
|
|
3556
|
+
first = Math.min(first, index);
|
|
3557
|
+
last = Math.max(last, index);
|
|
3558
|
+
}
|
|
3559
|
+
nodeImage.upload(first, last);
|
|
3560
|
+
}
|
|
3561
|
+
function rebuildBadges() {
|
|
3562
|
+
if (!input) return;
|
|
3563
|
+
const nodes = input.scene.nodes;
|
|
3564
|
+
const generation = atlas.generation;
|
|
3565
|
+
badgeGlyphs.clear();
|
|
3566
|
+
badgeCount = 0;
|
|
3567
|
+
let words = new Uint32Array(badgeData);
|
|
3568
|
+
let floats = new Float32Array(badgeData);
|
|
3569
|
+
for (const index of badged) for (const badge of nodes.badges[index] ?? []) {
|
|
3570
|
+
if ((badgeCount + 1) * badgeStride > badgeData.byteLength) {
|
|
3571
|
+
const grown = /* @__PURE__ */ new ArrayBuffer(badgeData.byteLength * 2);
|
|
3572
|
+
new Uint8Array(grown).set(new Uint8Array(badgeData));
|
|
3573
|
+
badgeData = grown;
|
|
3574
|
+
words = new Uint32Array(badgeData);
|
|
3575
|
+
floats = new Float32Array(badgeData);
|
|
3576
|
+
}
|
|
3577
|
+
const line = badge.text ? atlas.line(badge.text, "500") : null;
|
|
3578
|
+
const at = badgeCount * (badgeStride / 4);
|
|
3579
|
+
words[at] = index;
|
|
3580
|
+
words[at + 1] = badge.corner | (line ? 4 : 0);
|
|
3581
|
+
words[at + 2] = badge.fill;
|
|
3582
|
+
words[at + 3] = badge.ring;
|
|
3583
|
+
floats[at + 4] = line?.width ?? 0;
|
|
3584
|
+
if (line) badgeGlyphs.push(index, 2 | badge.corner << 2 | badge.color << 4, line, atlas.height);
|
|
3585
|
+
badgeCount++;
|
|
3586
|
+
}
|
|
3587
|
+
if (atlas.generation !== generation) {
|
|
3588
|
+
labelsStale = true;
|
|
3589
|
+
rebuildBadges();
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
badgesStale = false;
|
|
3593
|
+
badgeGeneration = atlas.generation;
|
|
3594
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, badgeBuffer);
|
|
3595
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(badgeData, 0, badgeCount * badgeStride), gl.DYNAMIC_DRAW);
|
|
3596
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, badgeGlyphBuffer);
|
|
3597
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(badgeGlyphs.data, 0, badgeGlyphs.count * 32), gl.DYNAMIC_DRAW);
|
|
3120
3598
|
}
|
|
3121
3599
|
function packEdge(edges, index) {
|
|
3122
3600
|
const at = index * 4;
|
|
@@ -3156,6 +3634,7 @@ function createWebglRenderer(canvas) {
|
|
|
3156
3634
|
}
|
|
3157
3635
|
nodeStyle.upload(first, last);
|
|
3158
3636
|
nodePaint.upload(first, last);
|
|
3637
|
+
nodeImage.upload(first, last);
|
|
3159
3638
|
if (next.positionsChanged || next.full || grown) {
|
|
3160
3639
|
const xy = next.positions.xy;
|
|
3161
3640
|
nodePosition.data.set(xy.subarray(0, Math.min(xy.length, nodePosition.data.length)));
|
|
@@ -3298,7 +3777,8 @@ function createWebglRenderer(canvas) {
|
|
|
3298
3777
|
const passes = [
|
|
3299
3778
|
[nodeProgram, [corner(nodeCorner), instance(warmIndex)]],
|
|
3300
3779
|
[edgeProgram, [corner(edgeCorner, 3), instance(warmIndex)]],
|
|
3301
|
-
[labelProgram, glyphs(warmGlyph)]
|
|
3780
|
+
[labelProgram, glyphs(warmGlyph)],
|
|
3781
|
+
[badgeProgram, badgeAttributes(warmBadge)]
|
|
3302
3782
|
];
|
|
3303
3783
|
gl.enable(gl.SCISSOR_TEST);
|
|
3304
3784
|
gl.scissor(0, 0, 0, 0);
|
|
@@ -3315,6 +3795,7 @@ function createWebglRenderer(canvas) {
|
|
|
3315
3795
|
sync(next) {
|
|
3316
3796
|
input = next;
|
|
3317
3797
|
syncNodes(next);
|
|
3798
|
+
refreshImages(next.scene.nodes);
|
|
3318
3799
|
syncEdges(next);
|
|
3319
3800
|
syncStates(next);
|
|
3320
3801
|
if (next.dirty.nodes.size > 0 || next.full) labelsStale = true;
|
|
@@ -3351,7 +3832,12 @@ function createWebglRenderer(canvas) {
|
|
|
3351
3832
|
frameBlock[7] = 8;
|
|
3352
3833
|
gl.bindBuffer(gl.UNIFORM_BUFFER, frameBuffer);
|
|
3353
3834
|
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, frameBlock);
|
|
3835
|
+
if (badgesStale || badgeGeneration !== atlas.generation) rebuildBadges();
|
|
3836
|
+
const generation = atlas.generation;
|
|
3354
3837
|
const wait = selectLabels(frame, performance.now());
|
|
3838
|
+
if (atlas.generation !== generation) rebuildBadges();
|
|
3839
|
+
syncAtlas();
|
|
3840
|
+
images.prepare();
|
|
3355
3841
|
gl.viewport(0, 0, width, height);
|
|
3356
3842
|
gl.clearColor(0, 0, 0, 0);
|
|
3357
3843
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
@@ -3366,10 +3852,16 @@ function createWebglRenderer(canvas) {
|
|
|
3366
3852
|
draw(pass.program, pass.main, curved.size, layer.slot, 0);
|
|
3367
3853
|
}
|
|
3368
3854
|
} else draw(nodeProgram, vertexArrays.nodes, nodeCapacity, layer.slot, 0);
|
|
3855
|
+
draw(badgeProgram, vertexArrays.badges, badgeCount, 0, 0);
|
|
3856
|
+
draw(labelProgram, vertexArrays.badgeGlyphs, badgeGlyphs.count, 0, 0);
|
|
3369
3857
|
draw(labelProgram, vertexArrays.labels, labels.main.count, 0, 0);
|
|
3370
3858
|
draw(edgeProgram, vertexArrays.edgeOverlay, edgeOverlayCount, 0, 1);
|
|
3371
3859
|
if (curvePass && curved.size > 0) draw(curvePass.program, curvePass.overlay, edgeOverlayCount, 0, 1);
|
|
3372
3860
|
draw(nodeProgram, vertexArrays.nodeOverlay, nodeOverlayCount, 0, 1);
|
|
3861
|
+
if (nodeOverlayCount > 0) {
|
|
3862
|
+
draw(badgeProgram, vertexArrays.badges, badgeCount, 0, 1);
|
|
3863
|
+
draw(labelProgram, vertexArrays.badgeGlyphs, badgeGlyphs.count, 0, 1);
|
|
3864
|
+
}
|
|
3373
3865
|
draw(labelProgram, vertexArrays.labelHighlight, labels.highlight.count, 0, 1);
|
|
3374
3866
|
gl.bindVertexArray(null);
|
|
3375
3867
|
return wait;
|
|
@@ -3378,6 +3870,7 @@ function createWebglRenderer(canvas) {
|
|
|
3378
3870
|
for (const texture of [...nodeTextures, ...edgeTextures]) texture.destroy();
|
|
3379
3871
|
gl.deleteTexture(palette);
|
|
3380
3872
|
gl.deleteTexture(atlasTexture);
|
|
3873
|
+
images.destroy();
|
|
3381
3874
|
for (const buffer of [
|
|
3382
3875
|
...buffers,
|
|
3383
3876
|
frameBuffer,
|
|
@@ -3402,12 +3895,15 @@ function createWebglRenderer(canvas) {
|
|
|
3402
3895
|
[units.edgeTopology, edgeTopology.texture],
|
|
3403
3896
|
[units.edgeStyle, edgeStyle.texture],
|
|
3404
3897
|
[units.edgeState, edgeState.texture],
|
|
3405
|
-
[units.atlas, atlasTexture]
|
|
3898
|
+
[units.atlas, atlasTexture],
|
|
3899
|
+
[units.nodeImage, nodeImage.texture]
|
|
3406
3900
|
];
|
|
3407
3901
|
for (const [unit, texture] of textures) {
|
|
3408
3902
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
3409
3903
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
3410
3904
|
}
|
|
3905
|
+
gl.activeTexture(gl.TEXTURE0 + units.images);
|
|
3906
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, images.texture);
|
|
3411
3907
|
}
|
|
3412
3908
|
}
|
|
3413
3909
|
// src/host/events.ts
|
|
@@ -3437,8 +3933,71 @@ var Emitter = class {
|
|
|
3437
3933
|
this.#handlers.clear();
|
|
3438
3934
|
}
|
|
3439
3935
|
};
|
|
3936
|
+
// src/host/keyboard.ts
|
|
3937
|
+
const arrows = {
|
|
3938
|
+
ArrowLeft: {
|
|
3939
|
+
x: -1,
|
|
3940
|
+
y: 0
|
|
3941
|
+
},
|
|
3942
|
+
ArrowRight: {
|
|
3943
|
+
x: 1,
|
|
3944
|
+
y: 0
|
|
3945
|
+
},
|
|
3946
|
+
ArrowUp: {
|
|
3947
|
+
x: 0,
|
|
3948
|
+
y: -1
|
|
3949
|
+
},
|
|
3950
|
+
ArrowDown: {
|
|
3951
|
+
x: 0,
|
|
3952
|
+
y: 1
|
|
3953
|
+
}
|
|
3954
|
+
};
|
|
3955
|
+
const keyZoom = 1.5;
|
|
3956
|
+
function keyAction(event) {
|
|
3957
|
+
if (event.altKey) return null;
|
|
3958
|
+
const command = event.ctrlKey || event.metaKey;
|
|
3959
|
+
const direction = arrows[event.key];
|
|
3960
|
+
if (direction) {
|
|
3961
|
+
if (command) return null;
|
|
3962
|
+
return event.shiftKey ? {
|
|
3963
|
+
type: "pan",
|
|
3964
|
+
direction
|
|
3965
|
+
} : {
|
|
3966
|
+
type: "move",
|
|
3967
|
+
direction
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
switch (event.key) {
|
|
3971
|
+
case "Enter":
|
|
3972
|
+
case " ": return {
|
|
3973
|
+
type: "activate",
|
|
3974
|
+
additive: event.shiftKey || command
|
|
3975
|
+
};
|
|
3976
|
+
case "Escape": return { type: "escape" };
|
|
3977
|
+
}
|
|
3978
|
+
if (command) return null;
|
|
3979
|
+
switch (event.key) {
|
|
3980
|
+
case "+":
|
|
3981
|
+
case "=": return {
|
|
3982
|
+
type: "zoom",
|
|
3983
|
+
factor: keyZoom
|
|
3984
|
+
};
|
|
3985
|
+
case "-":
|
|
3986
|
+
case "_": return {
|
|
3987
|
+
type: "zoom",
|
|
3988
|
+
factor: 1 / keyZoom
|
|
3989
|
+
};
|
|
3990
|
+
case "0": return { type: "fit" };
|
|
3991
|
+
}
|
|
3992
|
+
return null;
|
|
3993
|
+
}
|
|
3440
3994
|
// src/host/state-registry.ts
|
|
3441
|
-
const highlightBits =
|
|
3995
|
+
const highlightBits = 7;
|
|
3996
|
+
const highlightStates = [
|
|
3997
|
+
"selected",
|
|
3998
|
+
"hovered",
|
|
3999
|
+
"focused"
|
|
4000
|
+
];
|
|
3442
4001
|
const kinds = ["nodes", "edges"];
|
|
3443
4002
|
function members() {
|
|
3444
4003
|
return {
|
|
@@ -3510,6 +4069,7 @@ var StateRegistry = class {
|
|
|
3510
4069
|
apply(changes) {
|
|
3511
4070
|
let selection = false;
|
|
3512
4071
|
let hovered = false;
|
|
4072
|
+
let focused = false;
|
|
3513
4073
|
const forget = (kind, removed) => {
|
|
3514
4074
|
for (const { key, index } of removed) {
|
|
3515
4075
|
this.#setMask(kind, index, 0);
|
|
@@ -3517,6 +4077,7 @@ var StateRegistry = class {
|
|
|
3517
4077
|
if (!this.#members.get(name)?.[kind].delete(key)) continue;
|
|
3518
4078
|
if (name === "selected") selection = true;
|
|
3519
4079
|
if (name === "hovered") hovered = true;
|
|
4080
|
+
if (name === "focused") focused = true;
|
|
3520
4081
|
}
|
|
3521
4082
|
}
|
|
3522
4083
|
};
|
|
@@ -3529,7 +4090,8 @@ var StateRegistry = class {
|
|
|
3529
4090
|
learn("edges", changes.edges.added);
|
|
3530
4091
|
return {
|
|
3531
4092
|
selection,
|
|
3532
|
-
hovered
|
|
4093
|
+
hovered,
|
|
4094
|
+
focused
|
|
3533
4095
|
};
|
|
3534
4096
|
}
|
|
3535
4097
|
takeHighlighted() {
|
|
@@ -3537,7 +4099,7 @@ var StateRegistry = class {
|
|
|
3537
4099
|
this.#highlightDirty = false;
|
|
3538
4100
|
const indices = (kind) => {
|
|
3539
4101
|
const found = /* @__PURE__ */ new Set();
|
|
3540
|
-
for (const name of
|
|
4102
|
+
for (const name of highlightStates) for (const key of this.#members.get(name)?.[kind] ?? []) {
|
|
3541
4103
|
const index = this.#index(kind, key);
|
|
3542
4104
|
if (index >= 0) found.add(index);
|
|
3543
4105
|
}
|
|
@@ -3582,6 +4144,14 @@ var StateRegistry = class {
|
|
|
3582
4144
|
}
|
|
3583
4145
|
};
|
|
3584
4146
|
// src/host/mount-graph.ts
|
|
4147
|
+
const svgNamespace = "http://www.w3.org/2000/svg";
|
|
4148
|
+
const lassoStep = 3;
|
|
4149
|
+
const keyPan = .1;
|
|
4150
|
+
const followMargin = 48;
|
|
4151
|
+
const keyDuration = 200;
|
|
4152
|
+
const visuallyHidden = "position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap";
|
|
4153
|
+
const focusOutline = "2px solid var(--graph-focus-ring,#c93700)";
|
|
4154
|
+
const lassoStyle = "fill:color-mix(in srgb,var(--graph-accent,#c93700) 10%,transparent);stroke:var(--graph-accent,#c93700);stroke-width:1.5;stroke-dasharray:4 3;stroke-linejoin:round";
|
|
3585
4155
|
const defaultPadding = 32;
|
|
3586
4156
|
const clickSlop = 4;
|
|
3587
4157
|
const pickTolerance = 4;
|
|
@@ -3636,11 +4206,15 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3636
4206
|
element.setAttribute("data-graph", "");
|
|
3637
4207
|
element.setAttribute("role", "img");
|
|
3638
4208
|
element.setAttribute("aria-label", options.ariaLabel);
|
|
3639
|
-
element.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden";
|
|
4209
|
+
element.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden;outline:none;outline-offset:-2px";
|
|
3640
4210
|
const canvas = document.createElement("canvas");
|
|
3641
4211
|
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;outline:none";
|
|
3642
|
-
|
|
4212
|
+
const announcer = document.createElement("div");
|
|
4213
|
+
announcer.setAttribute("aria-live", "polite");
|
|
4214
|
+
announcer.style.cssText = visuallyHidden;
|
|
4215
|
+
element.append(canvas, announcer);
|
|
3643
4216
|
container.append(element);
|
|
4217
|
+
let remembered = null;
|
|
3644
4218
|
let frameRequest = 0;
|
|
3645
4219
|
let laterRequest;
|
|
3646
4220
|
let needsFlush = false;
|
|
@@ -3668,7 +4242,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3668
4242
|
const emitter = new Emitter();
|
|
3669
4243
|
const pickIndex = new PickIndex();
|
|
3670
4244
|
const themeResolver = new ThemeResolver(element);
|
|
3671
|
-
let renderer = createRenderer(canvas);
|
|
4245
|
+
let renderer = createRenderer(canvas, () => request());
|
|
3672
4246
|
registry.define(freeStateNames(definition.states));
|
|
3673
4247
|
if (options.selection !== void 0) registry.set("selected", options.selection);
|
|
3674
4248
|
const store = createStore({
|
|
@@ -3680,6 +4254,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3680
4254
|
width: 0,
|
|
3681
4255
|
height: 0,
|
|
3682
4256
|
hovered: null,
|
|
4257
|
+
focused: null,
|
|
3683
4258
|
selection: controlled ? registry.members("selected") : noKeys,
|
|
3684
4259
|
states: freeStates(),
|
|
3685
4260
|
positionsVersion: 0
|
|
@@ -3872,16 +4447,197 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3872
4447
|
updateCursor();
|
|
3873
4448
|
request();
|
|
3874
4449
|
}
|
|
4450
|
+
function nodePoint(key) {
|
|
4451
|
+
const record = model.node(key);
|
|
4452
|
+
return record ? {
|
|
4453
|
+
x: positions.x(record.index),
|
|
4454
|
+
y: positions.y(record.index)
|
|
4455
|
+
} : null;
|
|
4456
|
+
}
|
|
4457
|
+
function focusPoint(focus) {
|
|
4458
|
+
if (focus.kind === "node") return nodePoint(focus.key);
|
|
4459
|
+
const ends = model.endpoints(focus.key);
|
|
4460
|
+
const a = ends ? nodePoint(ends.source) : null;
|
|
4461
|
+
const b = ends ? nodePoint(ends.target) : null;
|
|
4462
|
+
return a && b ? {
|
|
4463
|
+
x: (a.x + b.x) / 2,
|
|
4464
|
+
y: (a.y + b.y) / 2
|
|
4465
|
+
} : null;
|
|
4466
|
+
}
|
|
4467
|
+
function labelOf(focus) {
|
|
4468
|
+
const record = focus.kind === "node" ? model.node(focus.key) : model.edge(focus.key);
|
|
4469
|
+
const labels = focus.kind === "node" ? scene.nodes.label : scene.edges.label;
|
|
4470
|
+
return record && labels[record.index] || String(focus.key);
|
|
4471
|
+
}
|
|
4472
|
+
function announce(text) {
|
|
4473
|
+
announcer.textContent = announcer.textContent === text ? `${text}\u00a0` : text;
|
|
4474
|
+
}
|
|
4475
|
+
function reveal(focus) {
|
|
4476
|
+
const point = focusPoint(focus);
|
|
4477
|
+
if (!point) return;
|
|
4478
|
+
const { view } = store.get();
|
|
4479
|
+
const size = viewport();
|
|
4480
|
+
const screen = toScreen(view, size, point);
|
|
4481
|
+
if (!(screen.x >= followMargin && screen.x <= size.width - followMargin && screen.y >= followMargin && screen.y <= size.height - followMargin)) animateTo({
|
|
4482
|
+
...view,
|
|
4483
|
+
x: point.x,
|
|
4484
|
+
y: point.y
|
|
4485
|
+
}, keyDuration);
|
|
4486
|
+
}
|
|
4487
|
+
function setFocused(next, follow = false) {
|
|
4488
|
+
if (sameTarget(store.get().focused, next)) return;
|
|
4489
|
+
registry.set("focused", next ? { [kindOf[next.kind]]: [next.key] } : {});
|
|
4490
|
+
patch({ focused: next });
|
|
4491
|
+
if (next) {
|
|
4492
|
+
element.style.outline = "none";
|
|
4493
|
+
announce(labelOf(next));
|
|
4494
|
+
if (follow) reveal(next);
|
|
4495
|
+
}
|
|
4496
|
+
emitter.emit("focuschange", { target: next });
|
|
4497
|
+
request();
|
|
4498
|
+
}
|
|
4499
|
+
function moveFocus(direction) {
|
|
4500
|
+
const current = store.get().focused;
|
|
4501
|
+
const from = current?.kind === "node" ? model.node(current.key)?.index ?? -1 : -1;
|
|
4502
|
+
let index = from >= 0 ? nodeToward(from, direction, scene.nodes, positions) : -1;
|
|
4503
|
+
if (from < 0) {
|
|
4504
|
+
const selected = registry.members("selected").nodes;
|
|
4505
|
+
const last = selected[selected.length - 1];
|
|
4506
|
+
const center = store.get().view;
|
|
4507
|
+
index = last !== void 0 ? model.node(last)?.index ?? -1 : nearestNode((current && focusPoint(current)) ?? center, scene.nodes, positions);
|
|
4508
|
+
}
|
|
4509
|
+
const key = index >= 0 ? scene.nodes.keys[index] : void 0;
|
|
4510
|
+
const next = key !== void 0 ? target("node", key) : null;
|
|
4511
|
+
if (next) setFocused(next, true);
|
|
4512
|
+
}
|
|
4513
|
+
function focusVisible() {
|
|
4514
|
+
try {
|
|
4515
|
+
return element.matches(":focus-visible");
|
|
4516
|
+
} catch {
|
|
4517
|
+
return true;
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
function onFocus() {
|
|
4521
|
+
if (!definition.interaction.keyboard || !focusVisible()) return;
|
|
4522
|
+
const restored = remembered ? target(remembered.kind, remembered.key) : null;
|
|
4523
|
+
remembered = null;
|
|
4524
|
+
if (restored) setFocused(restored, true);
|
|
4525
|
+
else if (!store.get().focused) element.style.outline = focusOutline;
|
|
4526
|
+
}
|
|
4527
|
+
function onBlur() {
|
|
4528
|
+
element.style.outline = "none";
|
|
4529
|
+
const current = store.get().focused;
|
|
4530
|
+
if (!current) return;
|
|
4531
|
+
remembered = current;
|
|
4532
|
+
setFocused(null);
|
|
4533
|
+
}
|
|
4534
|
+
function cancelLasso() {
|
|
4535
|
+
if (!gesture?.lasso) return false;
|
|
4536
|
+
if (canvas.hasPointerCapture?.(gesture.id)) canvas.releasePointerCapture(gesture.id);
|
|
4537
|
+
gesture = null;
|
|
4538
|
+
drawLasso(null);
|
|
4539
|
+
updateCursor();
|
|
4540
|
+
return true;
|
|
4541
|
+
}
|
|
4542
|
+
function runKey(event) {
|
|
4543
|
+
const action = keyAction(event);
|
|
4544
|
+
if (!action) return false;
|
|
4545
|
+
const { interaction } = definition;
|
|
4546
|
+
const { view, focused, selection } = store.get();
|
|
4547
|
+
const size = viewport();
|
|
4548
|
+
switch (action.type) {
|
|
4549
|
+
case "move":
|
|
4550
|
+
moveFocus(action.direction);
|
|
4551
|
+
return true;
|
|
4552
|
+
case "pan": {
|
|
4553
|
+
if (!interaction.pan) return false;
|
|
4554
|
+
const step = Math.min(size.width, size.height) * keyPan;
|
|
4555
|
+
commitView(panBy(view, -action.direction.x * step, -action.direction.y * step));
|
|
4556
|
+
return true;
|
|
4557
|
+
}
|
|
4558
|
+
case "zoom": {
|
|
4559
|
+
if (!interaction.zoom) return false;
|
|
4560
|
+
const point = focused ? focusPoint(focused) : null;
|
|
4561
|
+
animateTo(zoomAround(view, size, point ? toScreen(view, size, point) : {
|
|
4562
|
+
x: size.width / 2,
|
|
4563
|
+
y: size.height / 2
|
|
4564
|
+
}, action.factor), keyDuration);
|
|
4565
|
+
return true;
|
|
4566
|
+
}
|
|
4567
|
+
case "fit": return fitTo(void 0, { duration: keyDuration });
|
|
4568
|
+
case "activate":
|
|
4569
|
+
if (!focused || interaction.select === false) return false;
|
|
4570
|
+
selectTarget(focused, action.additive);
|
|
4571
|
+
return true;
|
|
4572
|
+
case "escape":
|
|
4573
|
+
if (cancelLasso()) return true;
|
|
4574
|
+
if (interaction.select !== false && selection.nodes.length + selection.edges.length > 0) {
|
|
4575
|
+
requestSelection(noKeys);
|
|
4576
|
+
return true;
|
|
4577
|
+
}
|
|
4578
|
+
if (!focused) return false;
|
|
4579
|
+
setFocused(null);
|
|
4580
|
+
return true;
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
function onKeyDown(event) {
|
|
4584
|
+
if (!definition.interaction.keyboard || event.target !== element) return;
|
|
4585
|
+
if (runKey(event)) event.preventDefault();
|
|
4586
|
+
}
|
|
4587
|
+
function applyKeyboard() {
|
|
4588
|
+
if (definition.interaction.keyboard) {
|
|
4589
|
+
element.tabIndex = 0;
|
|
4590
|
+
element.setAttribute("role", "application");
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
element.removeAttribute("tabindex");
|
|
4594
|
+
element.setAttribute("role", "img");
|
|
4595
|
+
element.style.outline = "none";
|
|
4596
|
+
setFocused(null);
|
|
4597
|
+
}
|
|
4598
|
+
let lassoLayer = null;
|
|
4599
|
+
function drawLasso(points) {
|
|
4600
|
+
if (!points) {
|
|
4601
|
+
lassoLayer?.remove();
|
|
4602
|
+
lassoLayer = null;
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
if (!lassoLayer) {
|
|
4606
|
+
lassoLayer = document.createElementNS(svgNamespace, "svg");
|
|
4607
|
+
lassoLayer.setAttribute("aria-hidden", "true");
|
|
4608
|
+
lassoLayer.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible";
|
|
4609
|
+
const path = document.createElementNS(svgNamespace, "path");
|
|
4610
|
+
path.style.cssText = lassoStyle;
|
|
4611
|
+
lassoLayer.append(path);
|
|
4612
|
+
element.append(lassoLayer);
|
|
4613
|
+
}
|
|
4614
|
+
lassoLayer.firstElementChild?.setAttribute("d", `M${points.map((point) => `${point.x} ${point.y}`).join("L")}Z`);
|
|
4615
|
+
}
|
|
4616
|
+
function selectInside(points) {
|
|
4617
|
+
const view = store.get().view;
|
|
4618
|
+
const size = viewport();
|
|
4619
|
+
const inside = nodesInside(points.map((point) => toWorld(view, size, point)), scene.nodes, positions).filter((key) => !registry.has("selected", "nodes", key));
|
|
4620
|
+
if (inside.length === 0) return;
|
|
4621
|
+
const current = registry.members("selected");
|
|
4622
|
+
requestSelection({
|
|
4623
|
+
...current,
|
|
4624
|
+
nodes: [...current.nodes, ...inside]
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
3875
4627
|
function updateCursor() {
|
|
3876
4628
|
const interactive = definition.interaction.select !== false || definition.interaction.drag;
|
|
3877
|
-
if (gesture?.
|
|
4629
|
+
if (gesture?.lasso) canvas.style.cursor = "crosshair";
|
|
4630
|
+
else if (gesture?.moved) canvas.style.cursor = "grabbing";
|
|
3878
4631
|
else if (store.get().hovered && interactive) canvas.style.cursor = "pointer";
|
|
3879
4632
|
else canvas.style.cursor = "";
|
|
3880
4633
|
}
|
|
3881
4634
|
function selectOnClick(clicked, event) {
|
|
4635
|
+
selectTarget(clicked, event.shiftKey || event.metaKey || event.ctrlKey);
|
|
4636
|
+
}
|
|
4637
|
+
function selectTarget(clicked, modifier) {
|
|
3882
4638
|
const mode = definition.interaction.select;
|
|
3883
4639
|
if (mode === false) return;
|
|
3884
|
-
const additive = mode === "multiple" &&
|
|
4640
|
+
const additive = mode === "multiple" && modifier;
|
|
3885
4641
|
if (!clicked) {
|
|
3886
4642
|
if (!additive) requestSelection(noKeys);
|
|
3887
4643
|
return;
|
|
@@ -3920,6 +4676,14 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
3920
4676
|
const fresh = target(hovered.kind, hovered.key);
|
|
3921
4677
|
if (fresh && fresh.datum !== hovered.datum) patch({ hovered: fresh });
|
|
3922
4678
|
}
|
|
4679
|
+
const focused = store.get().focused;
|
|
4680
|
+
if (result.focused) {
|
|
4681
|
+
patch({ focused: null });
|
|
4682
|
+
emitter.emit("focuschange", { target: null });
|
|
4683
|
+
} else if (focused) {
|
|
4684
|
+
const fresh = target(focused.kind, focused.key);
|
|
4685
|
+
if (fresh && fresh.datum !== focused.datum) patch({ focused: fresh });
|
|
4686
|
+
}
|
|
3923
4687
|
}
|
|
3924
4688
|
function movePositions(indices, to, duration) {
|
|
3925
4689
|
if (indices.length === 0) return;
|
|
@@ -4101,18 +4865,24 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4101
4865
|
function onPointerDown(event) {
|
|
4102
4866
|
if (event.button !== 0 || gesture) return;
|
|
4103
4867
|
viewAnimation = null;
|
|
4868
|
+
remembered = null;
|
|
4869
|
+
setFocused(null);
|
|
4104
4870
|
const point = local(event);
|
|
4105
4871
|
const hit = pickAt(point);
|
|
4106
4872
|
const drag = hit ? startDrag(hit, point) : null;
|
|
4873
|
+
const { lasso, select } = definition.interaction;
|
|
4874
|
+
const lassoing = !hit && event.shiftKey && lasso && select === "multiple";
|
|
4107
4875
|
gesture = {
|
|
4108
4876
|
id: event.pointerId,
|
|
4109
4877
|
start: point,
|
|
4110
4878
|
last: point,
|
|
4111
4879
|
target: hit,
|
|
4112
4880
|
drag,
|
|
4881
|
+
lasso: lassoing ? [point] : null,
|
|
4113
4882
|
moved: false
|
|
4114
4883
|
};
|
|
4115
4884
|
canvas.setPointerCapture?.(event.pointerId);
|
|
4885
|
+
if (lassoing) updateCursor();
|
|
4116
4886
|
}
|
|
4117
4887
|
function onPointerMove(event) {
|
|
4118
4888
|
const point = local(event);
|
|
@@ -4125,6 +4895,16 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4125
4895
|
gesture.moved = true;
|
|
4126
4896
|
updateCursor();
|
|
4127
4897
|
}
|
|
4898
|
+
const lasso = gesture.lasso;
|
|
4899
|
+
if (lasso) {
|
|
4900
|
+
const last = lasso[lasso.length - 1] ?? gesture.start;
|
|
4901
|
+
if (gesture.moved && Math.hypot(point.x - last.x, point.y - last.y) >= lassoStep) {
|
|
4902
|
+
lasso.push(point);
|
|
4903
|
+
drawLasso(lasso);
|
|
4904
|
+
}
|
|
4905
|
+
gesture.last = point;
|
|
4906
|
+
return;
|
|
4907
|
+
}
|
|
4128
4908
|
const drag = gesture.drag;
|
|
4129
4909
|
if (drag) {
|
|
4130
4910
|
if (!gesture.moved) return;
|
|
@@ -4161,6 +4941,10 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4161
4941
|
gesture = null;
|
|
4162
4942
|
if (canvas.hasPointerCapture?.(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
|
|
4163
4943
|
if (ended.drag?.started) emitter.emit("dragend", dragEvent(ended.drag, event));
|
|
4944
|
+
if (ended.lasso) {
|
|
4945
|
+
drawLasso(null);
|
|
4946
|
+
if (!cancelled && ended.moved) selectInside(ended.lasso);
|
|
4947
|
+
}
|
|
4164
4948
|
if (!cancelled && !ended.moved) {
|
|
4165
4949
|
emitter.emit("click", pointerEvent(event, ended.target));
|
|
4166
4950
|
selectOnClick(ended.target, event);
|
|
@@ -4198,7 +4982,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4198
4982
|
}
|
|
4199
4983
|
function onContextRestored() {
|
|
4200
4984
|
contextLost = false;
|
|
4201
|
-
renderer = createRenderer(canvas);
|
|
4985
|
+
renderer = createRenderer(canvas, () => request());
|
|
4202
4986
|
full = true;
|
|
4203
4987
|
refreshTheme();
|
|
4204
4988
|
}
|
|
@@ -4219,6 +5003,13 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4219
5003
|
["webglcontextrestored", onContextRestored]
|
|
4220
5004
|
];
|
|
4221
5005
|
for (const [name, listener, listenerOptions] of listeners) canvas.addEventListener(name, listener, listenerOptions);
|
|
5006
|
+
const elementListeners = [
|
|
5007
|
+
["keydown", onKeyDown],
|
|
5008
|
+
["focus", onFocus],
|
|
5009
|
+
["blur", onBlur]
|
|
5010
|
+
];
|
|
5011
|
+
for (const [name, listener] of elementListeners) element.addEventListener(name, listener);
|
|
5012
|
+
applyKeyboard();
|
|
4222
5013
|
model.setMarks(definition.marks);
|
|
4223
5014
|
scene.setDefinition(definition);
|
|
4224
5015
|
applyLayers();
|
|
@@ -4253,6 +5044,15 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4253
5044
|
set: (keys) => requestSelection(toKeys(keys)),
|
|
4254
5045
|
clear: () => requestSelection(noKeys)
|
|
4255
5046
|
},
|
|
5047
|
+
focus: {
|
|
5048
|
+
get: () => store.get().focused,
|
|
5049
|
+
set: (next) => {
|
|
5050
|
+
const resolved = next ? target(next.kind, next.key) : null;
|
|
5051
|
+
if (next && !resolved) return;
|
|
5052
|
+
if (resolved && definition.interaction.keyboard) element.focus({ preventScroll: true });
|
|
5053
|
+
setFocused(resolved, true);
|
|
5054
|
+
}
|
|
5055
|
+
},
|
|
4256
5056
|
states: {
|
|
4257
5057
|
set: (name, keys) => {
|
|
4258
5058
|
if (registry.set(declared(name), keys)) publishState(name);
|
|
@@ -4315,6 +5115,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4315
5115
|
applyLayers();
|
|
4316
5116
|
if (registry.define(freeStateNames(definition.states))) patch({ states: freeStates() });
|
|
4317
5117
|
applyStyles();
|
|
5118
|
+
applyKeyboard();
|
|
4318
5119
|
updateCursor();
|
|
4319
5120
|
request(true);
|
|
4320
5121
|
}
|
|
@@ -4327,6 +5128,7 @@ function createGraphHost(container, options, createRenderer) {
|
|
|
4327
5128
|
resizeObserver.disconnect();
|
|
4328
5129
|
stopTheme();
|
|
4329
5130
|
for (const [name, listener] of listeners) canvas.removeEventListener(name, listener);
|
|
5131
|
+
for (const [name, listener] of elementListeners) element.removeEventListener(name, listener);
|
|
4330
5132
|
emitter.clear();
|
|
4331
5133
|
model.destroy();
|
|
4332
5134
|
renderer.destroy();
|
|
@@ -4340,4 +5142,4 @@ function mountGraph(container, options) {
|
|
|
4340
5142
|
}
|
|
4341
5143
|
export { freeStateNames as n, maxFreeStates as r, mountGraph as t };
|
|
4342
5144
|
|
|
4343
|
-
//# sourceMappingURL=mount-graph-
|
|
5145
|
+
//# sourceMappingURL=mount-graph-CzVfiIte.js.map
|