@yggy-tech/graphraum 0.37.0 → 0.38.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/dist/index.d.ts +26 -0
- package/dist/index.js +346 -168
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -304,6 +304,12 @@ interface GraphraumDepthOfFieldOptions {
|
|
|
304
304
|
* `d` is its camera distance and `f` the focal distance, clamped to `maxBlur`.
|
|
305
305
|
*/
|
|
306
306
|
strength?: number;
|
|
307
|
+
/**
|
|
308
|
+
* Aperture for surfaces between the camera and the focus, 0..1. Defaults to `strength`. Set it
|
|
309
|
+
* above `strength` (with a larger `maxBlur`) to melt the foreground while the background behind
|
|
310
|
+
* the focus keeps the softer `strength` blur.
|
|
311
|
+
*/
|
|
312
|
+
nearStrength?: number;
|
|
307
313
|
/** Largest blur radius in CSS pixels. Default 12. */
|
|
308
314
|
maxBlur?: number;
|
|
309
315
|
/** Blur radii below this many CSS pixels render sharp, which widens the in-focus band. Default 0. */
|
|
@@ -636,6 +642,8 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
636
642
|
private readonly depthOfFieldFocus;
|
|
637
643
|
/** Strength actually applied: fades toward `strength` while enabled and toward 0 when disabled. */
|
|
638
644
|
private readonly depthOfFieldStrength;
|
|
645
|
+
/** The same for `nearStrength`, faded alongside. */
|
|
646
|
+
private readonly depthOfFieldNearStrength;
|
|
639
647
|
private depthOfFieldPass;
|
|
640
648
|
private depthOfFieldStatus;
|
|
641
649
|
constructor(container: HTMLElement, options?: GraphraumOptions<NodeAttributes, EdgeAttributes>);
|
|
@@ -647,6 +655,13 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
647
655
|
private canMutateDataInPlace;
|
|
648
656
|
private mutateDataInPlace;
|
|
649
657
|
private appendDataPatch;
|
|
658
|
+
/**
|
|
659
|
+
* Draws appended nodes and edges without repacking the ones already drawn. It applies only when the
|
|
660
|
+
* last pass drew the whole graph in index order, so the appended ones land after the existing ones
|
|
661
|
+
* exactly where a full pass would put them: 3D, or 2D without viewport culling, and no density or
|
|
662
|
+
* overview tier before or after. Returns false, having written nothing, when it does not apply.
|
|
663
|
+
*/
|
|
664
|
+
private materializeAppended;
|
|
650
665
|
private replaceData;
|
|
651
666
|
updateNodes(updates: readonly GraphraumNodeUpdate[]): void;
|
|
652
667
|
/** Applies a transferable XYZ layout batch. Call repeatedly as a worker produces progressive positions. */
|
|
@@ -709,6 +724,7 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
709
724
|
*/
|
|
710
725
|
setDepthOfField(options: GraphraumDepthOfFieldOptions, update?: GraphraumDepthOfFieldUpdate): Promise<boolean>;
|
|
711
726
|
private depthOfFieldTargetStrength;
|
|
727
|
+
private depthOfFieldTargetNearStrength;
|
|
712
728
|
getNodePresentation(nodeId: string): CompiledGraphraumPresentation | null;
|
|
713
729
|
/** Returns canvas-relative screen coordinates for a node, or null when the node does not exist. */
|
|
714
730
|
getNodeScreenPosition(nodeId: string): GraphraumScreenPosition | null;
|
|
@@ -858,6 +874,16 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
858
874
|
private createBackgroundTexture;
|
|
859
875
|
private applyBackground;
|
|
860
876
|
private materializeViewport;
|
|
877
|
+
/** A node drawn at its own position and size, which is every node outside the density tier. */
|
|
878
|
+
private plainMaterializedNode;
|
|
879
|
+
private nodeInstanceTargets;
|
|
880
|
+
/** Writes one node's matrix, pick matrix, color, shape and stroke into `slot`. */
|
|
881
|
+
private writeNodeInstance;
|
|
882
|
+
/**
|
|
883
|
+
* Writes packed edge instances at the given first slots and records what picking needs for them.
|
|
884
|
+
* Returns how many distinct edges got a segment.
|
|
885
|
+
*/
|
|
886
|
+
private writeEdgeInstances;
|
|
861
887
|
private materializeNodes;
|
|
862
888
|
private disposeGraphObjects;
|
|
863
889
|
}
|
package/dist/index.js
CHANGED
|
@@ -1268,6 +1268,7 @@ const DEPTH_OF_FIELD_DEFAULTS = {
|
|
|
1268
1268
|
maxBlur: 12,
|
|
1269
1269
|
minBlur: 0,
|
|
1270
1270
|
nearDistance: 0,
|
|
1271
|
+
nearStrength: null,
|
|
1271
1272
|
strength: .5,
|
|
1272
1273
|
transition: {
|
|
1273
1274
|
durationMs: 400,
|
|
@@ -1300,15 +1301,17 @@ function resolveDepthOfFieldOptions(options, base = DEPTH_OF_FIELD_DEFAULTS) {
|
|
|
1300
1301
|
maxBlur: options.maxBlur ?? base.maxBlur,
|
|
1301
1302
|
minBlur: options.minBlur ?? base.minBlur,
|
|
1302
1303
|
nearDistance: options.nearDistance ?? base.nearDistance,
|
|
1304
|
+
nearStrength: options.nearStrength ?? base.nearStrength,
|
|
1303
1305
|
strength: options.strength ?? base.strength,
|
|
1304
1306
|
transition: {
|
|
1305
1307
|
durationMs: options.transition?.durationMs ?? base.transition.durationMs,
|
|
1306
1308
|
easing: options.transition?.easing ?? base.transition.easing
|
|
1307
1309
|
}
|
|
1308
1310
|
};
|
|
1309
|
-
const { enabled, farDistance, focus, maxBlur, minBlur, nearDistance, strength, transition } = resolved;
|
|
1311
|
+
const { enabled, farDistance, focus, maxBlur, minBlur, nearDistance, nearStrength, strength, transition } = resolved;
|
|
1310
1312
|
if (typeof enabled !== "boolean") throw new Error(`Depth of field enabled must be a boolean; received ${String(enabled)}.`);
|
|
1311
1313
|
if (!isNonNegativeNumber(strength) || strength > 1) throw new Error(`Depth of field strength must be a number from 0 to 1; received ${String(strength)}.`);
|
|
1314
|
+
if (nearStrength !== null && (!isNonNegativeNumber(nearStrength) || nearStrength > 1)) throw new Error(`Depth of field nearStrength must be a number from 0 to 1; received ${String(nearStrength)}.`);
|
|
1312
1315
|
if (focus !== "selection" && (typeof focus !== "number" || !Number.isFinite(focus) || focus <= 0)) throw new Error(`Depth of field focus must be "selection" or a camera distance above 0 in world units; received ${String(focus)}.`);
|
|
1313
1316
|
if (!isNonNegativeNumber(maxBlur) || !isNonNegativeNumber(minBlur) || minBlur > maxBlur) throw new Error(`Depth of field minBlur and maxBlur must be non-negative CSS pixel radii with minBlur <= maxBlur; received ${String(minBlur)} and ${String(maxBlur)}.`);
|
|
1314
1317
|
if (!isNonNegativeNumber(nearDistance) || typeof farDistance !== "number" || Number.isNaN(farDistance) || farDistance <= nearDistance) throw new Error(`Depth of field nearDistance must be a non-negative world distance below farDistance; received ${String(nearDistance)} and ${String(farDistance)}.`);
|
|
@@ -1501,6 +1504,7 @@ uniform float cameraNear;
|
|
|
1501
1504
|
uniform float cameraFar;
|
|
1502
1505
|
uniform float focusDistance;
|
|
1503
1506
|
uniform float aperture;
|
|
1507
|
+
uniform float nearAperture;
|
|
1504
1508
|
uniform float minBlur;
|
|
1505
1509
|
uniform float maxBlur;
|
|
1506
1510
|
|
|
@@ -1512,7 +1516,8 @@ float surfaceDistance(ivec2 pixel) {
|
|
|
1512
1516
|
}
|
|
1513
1517
|
|
|
1514
1518
|
float radiusAt(float surface) {
|
|
1515
|
-
float
|
|
1519
|
+
float lens = surface < focusDistance ? nearAperture : aperture;
|
|
1520
|
+
float radius = lens * abs(surface - focusDistance) / max(surface, 1e-6);
|
|
1516
1521
|
return radius < minBlur ? 0.0 : min(radius, maxBlur);
|
|
1517
1522
|
}
|
|
1518
1523
|
|
|
@@ -1528,27 +1533,57 @@ layout(location = 1) out highp vec4 nearColor;
|
|
|
1528
1533
|
|
|
1529
1534
|
const float GOLDEN_ANGLE = 2.39996323;
|
|
1530
1535
|
|
|
1536
|
+
ivec2 base;
|
|
1537
|
+
float centreSurface;
|
|
1538
|
+
float rotation;
|
|
1539
|
+
|
|
1540
|
+
float tapDistance(int index, float reach) {
|
|
1541
|
+
return sqrt((float(index) + 0.5) / ${BLUR_TAPS}.0) * reach;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
ivec2 tapPixel(int index, float distanceFromCentre) {
|
|
1545
|
+
float angle = float(index) * GOLDEN_ANGLE + rotation;
|
|
1546
|
+
return clamp(base + ivec2(floor(vec2(cos(angle), sin(angle)) * distanceFromCentre + 0.5)), ivec2(0), depthSize - 1);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
vec4 nearTerm(float surface, float radius, float weight, vec4 color) {
|
|
1550
|
+
if (surface >= focusDistance || surface >= centreSurface) return vec4(0.0);
|
|
1551
|
+
return color * weight * (maxBlur * maxBlur / max(radius * radius, 1.0));
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1531
1554
|
void main() {
|
|
1532
|
-
|
|
1533
|
-
|
|
1555
|
+
base = ivec2(gl_FragCoord.xy) * 2;
|
|
1556
|
+
centreSurface = surfaceDistance(base);
|
|
1534
1557
|
vec4 sum = texelFetch(colorTexture, base, 0);
|
|
1535
1558
|
float total = 1.0;
|
|
1536
1559
|
vec4 near = vec4(0.0);
|
|
1537
1560
|
// Interleaved gradient noise turns the spiral's fixed tap pattern into fine noise that the
|
|
1538
1561
|
// composite's bilinear upsample smooths, instead of hatching along blurred edges.
|
|
1539
|
-
|
|
1562
|
+
rotation = 6.28318531 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
|
1563
|
+
// Mirrors blurGatherRadius in depth-of-field.ts.
|
|
1564
|
+
float centreRadius = radiusAt(centreSurface);
|
|
1565
|
+
float blurReach = min(maxBlur, max(centreRadius, aperture));
|
|
1566
|
+
bool nearInBlurLoop = blurReach >= maxBlur;
|
|
1540
1567
|
for (int index = 0; index < ${BLUR_TAPS}; index++) {
|
|
1541
|
-
float distanceFromCentre =
|
|
1542
|
-
|
|
1543
|
-
ivec2 pixel = clamp(base + ivec2(floor(vec2(cos(angle), sin(angle)) * distanceFromCentre + 0.5)), ivec2(0), depthSize - 1);
|
|
1568
|
+
float distanceFromCentre = tapDistance(index, blurReach);
|
|
1569
|
+
ivec2 pixel = tapPixel(index, distanceFromCentre);
|
|
1544
1570
|
float surface = surfaceDistance(pixel);
|
|
1545
1571
|
float radius = radiusAt(surface);
|
|
1546
|
-
float
|
|
1572
|
+
float reach = surface > centreSurface ? max(radius, centreRadius) : radius;
|
|
1573
|
+
float weight = clamp(reach - distanceFromCentre + 1.0, 0.0, 1.0);
|
|
1547
1574
|
vec4 color = texelFetch(colorTexture, pixel, 0);
|
|
1548
1575
|
sum += color * weight;
|
|
1549
1576
|
total += weight;
|
|
1550
|
-
if (
|
|
1551
|
-
|
|
1577
|
+
if (nearInBlurLoop) near += nearTerm(surface, radius, weight, color);
|
|
1578
|
+
}
|
|
1579
|
+
if (!nearInBlurLoop) {
|
|
1580
|
+
for (int index = 0; index < ${BLUR_TAPS}; index++) {
|
|
1581
|
+
float distanceFromCentre = tapDistance(index, maxBlur);
|
|
1582
|
+
ivec2 pixel = tapPixel(index, distanceFromCentre);
|
|
1583
|
+
float surface = surfaceDistance(pixel);
|
|
1584
|
+
float radius = radiusAt(surface);
|
|
1585
|
+
float weight = clamp(radius - distanceFromCentre + 1.0, 0.0, 1.0);
|
|
1586
|
+
near += nearTerm(surface, radius, weight, texelFetch(colorTexture, pixel, 0));
|
|
1552
1587
|
}
|
|
1553
1588
|
}
|
|
1554
1589
|
blurColor = sum / total;
|
|
@@ -1583,6 +1618,7 @@ function blurUniforms() {
|
|
|
1583
1618
|
focusDistance: { value: 1 },
|
|
1584
1619
|
maxBlur: { value: 0 },
|
|
1585
1620
|
minBlur: { value: 0 },
|
|
1621
|
+
nearAperture: { value: 0 },
|
|
1586
1622
|
nodeDepth: { value: null }
|
|
1587
1623
|
};
|
|
1588
1624
|
}
|
|
@@ -1681,6 +1717,7 @@ var DepthOfFieldPass = class {
|
|
|
1681
1717
|
focusDistance: frame.focusDistance,
|
|
1682
1718
|
maxBlur: frame.maxBlurPixels,
|
|
1683
1719
|
minBlur: frame.minBlurPixels,
|
|
1720
|
+
nearAperture: frame.nearAperturePixels,
|
|
1684
1721
|
nodeDepth: this.sceneTarget.depthTexture
|
|
1685
1722
|
};
|
|
1686
1723
|
this.quad.material = this.blurMaterial;
|
|
@@ -2210,13 +2247,18 @@ function writeEdgeColorSlots(color, start, count, paint) {
|
|
|
2210
2247
|
}
|
|
2211
2248
|
//#endregion
|
|
2212
2249
|
//#region src/edge-viewport-patch.ts
|
|
2213
|
-
/**
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2250
|
+
/**
|
|
2251
|
+
* Adds the slot ranges of appended edges to `layouts`. Appended segments take the place of the
|
|
2252
|
+
* first markers, which move up by `segments.length` slots, so every existing marker range shifts
|
|
2253
|
+
* with them. The result equals `buildVisibleEdgeLayouts` over the whole edge set.
|
|
2254
|
+
*/
|
|
2255
|
+
function appendVisibleEdgeLayouts(layouts, segments, markers, slots) {
|
|
2256
|
+
if (segments.length > 0) for (const layout of layouts.values()) layout.markerStart += segments.length;
|
|
2257
|
+
for (const [offset, segment] of segments.entries()) {
|
|
2258
|
+
const slot = slots.segmentStart + offset;
|
|
2217
2259
|
const layout = layouts.get(segment.edgeIndex) ?? {
|
|
2218
2260
|
markerCount: 0,
|
|
2219
|
-
markerStart:
|
|
2261
|
+
markerStart: slots.segmentTotal,
|
|
2220
2262
|
segmentCount: 0,
|
|
2221
2263
|
segmentStart: slot
|
|
2222
2264
|
};
|
|
@@ -2225,7 +2267,7 @@ function buildVisibleEdgeLayouts(segments, markers, segmentInstanceCount) {
|
|
|
2225
2267
|
layouts.set(segment.edgeIndex, layout);
|
|
2226
2268
|
}
|
|
2227
2269
|
for (const [offset, marker] of markers.entries()) {
|
|
2228
|
-
const slot =
|
|
2270
|
+
const slot = slots.markerStart + offset;
|
|
2229
2271
|
const layout = layouts.get(marker.edgeIndex) ?? {
|
|
2230
2272
|
markerCount: 0,
|
|
2231
2273
|
markerStart: slot,
|
|
@@ -2238,6 +2280,14 @@ function buildVisibleEdgeLayouts(segments, markers, segmentInstanceCount) {
|
|
|
2238
2280
|
}
|
|
2239
2281
|
return layouts;
|
|
2240
2282
|
}
|
|
2283
|
+
/** Records GPU slot ranges for each visible edge after a full viewport materialize. */
|
|
2284
|
+
function buildVisibleEdgeLayouts(segments, markers, segmentInstanceCount) {
|
|
2285
|
+
return appendVisibleEdgeLayouts(/* @__PURE__ */ new Map(), segments, markers, {
|
|
2286
|
+
markerStart: segmentInstanceCount,
|
|
2287
|
+
segmentStart: 0,
|
|
2288
|
+
segmentTotal: segmentInstanceCount
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2241
2291
|
/**
|
|
2242
2292
|
* Rewrites GPU edge instances for moved endpoints without repacking the whole viewport.
|
|
2243
2293
|
* Returns ok=false when any visible edge would change segment or marker counts.
|
|
@@ -2273,7 +2323,7 @@ function patchVisibleEdgeInstances(geometry, input, pickableSegments) {
|
|
|
2273
2323
|
for (const [offset, segment] of packed.segments.entries()) {
|
|
2274
2324
|
const slot = layout.segmentStart + offset;
|
|
2275
2325
|
writeEdgeSegmentInstance(geometry, slot, segment);
|
|
2276
|
-
nextPickable[slot] = {
|
|
2326
|
+
if (input.pickable !== false) nextPickable[slot] = {
|
|
2277
2327
|
edgeIndex,
|
|
2278
2328
|
hitSlop: Math.max(input.minHitSlop, segment.width * input.worldPerPixel / 2 + 2 * input.worldPerPixel),
|
|
2279
2329
|
x1: segment.x1,
|
|
@@ -3619,6 +3669,8 @@ var Graphraum = class {
|
|
|
3619
3669
|
depthOfFieldFocus = new DepthOfFieldFocus();
|
|
3620
3670
|
/** Strength actually applied: fades toward `strength` while enabled and toward 0 when disabled. */
|
|
3621
3671
|
depthOfFieldStrength = new Tween(0);
|
|
3672
|
+
/** The same for `nearStrength`, faded alongside. */
|
|
3673
|
+
depthOfFieldNearStrength = new Tween(0);
|
|
3622
3674
|
depthOfFieldPass = null;
|
|
3623
3675
|
depthOfFieldStatus = "off";
|
|
3624
3676
|
constructor(container, options = {}) {
|
|
@@ -3636,6 +3688,7 @@ var Graphraum = class {
|
|
|
3636
3688
|
if (options.depthOfField !== void 0) {
|
|
3637
3689
|
this.depthOfField = resolveDepthOfFieldOptions(options.depthOfField);
|
|
3638
3690
|
this.depthOfFieldStrength.retarget(this.depthOfFieldTargetStrength(), 0, resolveEasing("linear"));
|
|
3691
|
+
this.depthOfFieldNearStrength.retarget(this.depthOfFieldTargetNearStrength(), 0, resolveEasing("linear"));
|
|
3639
3692
|
}
|
|
3640
3693
|
this.antialias = options.antialias ?? false;
|
|
3641
3694
|
this.renderer = new WebGLRenderer({
|
|
@@ -3742,77 +3795,168 @@ var Graphraum = class {
|
|
|
3742
3795
|
const addedNodes = patch.addedNodes ?? [];
|
|
3743
3796
|
const addedEdges = patch.addedEdges ?? [];
|
|
3744
3797
|
if (addedNodes.length === 0 && addedEdges.length === 0) return;
|
|
3745
|
-
const
|
|
3746
|
-
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
const
|
|
3798
|
+
const nodes = this.data.nodes;
|
|
3799
|
+
const edges = this.data.edges;
|
|
3800
|
+
const previous = {
|
|
3801
|
+
edges: edges.length,
|
|
3802
|
+
nodes: nodes.length
|
|
3803
|
+
};
|
|
3804
|
+
const added = new Map(addedNodes.map((node) => [node.id, node]));
|
|
3805
|
+
const nodeById = (id) => added.get(id) ?? nodes[this.nodeIndices.get(id) ?? -1];
|
|
3806
|
+
const patchNodes = new Map(added);
|
|
3807
|
+
for (const edge of addedEdges) for (const id of [edge.source, edge.target]) {
|
|
3808
|
+
const node = nodeById(id);
|
|
3809
|
+
if (node && !patchNodes.has(id)) patchNodes.set(id, node);
|
|
3810
|
+
}
|
|
3753
3811
|
const compiled = compileGraph({
|
|
3754
|
-
nodes: patchNodes,
|
|
3812
|
+
nodes: [...patchNodes.values()],
|
|
3755
3813
|
edges: addedEdges
|
|
3756
3814
|
}, this.visuals);
|
|
3757
|
-
const nodeVisuals = new Map(patchNodes.map((
|
|
3758
|
-
const
|
|
3759
|
-
|
|
3760
|
-
...nodeVisuals.get(node.id)
|
|
3761
|
-
}))];
|
|
3762
|
-
const nextNodeIndices = new Map(this.nodeIndices);
|
|
3763
|
-
for (const [index, node] of nextNodes.entries()) nextNodeIndices.set(node.id, index);
|
|
3764
|
-
const nextEdges = [...this.data.edges, ...addedEdges.map((edge, index) => ({
|
|
3765
|
-
...edge,
|
|
3766
|
-
...compiled.edgeVisuals[index]
|
|
3767
|
-
}))];
|
|
3768
|
-
const nextEdgeIndices = new Uint32Array(nextEdges.length * 2);
|
|
3769
|
-
nextEdgeIndices.set(this.edgeNodeIndices);
|
|
3770
|
-
const nextEdgePositions = new Float32Array(nextEdges.length * 6);
|
|
3771
|
-
nextEdgePositions.set(this.canonicalEdgePositions);
|
|
3772
|
-
const nextIncidentEdges = this.incidentEdgeIndices.map((indices) => [...indices]);
|
|
3773
|
-
for (const [index, edge] of nextEdges.entries()) {
|
|
3774
|
-
const source = nextNodeIndices.get(edge.source);
|
|
3775
|
-
const target = nextNodeIndices.get(edge.target);
|
|
3776
|
-
if (source === void 0 || target === void 0) continue;
|
|
3777
|
-
nextEdgeIndices[index * 2] = source;
|
|
3778
|
-
nextEdgeIndices[index * 2 + 1] = target;
|
|
3779
|
-
if (!nextIncidentEdges[source]) nextIncidentEdges[source] = [];
|
|
3780
|
-
if (!nextIncidentEdges[target]) nextIncidentEdges[target] = [];
|
|
3781
|
-
if (index >= this.data.edges.length) {
|
|
3782
|
-
nextIncidentEdges[source]?.push(index);
|
|
3783
|
-
nextIncidentEdges[target]?.push(index);
|
|
3784
|
-
const sourceNode = nextNodes[source];
|
|
3785
|
-
const targetNode = nextNodes[target];
|
|
3786
|
-
if (sourceNode && targetNode) nextEdgePositions.set([
|
|
3787
|
-
sourceNode.position.x,
|
|
3788
|
-
sourceNode.position.y,
|
|
3789
|
-
sourceNode.position.z ?? 0,
|
|
3790
|
-
targetNode.position.x,
|
|
3791
|
-
targetNode.position.y,
|
|
3792
|
-
targetNode.position.z ?? 0
|
|
3793
|
-
], index * 6);
|
|
3794
|
-
}
|
|
3795
|
-
}
|
|
3796
|
-
this.data = {
|
|
3797
|
-
nodes: nextNodes,
|
|
3798
|
-
edges: nextEdges
|
|
3799
|
-
};
|
|
3800
|
-
this.nodeIds = nextNodes.map((node) => node.id);
|
|
3801
|
-
this.hasGlowNodes = nextNodes.some(hasNodeGlow);
|
|
3802
|
-
this.nodeIndices = nextNodeIndices;
|
|
3803
|
-
this.edgeNodeIndices = nextEdgeIndices;
|
|
3804
|
-
this.canonicalEdgePositions = nextEdgePositions;
|
|
3805
|
-
this.incidentEdgeIndices = nextIncidentEdges;
|
|
3815
|
+
const nodeVisuals = new Map([...patchNodes.keys()].map((id, index) => [id, compiled.nodeVisuals[index]]));
|
|
3816
|
+
const nodeIds = this.nodeIds;
|
|
3817
|
+
const incidentEdges = this.incidentEdgeIndices;
|
|
3806
3818
|
for (const node of addedNodes) {
|
|
3807
|
-
const index =
|
|
3808
|
-
|
|
3809
|
-
|
|
3819
|
+
const index = nodes.length;
|
|
3820
|
+
this.nodeIndices.set(node.id, index);
|
|
3821
|
+
nodeIds.push(node.id);
|
|
3822
|
+
nodes.push({
|
|
3823
|
+
...node,
|
|
3824
|
+
...nodeVisuals.get(node.id),
|
|
3825
|
+
position: { ...node.position }
|
|
3826
|
+
});
|
|
3827
|
+
incidentEdges.push([]);
|
|
3828
|
+
this.spatialGrid2d.set(index, nodes[index]);
|
|
3829
|
+
}
|
|
3830
|
+
this.edgeNodeIndices = grown(this.edgeNodeIndices, (previous.edges + addedEdges.length) * 2);
|
|
3831
|
+
this.canonicalEdgePositions = grown(this.canonicalEdgePositions, (previous.edges + addedEdges.length) * 6);
|
|
3832
|
+
for (const [offset, edge] of addedEdges.entries()) {
|
|
3833
|
+
const index = previous.edges + offset;
|
|
3834
|
+
const source = this.nodeIndices.get(edge.source);
|
|
3835
|
+
const target = this.nodeIndices.get(edge.target);
|
|
3836
|
+
edges.push({
|
|
3837
|
+
...edge,
|
|
3838
|
+
...compiled.edgeVisuals[offset]
|
|
3839
|
+
});
|
|
3840
|
+
if (source === void 0 || target === void 0) continue;
|
|
3841
|
+
this.edgeNodeIndices[index * 2] = source;
|
|
3842
|
+
this.edgeNodeIndices[index * 2 + 1] = target;
|
|
3843
|
+
incidentEdges[source]?.push(index);
|
|
3844
|
+
if (target !== source) incidentEdges[target]?.push(index);
|
|
3845
|
+
const sourceNode = nodes[source];
|
|
3846
|
+
const targetNode = nodes[target];
|
|
3847
|
+
if (!sourceNode || !targetNode) continue;
|
|
3848
|
+
this.canonicalEdgePositions.set([
|
|
3849
|
+
sourceNode.position.x,
|
|
3850
|
+
sourceNode.position.y,
|
|
3851
|
+
sourceNode.position.z ?? 0,
|
|
3852
|
+
targetNode.position.x,
|
|
3853
|
+
targetNode.position.y,
|
|
3854
|
+
targetNode.position.z ?? 0
|
|
3855
|
+
], index * 6);
|
|
3810
3856
|
}
|
|
3857
|
+
if (this.edgeIndexCache.edges === edges) for (const [offset, edge] of addedEdges.entries()) this.edgeIndexCache.indices.set(edge.id, previous.edges + offset);
|
|
3858
|
+
const addedGlow = addedNodes.some(hasNodeGlow);
|
|
3859
|
+
this.hasGlowNodes ||= addedGlow;
|
|
3811
3860
|
for (const [id, presentation] of compiled.nodePresentations) this.nodePresentations.set(id, presentation);
|
|
3812
3861
|
for (const [id, presentation] of compiled.edgePresentations) this.edgePresentations.set(id, presentation);
|
|
3813
|
-
this.
|
|
3862
|
+
if (this.materializeAppended(previous)) {
|
|
3863
|
+
if (addedGlow) this.syncGlow();
|
|
3864
|
+
} else this.materializeViewport();
|
|
3814
3865
|
this.requestRender();
|
|
3815
3866
|
}
|
|
3867
|
+
/**
|
|
3868
|
+
* Draws appended nodes and edges without repacking the ones already drawn. It applies only when the
|
|
3869
|
+
* last pass drew the whole graph in index order, so the appended ones land after the existing ones
|
|
3870
|
+
* exactly where a full pass would put them: 3D, or 2D without viewport culling, and no density or
|
|
3871
|
+
* overview tier before or after. Returns false, having written nothing, when it does not apply.
|
|
3872
|
+
*/
|
|
3873
|
+
materializeAppended(previous) {
|
|
3874
|
+
const { edgeMesh, nodeMesh, nodePickMesh } = this;
|
|
3875
|
+
if (!nodeMesh || !nodePickMesh || !edgeMesh) return false;
|
|
3876
|
+
const total = {
|
|
3877
|
+
edges: this.data.edges.length,
|
|
3878
|
+
nodes: this.data.nodes.length
|
|
3879
|
+
};
|
|
3880
|
+
const drewEverything = this.visibleNodeCount === previous.nodes && this.visibleEdgeCandidateCount === previous.edges;
|
|
3881
|
+
if (this.getViewportBounds2d() !== null || this.densityLodActive || this.lastEdgeLodTier !== "detail" || !drewEverything || total.nodes > this.maxVisibleNodes || total.edges > this.maxVisibleEdges) return false;
|
|
3882
|
+
const segmentsBefore = this.visibleEdgeSegmentCount;
|
|
3883
|
+
const markersBefore = this.visibleEdgeMarkerCount;
|
|
3884
|
+
const packed = packEdgeInstances({
|
|
3885
|
+
edgeIndices: Array.from({ length: total.edges - previous.edges }, (_, offset) => previous.edges + offset),
|
|
3886
|
+
edgeStates: this.edgeStateStyling(),
|
|
3887
|
+
edgeVisuals: this.data.edges,
|
|
3888
|
+
endpointPositions: this.canonicalEdgePositions,
|
|
3889
|
+
defaults: {
|
|
3890
|
+
color: this.theme.edge,
|
|
3891
|
+
opacity: this.theme.edgeOpacity,
|
|
3892
|
+
width: this.theme.edgeWidth
|
|
3893
|
+
},
|
|
3894
|
+
tier: this.lastEdgeLodTier,
|
|
3895
|
+
maxSegments: this.edgeSegmentCapacity - segmentsBefore,
|
|
3896
|
+
endpointAttach: this.theme.endpointAttach,
|
|
3897
|
+
edgeNodeIndices: this.edgeNodeIndices,
|
|
3898
|
+
mode: this.mode,
|
|
3899
|
+
nodeOutlines: this.data.nodes
|
|
3900
|
+
});
|
|
3901
|
+
const segmentsTotal = segmentsBefore + packed.segments.length;
|
|
3902
|
+
const markersTotal = markersBefore + packed.markers.length;
|
|
3903
|
+
if (packed.truncated || markersTotal > this.markerCapacity) return false;
|
|
3904
|
+
const firstNodeSlot = this.visibleNodeCount;
|
|
3905
|
+
const targets = this.nodeInstanceTargets();
|
|
3906
|
+
for (let index = previous.nodes; index < total.nodes; index += 1) {
|
|
3907
|
+
const materialized = this.plainMaterializedNode(index);
|
|
3908
|
+
const node = this.data.nodes[index];
|
|
3909
|
+
if (!materialized || !node) continue;
|
|
3910
|
+
const slot = firstNodeSlot + (index - previous.nodes);
|
|
3911
|
+
this.visibleNodeSlots.set(index, slot);
|
|
3912
|
+
this.visibleNodeIndices.push(index);
|
|
3913
|
+
this.writeNodeInstance(targets, slot, materialized, node);
|
|
3914
|
+
}
|
|
3915
|
+
const addedNodeCount = total.nodes - previous.nodes;
|
|
3916
|
+
nodeMesh.count = total.nodes;
|
|
3917
|
+
nodePickMesh.count = total.nodes;
|
|
3918
|
+
nodeMesh.instanceMatrix.addUpdateRange(firstNodeSlot * 16, addedNodeCount * 16);
|
|
3919
|
+
nodeMesh.instanceMatrix.needsUpdate = true;
|
|
3920
|
+
nodePickMesh.instanceMatrix.addUpdateRange(firstNodeSlot * 16, addedNodeCount * 16);
|
|
3921
|
+
nodePickMesh.instanceMatrix.needsUpdate = true;
|
|
3922
|
+
if (nodeMesh.instanceColor) {
|
|
3923
|
+
nodeMesh.instanceColor.addUpdateRange(firstNodeSlot * 3, addedNodeCount * 3);
|
|
3924
|
+
nodeMesh.instanceColor.needsUpdate = true;
|
|
3925
|
+
}
|
|
3926
|
+
targets.shape.addUpdateRange(firstNodeSlot, addedNodeCount);
|
|
3927
|
+
targets.shape.needsUpdate = true;
|
|
3928
|
+
targets.strokeWidth.addUpdateRange(firstNodeSlot, addedNodeCount);
|
|
3929
|
+
targets.strokeWidth.needsUpdate = true;
|
|
3930
|
+
targets.strokeColor.addUpdateRange(firstNodeSlot * 3, addedNodeCount * 3);
|
|
3931
|
+
targets.strokeColor.needsUpdate = true;
|
|
3932
|
+
const edgeGeometry = edgeMesh.geometry;
|
|
3933
|
+
if (packed.segments.length > 0 && markersBefore > 0) for (const name of EDGE_INSTANCE_ATTRIBUTES) {
|
|
3934
|
+
const attribute = getInstancedAttribute(edgeGeometry, name);
|
|
3935
|
+
attribute.array.copyWithin((segmentsBefore + packed.segments.length) * attribute.itemSize, segmentsBefore * attribute.itemSize, (segmentsBefore + markersBefore) * attribute.itemSize);
|
|
3936
|
+
}
|
|
3937
|
+
appendVisibleEdgeLayouts(this.visibleEdgeLayouts, packed.segments, packed.markers, {
|
|
3938
|
+
markerStart: segmentsTotal + markersBefore,
|
|
3939
|
+
segmentStart: segmentsBefore,
|
|
3940
|
+
segmentTotal: segmentsTotal
|
|
3941
|
+
});
|
|
3942
|
+
const addedEdgeCount = this.writeEdgeInstances(packed, {
|
|
3943
|
+
markerStart: segmentsTotal + markersBefore,
|
|
3944
|
+
segmentStart: segmentsBefore
|
|
3945
|
+
}, this.worldUnitsPerPixel());
|
|
3946
|
+
edgeMesh.count = segmentsTotal + markersTotal;
|
|
3947
|
+
for (const name of EDGE_INSTANCE_ATTRIBUTES) {
|
|
3948
|
+
const attribute = getInstancedAttribute(edgeGeometry, name);
|
|
3949
|
+
attribute.addUpdateRange(segmentsBefore * attribute.itemSize, (segmentsTotal + markersTotal - segmentsBefore) * attribute.itemSize);
|
|
3950
|
+
attribute.needsUpdate = true;
|
|
3951
|
+
}
|
|
3952
|
+
this.visibleNodeCandidateCount = total.nodes;
|
|
3953
|
+
this.visibleNodeCount = total.nodes;
|
|
3954
|
+
this.visibleEdgeCandidateCount = total.edges;
|
|
3955
|
+
this.visibleEdgeSegmentCount = segmentsTotal;
|
|
3956
|
+
this.visibleEdgeCount += addedEdgeCount;
|
|
3957
|
+
this.visibleEdgeMarkerCount = markersTotal;
|
|
3958
|
+
return true;
|
|
3959
|
+
}
|
|
3816
3960
|
replaceData(data, fitView) {
|
|
3817
3961
|
this.nodeDrag.cancel({ restore: false });
|
|
3818
3962
|
const compiled = compileGraph(data, this.visuals);
|
|
@@ -4000,13 +4144,8 @@ var Graphraum = class {
|
|
|
4000
4144
|
layouts: this.visibleEdgeLayouts,
|
|
4001
4145
|
minHitSlop,
|
|
4002
4146
|
mode: this.mode,
|
|
4003
|
-
nodeOutlines: this.data.nodes
|
|
4004
|
-
|
|
4005
|
-
shape: node.shape,
|
|
4006
|
-
size: node.size,
|
|
4007
|
-
strokeWidth: node.strokeWidth,
|
|
4008
|
-
width: node.width
|
|
4009
|
-
})),
|
|
4147
|
+
nodeOutlines: this.data.nodes,
|
|
4148
|
+
pickable: this.camera instanceof OrthographicCamera,
|
|
4010
4149
|
tier: this.lastEdgeLodTier,
|
|
4011
4150
|
worldPerPixel
|
|
4012
4151
|
}, this.pickableEdgeSegments);
|
|
@@ -4188,12 +4327,17 @@ var Graphraum = class {
|
|
|
4188
4327
|
if (this.depthOfField.enabled && !previous.enabled) this.depthOfFieldFocus.retarget(0, resolveEasing(easing));
|
|
4189
4328
|
const fadeMs = animate && !prefersReducedMotion() ? durationMs : 0;
|
|
4190
4329
|
const done = this.depthOfFieldStrength.retarget(this.depthOfFieldTargetStrength(), fadeMs, resolveEasing(easing));
|
|
4330
|
+
this.depthOfFieldNearStrength.retarget(this.depthOfFieldTargetNearStrength(), fadeMs, resolveEasing(easing));
|
|
4191
4331
|
this.requestRender();
|
|
4192
4332
|
return done;
|
|
4193
4333
|
}
|
|
4194
4334
|
depthOfFieldTargetStrength() {
|
|
4195
4335
|
return this.depthOfField.enabled ? this.depthOfField.strength : 0;
|
|
4196
4336
|
}
|
|
4337
|
+
depthOfFieldTargetNearStrength() {
|
|
4338
|
+
const { enabled, nearStrength, strength } = this.depthOfField;
|
|
4339
|
+
return enabled ? nearStrength ?? strength : 0;
|
|
4340
|
+
}
|
|
4197
4341
|
getNodePresentation(nodeId) {
|
|
4198
4342
|
return this.nodePresentations.get(nodeId) ?? null;
|
|
4199
4343
|
}
|
|
@@ -4684,6 +4828,7 @@ var Graphraum = class {
|
|
|
4684
4828
|
if (this.gpuTimer) for (const query of this.gpuTimer.pending) this.gpuTimer.context.deleteQuery(query);
|
|
4685
4829
|
this.disposeBackgroundTexture();
|
|
4686
4830
|
this.depthOfFieldStrength.cancel();
|
|
4831
|
+
this.depthOfFieldNearStrength.cancel();
|
|
4687
4832
|
this.disposeDepthOfFieldPass();
|
|
4688
4833
|
this.disposeGraphObjects();
|
|
4689
4834
|
this.renderer.dispose();
|
|
@@ -4734,19 +4879,22 @@ var Graphraum = class {
|
|
|
4734
4879
|
prepareDepthOfField(nowMs) {
|
|
4735
4880
|
const options = this.depthOfField;
|
|
4736
4881
|
const strengthTween = this.depthOfFieldStrength;
|
|
4737
|
-
|
|
4882
|
+
const nearStrengthTween = this.depthOfFieldNearStrength;
|
|
4883
|
+
if (!options.enabled && strengthTween.settled && nearStrengthTween.settled) {
|
|
4738
4884
|
this.depthOfFieldStatus = "off";
|
|
4739
4885
|
this.disposeDepthOfFieldPass();
|
|
4740
4886
|
return null;
|
|
4741
4887
|
}
|
|
4742
4888
|
if (!(this.camera instanceof PerspectiveCamera)) {
|
|
4743
4889
|
strengthTween.jump();
|
|
4890
|
+
nearStrengthTween.jump();
|
|
4744
4891
|
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4745
4892
|
this.disposeDepthOfFieldPass();
|
|
4746
4893
|
return null;
|
|
4747
4894
|
}
|
|
4748
4895
|
if (!DepthOfFieldPass.isSupported(this.renderer)) {
|
|
4749
4896
|
strengthTween.jump();
|
|
4897
|
+
nearStrengthTween.jump();
|
|
4750
4898
|
this.depthOfFieldStatus = options.enabled ? "unsupported" : "off";
|
|
4751
4899
|
if (options.enabled && !warnedDepthOfFieldUnsupported) {
|
|
4752
4900
|
warnedDepthOfFieldUnsupported = true;
|
|
@@ -4755,7 +4903,8 @@ var Graphraum = class {
|
|
|
4755
4903
|
return null;
|
|
4756
4904
|
}
|
|
4757
4905
|
const strength = strengthTween.sample(nowMs);
|
|
4758
|
-
|
|
4906
|
+
const nearStrength = nearStrengthTween.sample(nowMs);
|
|
4907
|
+
if (!strengthTween.settled || !nearStrengthTween.settled) this.requestRender();
|
|
4759
4908
|
this.camera.updateMatrixWorld();
|
|
4760
4909
|
let focusDistance = typeof options.focus === "number" ? options.focus : null;
|
|
4761
4910
|
let amount = 1;
|
|
@@ -4771,9 +4920,11 @@ var Graphraum = class {
|
|
|
4771
4920
|
}
|
|
4772
4921
|
focusDistance = Math.max(focusDistance, this.camera.near);
|
|
4773
4922
|
const pixelRatio = this.renderer.getPixelRatio();
|
|
4774
|
-
const
|
|
4923
|
+
const pixelsPerStrength = amount * distanceFade(focusDistance, options.nearDistance, options.farDistance) * 24 * pixelRatio;
|
|
4924
|
+
const aperturePixels = strength * pixelsPerStrength;
|
|
4925
|
+
const nearAperturePixels = nearStrength * pixelsPerStrength;
|
|
4775
4926
|
const maxBlurPixels = options.maxBlur * pixelRatio;
|
|
4776
|
-
if (aperturePixels < .25 || maxBlurPixels < .25) {
|
|
4927
|
+
if (Math.max(aperturePixels, nearAperturePixels) < .25 || maxBlurPixels < .25) {
|
|
4777
4928
|
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4778
4929
|
return null;
|
|
4779
4930
|
}
|
|
@@ -4783,7 +4934,8 @@ var Graphraum = class {
|
|
|
4783
4934
|
aperturePixels,
|
|
4784
4935
|
focusDistance,
|
|
4785
4936
|
maxBlurPixels,
|
|
4786
|
-
minBlurPixels: options.minBlur * pixelRatio
|
|
4937
|
+
minBlurPixels: options.minBlur * pixelRatio,
|
|
4938
|
+
nearAperturePixels
|
|
4787
4939
|
};
|
|
4788
4940
|
}
|
|
4789
4941
|
/** Mean world position of the selected nodes, read every frame so a running layout keeps them in focus. */
|
|
@@ -4909,7 +5061,7 @@ var Graphraum = class {
|
|
|
4909
5061
|
return null;
|
|
4910
5062
|
}
|
|
4911
5063
|
pickEdge(clientX, clientY) {
|
|
4912
|
-
if (!this.edgeMesh || this.
|
|
5064
|
+
if (!this.edgeMesh || this.visibleEdgeSegmentCount === 0) return null;
|
|
4913
5065
|
const bounds = this.renderer.domElement.getBoundingClientRect();
|
|
4914
5066
|
if (bounds.width === 0 || bounds.height === 0) return null;
|
|
4915
5067
|
this.pointer.set((clientX - bounds.left) / bounds.width * 2 - 1, -((clientY - bounds.top) / bounds.height) * 2 + 1);
|
|
@@ -5077,35 +5229,20 @@ var Graphraum = class {
|
|
|
5077
5229
|
const visibleNodeIndices = materializedNodes.map(({ index }) => index);
|
|
5078
5230
|
const edgeCandidates = bounds ? collectIncidentEdges(visibleNodeIndices, this.incidentEdgeIndices) : this.data.edges.map((_, index) => index);
|
|
5079
5231
|
const visibleEdgeIndices = applyEdgeBudget(edgeCandidates, this.maxVisibleEdges);
|
|
5080
|
-
const matrix = new Matrix4();
|
|
5081
5232
|
this.visibleNodeSlots = /* @__PURE__ */ new Map();
|
|
5082
5233
|
this.visibleNodeIndices = visibleNodeIndices;
|
|
5083
5234
|
this.nodeMesh.instanceMatrix.clearUpdateRanges();
|
|
5084
5235
|
this.nodePickMesh.instanceMatrix.clearUpdateRanges();
|
|
5085
5236
|
this.nodeMesh.instanceColor?.clearUpdateRanges();
|
|
5086
|
-
const
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
nodeStrokeWidth.clearUpdateRanges();
|
|
5091
|
-
nodeStrokeColor.clearUpdateRanges();
|
|
5237
|
+
const targets = this.nodeInstanceTargets();
|
|
5238
|
+
targets.shape.clearUpdateRanges();
|
|
5239
|
+
targets.strokeWidth.clearUpdateRanges();
|
|
5240
|
+
targets.strokeColor.clearUpdateRanges();
|
|
5092
5241
|
for (const [slot, materialized] of materializedNodes.entries()) {
|
|
5093
|
-
const
|
|
5094
|
-
const node = this.data.nodes[nodeIndex];
|
|
5242
|
+
const node = this.data.nodes[materialized.index];
|
|
5095
5243
|
if (!node) continue;
|
|
5096
|
-
this.visibleNodeSlots.set(
|
|
5097
|
-
|
|
5098
|
-
const maxAxis = Math.max(width, height);
|
|
5099
|
-
matrix.makeScale(width, height, maxAxis);
|
|
5100
|
-
matrix.setPosition(materialized.x, materialized.y, materialized.z);
|
|
5101
|
-
this.nodeMesh.setMatrixAt(slot, matrix);
|
|
5102
|
-
const pickScale = maxAxis * Math.SQRT2;
|
|
5103
|
-
matrix.makeScale(pickScale, pickScale, pickScale);
|
|
5104
|
-
matrix.setPosition(materialized.x, materialized.y, materialized.z);
|
|
5105
|
-
this.nodePickMesh.setMatrixAt(slot, matrix);
|
|
5106
|
-
this.nodeMesh.setColorAt(slot, this.getNodeColor(node));
|
|
5107
|
-
setNodeShapeAt(nodeShape, slot, node.shape);
|
|
5108
|
-
setNodeStrokeAt(nodeStrokeWidth, nodeStrokeColor, slot, resolveNodeStroke(node, this.theme));
|
|
5244
|
+
this.visibleNodeSlots.set(materialized.index, slot);
|
|
5245
|
+
this.writeNodeInstance(targets, slot, materialized, node);
|
|
5109
5246
|
}
|
|
5110
5247
|
this.nodeMesh.count = materializedNodes.length;
|
|
5111
5248
|
this.nodePickMesh.count = materializedNodes.length;
|
|
@@ -5117,12 +5254,12 @@ var Graphraum = class {
|
|
|
5117
5254
|
this.nodeMesh.instanceColor.addUpdateRange(0, materializedNodes.length * 3);
|
|
5118
5255
|
this.nodeMesh.instanceColor.needsUpdate = true;
|
|
5119
5256
|
}
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5257
|
+
targets.shape.addUpdateRange(0, materializedNodes.length);
|
|
5258
|
+
targets.shape.needsUpdate = true;
|
|
5259
|
+
targets.strokeWidth.addUpdateRange(0, materializedNodes.length);
|
|
5260
|
+
targets.strokeWidth.needsUpdate = true;
|
|
5261
|
+
targets.strokeColor.addUpdateRange(0, materializedNodes.length * 3);
|
|
5262
|
+
targets.strokeColor.needsUpdate = true;
|
|
5126
5263
|
const lodLevel = resolveLodLevel(this.densityLodActive, edgeCandidates.length, visibleEdgeIndices.length);
|
|
5127
5264
|
this.lastEdgeLodTier = edgeTierFromDiagnosticsLod(lodLevel);
|
|
5128
5265
|
const worldPerPixel = this.worldUnitsPerPixel();
|
|
@@ -5141,42 +5278,19 @@ var Graphraum = class {
|
|
|
5141
5278
|
endpointAttach: this.theme.endpointAttach,
|
|
5142
5279
|
edgeNodeIndices: this.edgeNodeIndices,
|
|
5143
5280
|
mode: this.mode,
|
|
5144
|
-
nodeOutlines: this.data.nodes
|
|
5145
|
-
height: node.height,
|
|
5146
|
-
shape: node.shape,
|
|
5147
|
-
size: node.size,
|
|
5148
|
-
strokeWidth: node.strokeWidth,
|
|
5149
|
-
width: node.width
|
|
5150
|
-
}))
|
|
5281
|
+
nodeOutlines: this.data.nodes
|
|
5151
5282
|
});
|
|
5152
5283
|
const edgeGeometry = this.edgeMesh.geometry;
|
|
5153
5284
|
this.visibleEdgeSlots = /* @__PURE__ */ new Map();
|
|
5154
5285
|
this.visibleEdgeLayouts = buildVisibleEdgeLayouts(packed.segments, packed.markers, packed.segments.length);
|
|
5155
5286
|
this.pickableEdgeSegments = [];
|
|
5156
5287
|
this.visibleEdgeSegmentEdgeIndices = [];
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
distinctEdges.add(segment.edgeIndex);
|
|
5164
|
-
this.visibleEdgeSegmentEdgeIndices.push(segment.edgeIndex);
|
|
5165
|
-
this.pickableEdgeSegments.push({
|
|
5166
|
-
edgeIndex: segment.edgeIndex,
|
|
5167
|
-
hitSlop: Math.max(minHitSlop, segment.width * worldPerPixel / 2 + 2 * worldPerPixel),
|
|
5168
|
-
x1: segment.x1,
|
|
5169
|
-
y1: segment.y1,
|
|
5170
|
-
x2: segment.x2,
|
|
5171
|
-
y2: segment.y2
|
|
5172
|
-
});
|
|
5173
|
-
edgeSlot += 1;
|
|
5174
|
-
}
|
|
5175
|
-
const segmentCount = edgeSlot;
|
|
5176
|
-
for (const marker of packed.markers) {
|
|
5177
|
-
writeEdgeMarkerInstance(edgeGeometry, edgeSlot, marker);
|
|
5178
|
-
edgeSlot += 1;
|
|
5179
|
-
}
|
|
5288
|
+
const distinctEdgeCount = this.writeEdgeInstances(packed, {
|
|
5289
|
+
markerStart: packed.segments.length,
|
|
5290
|
+
segmentStart: 0
|
|
5291
|
+
}, worldPerPixel);
|
|
5292
|
+
const segmentCount = packed.segments.length;
|
|
5293
|
+
const edgeSlot = segmentCount + packed.markers.length;
|
|
5180
5294
|
this.edgeMesh.count = edgeSlot;
|
|
5181
5295
|
for (const name of EDGE_INSTANCE_ATTRIBUTES) {
|
|
5182
5296
|
const attribute = edgeGeometry.getAttribute(name);
|
|
@@ -5188,28 +5302,85 @@ var Graphraum = class {
|
|
|
5188
5302
|
this.visibleNodeCount = materializedNodes.length;
|
|
5189
5303
|
this.visibleEdgeCandidateCount = edgeCandidates.length;
|
|
5190
5304
|
this.visibleEdgeSegmentCount = segmentCount;
|
|
5191
|
-
this.visibleEdgeCount =
|
|
5305
|
+
this.visibleEdgeCount = distinctEdgeCount;
|
|
5192
5306
|
this.visibleEdgeMarkerCount = packed.markers.length;
|
|
5193
5307
|
this.syncGlow();
|
|
5194
5308
|
}
|
|
5309
|
+
/** A node drawn at its own position and size, which is every node outside the density tier. */
|
|
5310
|
+
plainMaterializedNode(index) {
|
|
5311
|
+
const node = this.data.nodes[index];
|
|
5312
|
+
if (!node) return null;
|
|
5313
|
+
const { height, width } = resolveNodeAxes({
|
|
5314
|
+
height: node.height,
|
|
5315
|
+
nodeId: node.id,
|
|
5316
|
+
size: node.size,
|
|
5317
|
+
width: node.width
|
|
5318
|
+
});
|
|
5319
|
+
return {
|
|
5320
|
+
height,
|
|
5321
|
+
index,
|
|
5322
|
+
width,
|
|
5323
|
+
x: node.position.x,
|
|
5324
|
+
y: node.position.y,
|
|
5325
|
+
z: node.position.z ?? 0
|
|
5326
|
+
};
|
|
5327
|
+
}
|
|
5328
|
+
nodeInstanceTargets() {
|
|
5329
|
+
const geometry = this.nodeMesh.geometry;
|
|
5330
|
+
return {
|
|
5331
|
+
matrix: new Matrix4(),
|
|
5332
|
+
shape: geometry.getAttribute("instanceShape"),
|
|
5333
|
+
strokeColor: geometry.getAttribute("instanceStrokeColor"),
|
|
5334
|
+
strokeWidth: geometry.getAttribute("instanceStrokeWidth")
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
/** Writes one node's matrix, pick matrix, color, shape and stroke into `slot`. */
|
|
5338
|
+
writeNodeInstance({ matrix, shape, strokeColor, strokeWidth }, slot, materialized, node) {
|
|
5339
|
+
const { height, width } = materialized;
|
|
5340
|
+
const maxAxis = Math.max(width, height);
|
|
5341
|
+
matrix.makeScale(width, height, maxAxis);
|
|
5342
|
+
matrix.setPosition(materialized.x, materialized.y, materialized.z);
|
|
5343
|
+
this.nodeMesh.setMatrixAt(slot, matrix);
|
|
5344
|
+
const pickScale = maxAxis * Math.SQRT2;
|
|
5345
|
+
matrix.makeScale(pickScale, pickScale, pickScale);
|
|
5346
|
+
matrix.setPosition(materialized.x, materialized.y, materialized.z);
|
|
5347
|
+
this.nodePickMesh.setMatrixAt(slot, matrix);
|
|
5348
|
+
this.nodeMesh.setColorAt(slot, this.getNodeColor(node));
|
|
5349
|
+
setNodeShapeAt(shape, slot, node.shape);
|
|
5350
|
+
setNodeStrokeAt(strokeWidth, strokeColor, slot, resolveNodeStroke(node, this.theme));
|
|
5351
|
+
}
|
|
5352
|
+
/**
|
|
5353
|
+
* Writes packed edge instances at the given first slots and records what picking needs for them.
|
|
5354
|
+
* Returns how many distinct edges got a segment.
|
|
5355
|
+
*/
|
|
5356
|
+
writeEdgeInstances(packed, start, worldPerPixel) {
|
|
5357
|
+
const edgeGeometry = this.edgeMesh.geometry;
|
|
5358
|
+
const minHitSlop = Math.max(4 * worldPerPixel, this.theme.edgeWidth * worldPerPixel);
|
|
5359
|
+
const distinctEdges = /* @__PURE__ */ new Set();
|
|
5360
|
+
const keepPickable = this.camera instanceof OrthographicCamera;
|
|
5361
|
+
for (const [offset, segment] of packed.segments.entries()) {
|
|
5362
|
+
const slot = start.segmentStart + offset;
|
|
5363
|
+
writeEdgeSegmentInstance(edgeGeometry, slot, segment);
|
|
5364
|
+
if (!this.visibleEdgeSlots.has(segment.edgeIndex)) this.visibleEdgeSlots.set(segment.edgeIndex, slot);
|
|
5365
|
+
distinctEdges.add(segment.edgeIndex);
|
|
5366
|
+
this.visibleEdgeSegmentEdgeIndices.push(segment.edgeIndex);
|
|
5367
|
+
if (!keepPickable) continue;
|
|
5368
|
+
this.pickableEdgeSegments.push({
|
|
5369
|
+
edgeIndex: segment.edgeIndex,
|
|
5370
|
+
hitSlop: Math.max(minHitSlop, segment.width * worldPerPixel / 2 + 2 * worldPerPixel),
|
|
5371
|
+
x1: segment.x1,
|
|
5372
|
+
y1: segment.y1,
|
|
5373
|
+
x2: segment.x2,
|
|
5374
|
+
y2: segment.y2
|
|
5375
|
+
});
|
|
5376
|
+
}
|
|
5377
|
+
for (const [offset, marker] of packed.markers.entries()) writeEdgeMarkerInstance(edgeGeometry, start.markerStart + offset, marker);
|
|
5378
|
+
return distinctEdges.size;
|
|
5379
|
+
}
|
|
5195
5380
|
materializeNodes(indices, bounds) {
|
|
5196
5381
|
if (!this.densityLodActive || !bounds) return indices.slice(0, this.maxVisibleNodes).flatMap((index) => {
|
|
5197
|
-
const
|
|
5198
|
-
|
|
5199
|
-
const { height, width } = resolveNodeAxes({
|
|
5200
|
-
height: node.height,
|
|
5201
|
-
nodeId: node.id,
|
|
5202
|
-
size: node.size,
|
|
5203
|
-
width: node.width
|
|
5204
|
-
});
|
|
5205
|
-
return [{
|
|
5206
|
-
height,
|
|
5207
|
-
index,
|
|
5208
|
-
width,
|
|
5209
|
-
x: node.position.x,
|
|
5210
|
-
y: node.position.y,
|
|
5211
|
-
z: node.position.z ?? 0
|
|
5212
|
-
}];
|
|
5382
|
+
const materialized = this.plainMaterializedNode(index);
|
|
5383
|
+
return materialized ? [materialized] : [];
|
|
5213
5384
|
});
|
|
5214
5385
|
const aspect = Math.max(this.container.clientWidth, 1) / Math.max(this.container.clientHeight, 1);
|
|
5215
5386
|
const columns = Math.max(1, Math.floor(Math.sqrt(this.maxVisibleNodes * aspect)));
|
|
@@ -5311,6 +5482,13 @@ function setOrthographicFrustum(camera, aspect, visibleHeight) {
|
|
|
5311
5482
|
function hasNodeGlow(node) {
|
|
5312
5483
|
return (node.glow ?? 0) > 0;
|
|
5313
5484
|
}
|
|
5485
|
+
/** `array`, or a copy with room for at least `length` entries, doubling so repeated appends stay cheap. */
|
|
5486
|
+
function grown(array, length) {
|
|
5487
|
+
if (array.length >= length) return array;
|
|
5488
|
+
const next = new array.constructor(nextCapacity(length));
|
|
5489
|
+
next.set(array);
|
|
5490
|
+
return next;
|
|
5491
|
+
}
|
|
5314
5492
|
function nextCapacity(length) {
|
|
5315
5493
|
let capacity = 1;
|
|
5316
5494
|
while (capacity < length) capacity *= 2;
|