@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 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 radius = aperture * abs(surface - focusDistance) / max(surface, 1e-6);
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
- ivec2 base = ivec2(gl_FragCoord.xy) * 2;
1533
- float centreSurface = surfaceDistance(base);
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
- float rotation = 6.28318531 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
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 = sqrt((float(index) + 0.5) / ${BLUR_TAPS}.0) * maxBlur;
1542
- float angle = float(index) * GOLDEN_ANGLE + rotation;
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 weight = clamp(radius - distanceFromCentre + 1.0, 0.0, 1.0);
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 (surface < focusDistance && surface < centreSurface) {
1551
- near += color * weight * (maxBlur * maxBlur / max(radius * radius, 1.0));
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
- /** Records GPU slot ranges for each visible edge after a full viewport materialize. */
2214
- function buildVisibleEdgeLayouts(segments, markers, segmentInstanceCount) {
2215
- const layouts = /* @__PURE__ */ new Map();
2216
- for (const [slot, segment] of segments.entries()) {
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: segmentInstanceCount,
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 = segmentInstanceCount + offset;
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 nodeById = new Map(this.data.nodes.map((node) => [node.id, node]));
3746
- for (const node of addedNodes) nodeById.set(node.id, node);
3747
- const edgeEndpointNodes = addedEdges.flatMap((edge) => {
3748
- const source = nodeById.get(edge.source);
3749
- const target = nodeById.get(edge.target);
3750
- return source && target ? [source, target] : [];
3751
- });
3752
- const patchNodes = [...addedNodes, ...edgeEndpointNodes].filter((node, index, all) => all.findIndex((candidate) => candidate.id === node.id) === index);
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((node, index) => [node.id, compiled.nodeVisuals[index]]));
3758
- const nextNodes = [...this.data.nodes, ...addedNodes.map((node) => ({
3759
- ...node,
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 = nextNodeIndices.get(node.id);
3808
- const nextNode = index === void 0 ? void 0 : this.data.nodes[index];
3809
- if (index !== void 0 && nextNode) this.spatialGrid2d.set(index, nextNode);
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.materializeViewport();
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.map((node) => ({
4004
- height: node.height,
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
- if (!options.enabled && strengthTween.settled) {
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
- if (!strengthTween.settled) this.requestRender();
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 aperturePixels = strength * amount * distanceFade(focusDistance, options.nearDistance, options.farDistance) * 24 * pixelRatio;
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.pickableEdgeSegments.length === 0) return null;
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 nodeShape = this.nodeMesh.geometry.getAttribute("instanceShape");
5087
- const nodeStrokeWidth = this.nodeMesh.geometry.getAttribute("instanceStrokeWidth");
5088
- const nodeStrokeColor = this.nodeMesh.geometry.getAttribute("instanceStrokeColor");
5089
- nodeShape.clearUpdateRanges();
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 nodeIndex = materialized.index;
5094
- const node = this.data.nodes[nodeIndex];
5242
+ const node = this.data.nodes[materialized.index];
5095
5243
  if (!node) continue;
5096
- this.visibleNodeSlots.set(nodeIndex, slot);
5097
- const { height, width } = materialized;
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
- nodeShape.addUpdateRange(0, materializedNodes.length);
5121
- nodeShape.needsUpdate = true;
5122
- nodeStrokeWidth.addUpdateRange(0, materializedNodes.length);
5123
- nodeStrokeWidth.needsUpdate = true;
5124
- nodeStrokeColor.addUpdateRange(0, materializedNodes.length * 3);
5125
- nodeStrokeColor.needsUpdate = true;
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.map((node) => ({
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
- let edgeSlot = 0;
5158
- const distinctEdges = /* @__PURE__ */ new Set();
5159
- const minHitSlop = Math.max(4 * worldPerPixel, this.theme.edgeWidth * worldPerPixel);
5160
- for (const segment of packed.segments) {
5161
- writeEdgeSegmentInstance(edgeGeometry, edgeSlot, segment);
5162
- if (!this.visibleEdgeSlots.has(segment.edgeIndex)) this.visibleEdgeSlots.set(segment.edgeIndex, edgeSlot);
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 = distinctEdges.size;
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 node = this.data.nodes[index];
5198
- if (!node) return [];
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yggy-tech/graphraum",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "yggy's WebGL engine for interactive graphs in 2D and 3D",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",