@yggy-tech/graphraum 0.36.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 +49 -1
- package/dist/index.js +624 -168
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,16 @@ interface GraphraumSetDataOptions {
|
|
|
80
80
|
*/
|
|
81
81
|
fit?: boolean;
|
|
82
82
|
}
|
|
83
|
+
type GraphraumNodeDragPhase = "start" | "move" | "end" | "cancel";
|
|
84
|
+
/** One step of a node drag. `cancel` carries the position the node was restored to (or left at). */
|
|
85
|
+
interface GraphraumNodeDragEvent {
|
|
86
|
+
nodeId: string;
|
|
87
|
+
phase: GraphraumNodeDragPhase;
|
|
88
|
+
position: GraphraumPosition;
|
|
89
|
+
}
|
|
90
|
+
interface GraphraumNodeDragOptions {
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
}
|
|
83
93
|
interface GraphraumCameraPoint3d {
|
|
84
94
|
x: number;
|
|
85
95
|
y: number;
|
|
@@ -294,6 +304,12 @@ interface GraphraumDepthOfFieldOptions {
|
|
|
294
304
|
* `d` is its camera distance and `f` the focal distance, clamped to `maxBlur`.
|
|
295
305
|
*/
|
|
296
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;
|
|
297
313
|
/** Largest blur radius in CSS pixels. Default 12. */
|
|
298
314
|
maxBlur?: number;
|
|
299
315
|
/** Blur radii below this many CSS pixels render sharp, which widens the in-focus band. Default 0. */
|
|
@@ -618,12 +634,16 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
618
634
|
private gpuTimer;
|
|
619
635
|
private readonly viewListeners;
|
|
620
636
|
private readonly focusListeners;
|
|
637
|
+
private readonly nodeDragListeners;
|
|
638
|
+
private readonly nodeDrag;
|
|
621
639
|
private lastHoveredNodeId;
|
|
622
640
|
private readonly antialias;
|
|
623
641
|
private depthOfField;
|
|
624
642
|
private readonly depthOfFieldFocus;
|
|
625
643
|
/** Strength actually applied: fades toward `strength` while enabled and toward 0 when disabled. */
|
|
626
644
|
private readonly depthOfFieldStrength;
|
|
645
|
+
/** The same for `nearStrength`, faded alongside. */
|
|
646
|
+
private readonly depthOfFieldNearStrength;
|
|
627
647
|
private depthOfFieldPass;
|
|
628
648
|
private depthOfFieldStatus;
|
|
629
649
|
constructor(container: HTMLElement, options?: GraphraumOptions<NodeAttributes, EdgeAttributes>);
|
|
@@ -635,6 +655,13 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
635
655
|
private canMutateDataInPlace;
|
|
636
656
|
private mutateDataInPlace;
|
|
637
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;
|
|
638
665
|
private replaceData;
|
|
639
666
|
updateNodes(updates: readonly GraphraumNodeUpdate[]): void;
|
|
640
667
|
/** Applies a transferable XYZ layout batch. Call repeatedly as a worker produces progressive positions. */
|
|
@@ -697,6 +724,7 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
697
724
|
*/
|
|
698
725
|
setDepthOfField(options: GraphraumDepthOfFieldOptions, update?: GraphraumDepthOfFieldUpdate): Promise<boolean>;
|
|
699
726
|
private depthOfFieldTargetStrength;
|
|
727
|
+
private depthOfFieldTargetNearStrength;
|
|
700
728
|
getNodePresentation(nodeId: string): CompiledGraphraumPresentation | null;
|
|
701
729
|
/** Returns canvas-relative screen coordinates for a node, or null when the node does not exist. */
|
|
702
730
|
getNodeScreenPosition(nodeId: string): GraphraumScreenPosition | null;
|
|
@@ -715,6 +743,16 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
715
743
|
createOverlay(options?: GraphraumOverlayOptions): GraphraumOverlay<NodeAttributes, EdgeAttributes>;
|
|
716
744
|
/** Subscribes to camera and viewport changes. Returns an unsubscribe function. */
|
|
717
745
|
onViewChange(listener: () => void): () => boolean;
|
|
746
|
+
/**
|
|
747
|
+
* Turns node dragging on or off (default off). A drag starts when the pointer moves past a small
|
|
748
|
+
* tolerance after a press on a node (a finger must rest on the node first, so a swipe still pans
|
|
749
|
+
* the camera), moves the node on a plane facing the camera, and ends on release. Escape or a
|
|
750
|
+
* cancelled pointer put the node back. Not available while the density LOD shows clusters.
|
|
751
|
+
*/
|
|
752
|
+
setNodeDrag(options: GraphraumNodeDragOptions): void;
|
|
753
|
+
/** Subscribes to node drag steps (`start`, `move`, `end`, `cancel`). Returns an unsubscribe function. */
|
|
754
|
+
onNodeDrag(listener: (event: GraphraumNodeDragEvent) => void): () => boolean;
|
|
755
|
+
private createNodeDragHost;
|
|
718
756
|
/** Subscribes to selection and hover changes that affect overlay focus chrome. */
|
|
719
757
|
onFocusChange(listener: () => void): () => boolean;
|
|
720
758
|
private notifyFocusChange;
|
|
@@ -836,6 +874,16 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
836
874
|
private createBackgroundTexture;
|
|
837
875
|
private applyBackground;
|
|
838
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;
|
|
839
887
|
private materializeNodes;
|
|
840
888
|
private disposeGraphObjects;
|
|
841
889
|
}
|
|
@@ -983,4 +1031,4 @@ declare function isTransparentGraphraumBackground(background: GraphraumBackgroun
|
|
|
983
1031
|
/** Defines typed, serializable graph visuals without coupling application data to the render loop. */
|
|
984
1032
|
declare function defineVisuals<NodeAttributes = undefined, EdgeAttributes = undefined>(visuals: GraphraumVisualMapper<NodeAttributes, EdgeAttributes>): GraphraumVisualMapper<NodeAttributes, EdgeAttributes>;
|
|
985
1033
|
//#endregion
|
|
986
|
-
export { type CompiledGraphraumPresentation, DEFAULT_ENDPOINT_ATTACH, DEFAULT_ENDPOINT_CLEARANCE, DEFAULT_FORCE_SETTINGS, type EndpointAttach, type EndpointOutline, type ForceLayoutRequest, type ForceSettings, type GraphologyEdgeAttributes, type GraphologyNodeAttributes, type GraphologyRenderer, Graphraum, type GraphraumAutoOrbitOptions, type GraphraumAutoOrbitStatus, type GraphraumBackground, type GraphraumCameraPoint3d, type GraphraumCameraState, type GraphraumCameraState2d, type GraphraumCameraState3d, type GraphraumColor, type GraphraumData, type GraphraumDataPatch, type GraphraumDepthOfFieldOptions, type GraphraumDepthOfFieldStatus, type GraphraumDepthOfFieldTransition, type GraphraumDepthOfFieldUpdate, type GraphraumDiagnostics, type GraphraumEasing, type GraphraumEdge, type GraphraumEdgeEncoding, type GraphraumEdgeMarker, type GraphraumEdgeMarkerEnd, type GraphraumEdgePath, type GraphraumEdgeState, type GraphraumEdgeStyle, type GraphraumEdgeVisual, type GraphraumFlyToOptions, type GraphraumLabelCandidate, type GraphraumLayoutPositions, type GraphraumMode, type GraphraumNode, type GraphraumNodeEncoding, type GraphraumNodeGeometry, type GraphraumNodeShape, type GraphraumNodeState, type GraphraumNodeUpdate, type GraphraumNodeVisual, type GraphraumOptions, GraphraumOverlay, type GraphraumOverlayNode, type GraphraumOverlayOptions, type GraphraumPatternBackground, type GraphraumPickHit, type GraphraumPosition, type GraphraumPresentation, type GraphraumPresentationAction, type GraphraumPresentationProperty, type GraphraumPropertyValue, type GraphraumScreenPosition, type GraphraumSetDataOptions, type GraphraumTheme, type GraphraumThemeName, type GraphraumVisualMapper, type LabelBudgetCandidate, type RichNodePolicy, type SelectBudgetedLabelIdsInput, type SelectFocusLabelIdsInput, attachPointOnOutline, bindGraphology, boundOverlayIds, computeClusteredForcePositions, computeForcePositions, createForceSimulation, defineVisuals, forceIterationCount, graphraumEdgePaths, graphraumNodeShapes, graphraumTheme, graphraumThemeDark, graphraumThemeLight, graphraumThemes, isTransparentGraphraumBackground, normalizeGraphraumBackground, orderFocusNodeIds, resolveGraphraumTheme, selectBudgetedLabelIds, selectFocusLabelIds, selectRichNodeIds, trimEdgeEndpoints };
|
|
1034
|
+
export { type CompiledGraphraumPresentation, DEFAULT_ENDPOINT_ATTACH, DEFAULT_ENDPOINT_CLEARANCE, DEFAULT_FORCE_SETTINGS, type EndpointAttach, type EndpointOutline, type ForceLayoutRequest, type ForceSettings, type GraphologyEdgeAttributes, type GraphologyNodeAttributes, type GraphologyRenderer, Graphraum, type GraphraumAutoOrbitOptions, type GraphraumAutoOrbitStatus, type GraphraumBackground, type GraphraumCameraPoint3d, type GraphraumCameraState, type GraphraumCameraState2d, type GraphraumCameraState3d, type GraphraumColor, type GraphraumData, type GraphraumDataPatch, type GraphraumDepthOfFieldOptions, type GraphraumDepthOfFieldStatus, type GraphraumDepthOfFieldTransition, type GraphraumDepthOfFieldUpdate, type GraphraumDiagnostics, type GraphraumEasing, type GraphraumEdge, type GraphraumEdgeEncoding, type GraphraumEdgeMarker, type GraphraumEdgeMarkerEnd, type GraphraumEdgePath, type GraphraumEdgeState, type GraphraumEdgeStyle, type GraphraumEdgeVisual, type GraphraumFlyToOptions, type GraphraumLabelCandidate, type GraphraumLayoutPositions, type GraphraumMode, type GraphraumNode, type GraphraumNodeDragEvent, type GraphraumNodeDragOptions, type GraphraumNodeDragPhase, type GraphraumNodeEncoding, type GraphraumNodeGeometry, type GraphraumNodeShape, type GraphraumNodeState, type GraphraumNodeUpdate, type GraphraumNodeVisual, type GraphraumOptions, GraphraumOverlay, type GraphraumOverlayNode, type GraphraumOverlayOptions, type GraphraumPatternBackground, type GraphraumPickHit, type GraphraumPosition, type GraphraumPresentation, type GraphraumPresentationAction, type GraphraumPresentationProperty, type GraphraumPropertyValue, type GraphraumScreenPosition, type GraphraumSetDataOptions, type GraphraumTheme, type GraphraumThemeName, type GraphraumVisualMapper, type LabelBudgetCandidate, type RichNodePolicy, type SelectBudgetedLabelIdsInput, type SelectFocusLabelIdsInput, attachPointOnOutline, bindGraphology, boundOverlayIds, computeClusteredForcePositions, computeForcePositions, createForceSimulation, defineVisuals, forceIterationCount, graphraumEdgePaths, graphraumNodeShapes, graphraumTheme, graphraumThemeDark, graphraumThemeLight, graphraumThemes, isTransparentGraphraumBackground, normalizeGraphraumBackground, orderFocusNodeIds, resolveGraphraumTheme, selectBudgetedLabelIds, selectFocusLabelIds, selectRichNodeIds, trimEdgeEndpoints };
|
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,
|
|
@@ -2493,6 +2543,231 @@ function prepareLayoutPositions(nodes, nodeIndices, layout) {
|
|
|
2493
2543
|
};
|
|
2494
2544
|
});
|
|
2495
2545
|
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Drags one node with the pointer. A press on a node arms a gesture; once the pointer leaves the
|
|
2548
|
+
* click tolerance (a finger must rest on the node first) the node follows it until release.
|
|
2549
|
+
* Camera controls are switched off for the gesture and restored when it ends, so a press that stays
|
|
2550
|
+
* a click behaves as before. Escape, a cancelled pointer or lost pointer capture put the node back.
|
|
2551
|
+
*/
|
|
2552
|
+
var NodeDragController = class {
|
|
2553
|
+
host;
|
|
2554
|
+
holdMs;
|
|
2555
|
+
enabled = false;
|
|
2556
|
+
gesture = null;
|
|
2557
|
+
unlisten;
|
|
2558
|
+
unlistenKeys = null;
|
|
2559
|
+
constructor(host, holdMs = 350) {
|
|
2560
|
+
this.host = host;
|
|
2561
|
+
this.holdMs = holdMs;
|
|
2562
|
+
this.unlisten = [
|
|
2563
|
+
listen(host.element, "pointerdown", this.handleDown, { capture: true }),
|
|
2564
|
+
listen(host.element, "pointermove", this.handleMove),
|
|
2565
|
+
listen(host.element, "pointerup", this.handleUp),
|
|
2566
|
+
listen(host.element, "pointercancel", this.handleCancel),
|
|
2567
|
+
listen(host.element, "lostpointercapture", this.handleCancel)
|
|
2568
|
+
];
|
|
2569
|
+
}
|
|
2570
|
+
/** True from the first drag step until the node is dropped or the drag is cancelled. */
|
|
2571
|
+
get active() {
|
|
2572
|
+
return this.gesture?.phase === "dragging";
|
|
2573
|
+
}
|
|
2574
|
+
setEnabled(enabled) {
|
|
2575
|
+
if (!enabled) this.cancel({ restore: true });
|
|
2576
|
+
this.enabled = enabled;
|
|
2577
|
+
}
|
|
2578
|
+
/** Ends a running gesture as cancelled. `restore` puts the node back where it was grabbed. */
|
|
2579
|
+
cancel({ restore }) {
|
|
2580
|
+
const gesture = this.gesture;
|
|
2581
|
+
if (!gesture) return;
|
|
2582
|
+
if (gesture.phase === "dragging") {
|
|
2583
|
+
const restored = restore && this.host.nodePosition(gesture.nodeId) !== null;
|
|
2584
|
+
if (restored) this.host.moveNode(gesture.nodeId, gesture.origin);
|
|
2585
|
+
this.finish(gesture);
|
|
2586
|
+
this.host.emit({
|
|
2587
|
+
nodeId: gesture.nodeId,
|
|
2588
|
+
phase: "cancel",
|
|
2589
|
+
position: restored ? gesture.origin : gesture.last
|
|
2590
|
+
});
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
this.finish(gesture);
|
|
2594
|
+
}
|
|
2595
|
+
dispose() {
|
|
2596
|
+
this.cancel({ restore: false });
|
|
2597
|
+
this.enabled = false;
|
|
2598
|
+
for (const stop of this.unlisten) stop();
|
|
2599
|
+
}
|
|
2600
|
+
handleDown = (event) => {
|
|
2601
|
+
if (!this.enabled || this.gesture || event.button !== 0 || !this.host.canStart()) return;
|
|
2602
|
+
const nodeId = this.host.pickNode(event.clientX, event.clientY);
|
|
2603
|
+
if (nodeId === null) return;
|
|
2604
|
+
const origin = this.host.nodePosition(nodeId);
|
|
2605
|
+
if (!origin) return;
|
|
2606
|
+
const surface = this.host.createSurface(origin);
|
|
2607
|
+
const grabbed = surface?.(event.clientX, event.clientY);
|
|
2608
|
+
if (!surface || !grabbed) return;
|
|
2609
|
+
const touch = event.pointerType === "touch";
|
|
2610
|
+
const gesture = {
|
|
2611
|
+
controlsHeld: false,
|
|
2612
|
+
holdTimer: null,
|
|
2613
|
+
last: origin,
|
|
2614
|
+
nodeId,
|
|
2615
|
+
offset: {
|
|
2616
|
+
x: origin.x - grabbed.x,
|
|
2617
|
+
y: origin.y - grabbed.y,
|
|
2618
|
+
z: (origin.z ?? 0) - (grabbed.z ?? 0)
|
|
2619
|
+
},
|
|
2620
|
+
origin,
|
|
2621
|
+
phase: "pressed",
|
|
2622
|
+
pointerId: event.pointerId,
|
|
2623
|
+
startX: event.clientX,
|
|
2624
|
+
startY: event.clientY,
|
|
2625
|
+
surface,
|
|
2626
|
+
touch
|
|
2627
|
+
};
|
|
2628
|
+
this.gesture = gesture;
|
|
2629
|
+
this.unlistenKeys = listen(this.host.document, "keydown", this.handleKey);
|
|
2630
|
+
if (touch) {
|
|
2631
|
+
gesture.holdTimer = setTimeout(() => this.beginDrag(gesture), this.holdMs);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
this.holdControls(gesture);
|
|
2635
|
+
capture(this.host.element, event.pointerId);
|
|
2636
|
+
};
|
|
2637
|
+
handleMove = (event) => {
|
|
2638
|
+
const gesture = this.gesture;
|
|
2639
|
+
if (!gesture || event.pointerId !== gesture.pointerId) return;
|
|
2640
|
+
if (gesture.phase === "pressed") {
|
|
2641
|
+
if (Math.hypot(event.clientX - gesture.startX, event.clientY - gesture.startY) <= 5) return;
|
|
2642
|
+
if (gesture.touch) {
|
|
2643
|
+
this.finish(gesture);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
this.beginDrag(gesture);
|
|
2647
|
+
}
|
|
2648
|
+
this.follow(gesture, event.clientX, event.clientY);
|
|
2649
|
+
};
|
|
2650
|
+
handleUp = (event) => {
|
|
2651
|
+
const gesture = this.gesture;
|
|
2652
|
+
if (!gesture || event.pointerId !== gesture.pointerId) return;
|
|
2653
|
+
this.finish(gesture);
|
|
2654
|
+
if (gesture.phase === "dragging") this.host.emit({
|
|
2655
|
+
nodeId: gesture.nodeId,
|
|
2656
|
+
phase: "end",
|
|
2657
|
+
position: gesture.last
|
|
2658
|
+
});
|
|
2659
|
+
};
|
|
2660
|
+
handleCancel = (event) => {
|
|
2661
|
+
if (this.gesture && event.pointerId === this.gesture.pointerId) this.cancel({ restore: true });
|
|
2662
|
+
};
|
|
2663
|
+
handleKey = (event) => {
|
|
2664
|
+
if (event.key === "Escape") this.cancel({ restore: true });
|
|
2665
|
+
};
|
|
2666
|
+
beginDrag(gesture) {
|
|
2667
|
+
if (this.gesture !== gesture || gesture.phase === "dragging") return;
|
|
2668
|
+
gesture.phase = "dragging";
|
|
2669
|
+
clearHold(gesture);
|
|
2670
|
+
this.holdControls(gesture);
|
|
2671
|
+
if (gesture.touch) capture(this.host.element, gesture.pointerId);
|
|
2672
|
+
this.host.emit({
|
|
2673
|
+
nodeId: gesture.nodeId,
|
|
2674
|
+
phase: "start",
|
|
2675
|
+
position: gesture.origin
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
follow(gesture, clientX, clientY) {
|
|
2679
|
+
if (!this.host.nodePosition(gesture.nodeId)) {
|
|
2680
|
+
this.cancel({ restore: false });
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
const point = gesture.surface(clientX, clientY);
|
|
2684
|
+
if (!point) return;
|
|
2685
|
+
const z = point.z === void 0 ? gesture.origin.z : point.z + gesture.offset.z;
|
|
2686
|
+
const position = {
|
|
2687
|
+
x: point.x + gesture.offset.x,
|
|
2688
|
+
y: point.y + gesture.offset.y,
|
|
2689
|
+
...z === void 0 ? {} : { z }
|
|
2690
|
+
};
|
|
2691
|
+
gesture.last = position;
|
|
2692
|
+
this.host.moveNode(gesture.nodeId, position);
|
|
2693
|
+
this.host.emit({
|
|
2694
|
+
nodeId: gesture.nodeId,
|
|
2695
|
+
phase: "move",
|
|
2696
|
+
position
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
holdControls(gesture) {
|
|
2700
|
+
if (gesture.controlsHeld) return;
|
|
2701
|
+
gesture.controlsHeld = true;
|
|
2702
|
+
this.host.setControlsEnabled(false);
|
|
2703
|
+
}
|
|
2704
|
+
/** Drops the gesture and everything it held: timer, pointer capture, key listener, camera controls. */
|
|
2705
|
+
finish(gesture) {
|
|
2706
|
+
if (this.gesture !== gesture) return;
|
|
2707
|
+
this.gesture = null;
|
|
2708
|
+
clearHold(gesture);
|
|
2709
|
+
this.unlistenKeys?.();
|
|
2710
|
+
this.unlistenKeys = null;
|
|
2711
|
+
release(this.host.element, gesture.pointerId);
|
|
2712
|
+
if (gesture.controlsHeld) this.host.setControlsEnabled(true);
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
/** Typed `addEventListener` for the pointer and key events this module handles; returns the unsubscribe. */
|
|
2716
|
+
function listen(target, type, handler, options) {
|
|
2717
|
+
const listener = handler;
|
|
2718
|
+
target.addEventListener(type, listener, options);
|
|
2719
|
+
return () => target.removeEventListener(type, listener, options);
|
|
2720
|
+
}
|
|
2721
|
+
function clearHold(gesture) {
|
|
2722
|
+
if (gesture.holdTimer === null) return;
|
|
2723
|
+
clearTimeout(gesture.holdTimer);
|
|
2724
|
+
gesture.holdTimer = null;
|
|
2725
|
+
}
|
|
2726
|
+
function capture(element, pointerId) {
|
|
2727
|
+
try {
|
|
2728
|
+
element.setPointerCapture(pointerId);
|
|
2729
|
+
} catch {}
|
|
2730
|
+
}
|
|
2731
|
+
function release(element, pointerId) {
|
|
2732
|
+
try {
|
|
2733
|
+
element.releasePointerCapture(pointerId);
|
|
2734
|
+
} catch {}
|
|
2735
|
+
}
|
|
2736
|
+
//#endregion
|
|
2737
|
+
//#region src/node-drag-surface.ts
|
|
2738
|
+
/**
|
|
2739
|
+
* Where the pointer is in world space on the plane a dragged node moves in. 2D unprojects onto the
|
|
2740
|
+
* view plane and reports no depth. 3D intersects the pointer ray with a plane through the anchor
|
|
2741
|
+
* that faces the camera as it was at the press, so the node keeps its depth and follows the pointer
|
|
2742
|
+
* across the screen. Returns `null` for an empty canvas, and from the surface when the ray misses.
|
|
2743
|
+
*/
|
|
2744
|
+
function createNodeDragSurface({ anchor, bounds, camera, raycaster }) {
|
|
2745
|
+
const initial = bounds();
|
|
2746
|
+
if (initial.width === 0 || initial.height === 0) return null;
|
|
2747
|
+
const ndc = new Vector2();
|
|
2748
|
+
const toNdc = (clientX, clientY) => {
|
|
2749
|
+
const { height, left, top, width } = bounds();
|
|
2750
|
+
return ndc.set((clientX - left) / width * 2 - 1, -((clientY - top) / height) * 2 + 1);
|
|
2751
|
+
};
|
|
2752
|
+
if (camera.type === "OrthographicCamera") return (clientX, clientY) => {
|
|
2753
|
+
const point = toNdc(clientX, clientY);
|
|
2754
|
+
const world = new Vector3(point.x, point.y, 0).unproject(camera);
|
|
2755
|
+
return {
|
|
2756
|
+
x: world.x,
|
|
2757
|
+
y: world.y
|
|
2758
|
+
};
|
|
2759
|
+
};
|
|
2760
|
+
const plane = new Plane().setFromNormalAndCoplanarPoint(camera.getWorldDirection(new Vector3()), new Vector3(anchor.x, anchor.y, anchor.z ?? 0));
|
|
2761
|
+
const hit = new Vector3();
|
|
2762
|
+
return (clientX, clientY) => {
|
|
2763
|
+
raycaster.setFromCamera(toNdc(clientX, clientY), camera);
|
|
2764
|
+
return raycaster.ray.intersectPlane(plane, hit) ? {
|
|
2765
|
+
x: hit.x,
|
|
2766
|
+
y: hit.y,
|
|
2767
|
+
z: hit.z
|
|
2768
|
+
} : null;
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2496
2771
|
const vertexShader$1 = `
|
|
2497
2772
|
attribute float instanceGlowOpacity;
|
|
2498
2773
|
varying vec3 glowColor;
|
|
@@ -3386,12 +3661,16 @@ var Graphraum = class {
|
|
|
3386
3661
|
gpuTimer = null;
|
|
3387
3662
|
viewListeners = /* @__PURE__ */ new Set();
|
|
3388
3663
|
focusListeners = /* @__PURE__ */ new Set();
|
|
3664
|
+
nodeDragListeners = /* @__PURE__ */ new Set();
|
|
3665
|
+
nodeDrag;
|
|
3389
3666
|
lastHoveredNodeId = null;
|
|
3390
3667
|
antialias;
|
|
3391
3668
|
depthOfField = DEPTH_OF_FIELD_DEFAULTS;
|
|
3392
3669
|
depthOfFieldFocus = new DepthOfFieldFocus();
|
|
3393
3670
|
/** Strength actually applied: fades toward `strength` while enabled and toward 0 when disabled. */
|
|
3394
3671
|
depthOfFieldStrength = new Tween(0);
|
|
3672
|
+
/** The same for `nearStrength`, faded alongside. */
|
|
3673
|
+
depthOfFieldNearStrength = new Tween(0);
|
|
3395
3674
|
depthOfFieldPass = null;
|
|
3396
3675
|
depthOfFieldStatus = "off";
|
|
3397
3676
|
constructor(container, options = {}) {
|
|
@@ -3409,6 +3688,7 @@ var Graphraum = class {
|
|
|
3409
3688
|
if (options.depthOfField !== void 0) {
|
|
3410
3689
|
this.depthOfField = resolveDepthOfFieldOptions(options.depthOfField);
|
|
3411
3690
|
this.depthOfFieldStrength.retarget(this.depthOfFieldTargetStrength(), 0, resolveEasing("linear"));
|
|
3691
|
+
this.depthOfFieldNearStrength.retarget(this.depthOfFieldTargetNearStrength(), 0, resolveEasing("linear"));
|
|
3412
3692
|
}
|
|
3413
3693
|
this.antialias = options.antialias ?? false;
|
|
3414
3694
|
this.renderer = new WebGLRenderer({
|
|
@@ -3433,6 +3713,7 @@ var Graphraum = class {
|
|
|
3433
3713
|
this.renderer.domElement.addEventListener("pointermove", this.handlePointerMove);
|
|
3434
3714
|
this.renderer.domElement.addEventListener("pointerleave", this.handlePointerLeave);
|
|
3435
3715
|
for (const type of FLIGHT_CANCEL_EVENTS) this.renderer.domElement.addEventListener(type, this.cancelFlight);
|
|
3716
|
+
this.nodeDrag = new NodeDragController(this.createNodeDragHost());
|
|
3436
3717
|
this.camera = this.createCamera();
|
|
3437
3718
|
this.controls = this.createControls();
|
|
3438
3719
|
this.resizeObserver = new ResizeObserver(() => this.resize());
|
|
@@ -3514,78 +3795,170 @@ var Graphraum = class {
|
|
|
3514
3795
|
const addedNodes = patch.addedNodes ?? [];
|
|
3515
3796
|
const addedEdges = patch.addedEdges ?? [];
|
|
3516
3797
|
if (addedNodes.length === 0 && addedEdges.length === 0) return;
|
|
3517
|
-
const
|
|
3518
|
-
|
|
3519
|
-
const
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
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
|
+
}
|
|
3525
3811
|
const compiled = compileGraph({
|
|
3526
|
-
nodes: patchNodes,
|
|
3812
|
+
nodes: [...patchNodes.values()],
|
|
3527
3813
|
edges: addedEdges
|
|
3528
3814
|
}, this.visuals);
|
|
3529
|
-
const nodeVisuals = new Map(patchNodes.map((
|
|
3530
|
-
const
|
|
3531
|
-
|
|
3532
|
-
...nodeVisuals.get(node.id)
|
|
3533
|
-
}))];
|
|
3534
|
-
const nextNodeIndices = new Map(this.nodeIndices);
|
|
3535
|
-
for (const [index, node] of nextNodes.entries()) nextNodeIndices.set(node.id, index);
|
|
3536
|
-
const nextEdges = [...this.data.edges, ...addedEdges.map((edge, index) => ({
|
|
3537
|
-
...edge,
|
|
3538
|
-
...compiled.edgeVisuals[index]
|
|
3539
|
-
}))];
|
|
3540
|
-
const nextEdgeIndices = new Uint32Array(nextEdges.length * 2);
|
|
3541
|
-
nextEdgeIndices.set(this.edgeNodeIndices);
|
|
3542
|
-
const nextEdgePositions = new Float32Array(nextEdges.length * 6);
|
|
3543
|
-
nextEdgePositions.set(this.canonicalEdgePositions);
|
|
3544
|
-
const nextIncidentEdges = this.incidentEdgeIndices.map((indices) => [...indices]);
|
|
3545
|
-
for (const [index, edge] of nextEdges.entries()) {
|
|
3546
|
-
const source = nextNodeIndices.get(edge.source);
|
|
3547
|
-
const target = nextNodeIndices.get(edge.target);
|
|
3548
|
-
if (source === void 0 || target === void 0) continue;
|
|
3549
|
-
nextEdgeIndices[index * 2] = source;
|
|
3550
|
-
nextEdgeIndices[index * 2 + 1] = target;
|
|
3551
|
-
if (!nextIncidentEdges[source]) nextIncidentEdges[source] = [];
|
|
3552
|
-
if (!nextIncidentEdges[target]) nextIncidentEdges[target] = [];
|
|
3553
|
-
if (index >= this.data.edges.length) {
|
|
3554
|
-
nextIncidentEdges[source]?.push(index);
|
|
3555
|
-
nextIncidentEdges[target]?.push(index);
|
|
3556
|
-
const sourceNode = nextNodes[source];
|
|
3557
|
-
const targetNode = nextNodes[target];
|
|
3558
|
-
if (sourceNode && targetNode) nextEdgePositions.set([
|
|
3559
|
-
sourceNode.position.x,
|
|
3560
|
-
sourceNode.position.y,
|
|
3561
|
-
sourceNode.position.z ?? 0,
|
|
3562
|
-
targetNode.position.x,
|
|
3563
|
-
targetNode.position.y,
|
|
3564
|
-
targetNode.position.z ?? 0
|
|
3565
|
-
], index * 6);
|
|
3566
|
-
}
|
|
3567
|
-
}
|
|
3568
|
-
this.data = {
|
|
3569
|
-
nodes: nextNodes,
|
|
3570
|
-
edges: nextEdges
|
|
3571
|
-
};
|
|
3572
|
-
this.nodeIds = nextNodes.map((node) => node.id);
|
|
3573
|
-
this.hasGlowNodes = nextNodes.some(hasNodeGlow);
|
|
3574
|
-
this.nodeIndices = nextNodeIndices;
|
|
3575
|
-
this.edgeNodeIndices = nextEdgeIndices;
|
|
3576
|
-
this.canonicalEdgePositions = nextEdgePositions;
|
|
3577
|
-
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;
|
|
3578
3818
|
for (const node of addedNodes) {
|
|
3579
|
-
const index =
|
|
3580
|
-
|
|
3581
|
-
|
|
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);
|
|
3582
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;
|
|
3583
3860
|
for (const [id, presentation] of compiled.nodePresentations) this.nodePresentations.set(id, presentation);
|
|
3584
3861
|
for (const [id, presentation] of compiled.edgePresentations) this.edgePresentations.set(id, presentation);
|
|
3585
|
-
this.
|
|
3862
|
+
if (this.materializeAppended(previous)) {
|
|
3863
|
+
if (addedGlow) this.syncGlow();
|
|
3864
|
+
} else this.materializeViewport();
|
|
3586
3865
|
this.requestRender();
|
|
3587
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
|
+
}
|
|
3588
3960
|
replaceData(data, fitView) {
|
|
3961
|
+
this.nodeDrag.cancel({ restore: false });
|
|
3589
3962
|
const compiled = compileGraph(data, this.visuals);
|
|
3590
3963
|
this.disposeGraphObjects();
|
|
3591
3964
|
this.data = {
|
|
@@ -3771,13 +4144,8 @@ var Graphraum = class {
|
|
|
3771
4144
|
layouts: this.visibleEdgeLayouts,
|
|
3772
4145
|
minHitSlop,
|
|
3773
4146
|
mode: this.mode,
|
|
3774
|
-
nodeOutlines: this.data.nodes
|
|
3775
|
-
|
|
3776
|
-
shape: node.shape,
|
|
3777
|
-
size: node.size,
|
|
3778
|
-
strokeWidth: node.strokeWidth,
|
|
3779
|
-
width: node.width
|
|
3780
|
-
})),
|
|
4147
|
+
nodeOutlines: this.data.nodes,
|
|
4148
|
+
pickable: this.camera instanceof OrthographicCamera,
|
|
3781
4149
|
tier: this.lastEdgeLodTier,
|
|
3782
4150
|
worldPerPixel
|
|
3783
4151
|
}, this.pickableEdgeSegments);
|
|
@@ -3903,6 +4271,7 @@ var Graphraum = class {
|
|
|
3903
4271
|
setMode(mode) {
|
|
3904
4272
|
if (mode === this.mode) return;
|
|
3905
4273
|
this.cancelFlight();
|
|
4274
|
+
this.nodeDrag.cancel({ restore: true });
|
|
3906
4275
|
this.stopAutoOrbit();
|
|
3907
4276
|
this.mode = mode;
|
|
3908
4277
|
this.controls.removeEventListener("change", this.handleViewChange);
|
|
@@ -3958,12 +4327,17 @@ var Graphraum = class {
|
|
|
3958
4327
|
if (this.depthOfField.enabled && !previous.enabled) this.depthOfFieldFocus.retarget(0, resolveEasing(easing));
|
|
3959
4328
|
const fadeMs = animate && !prefersReducedMotion() ? durationMs : 0;
|
|
3960
4329
|
const done = this.depthOfFieldStrength.retarget(this.depthOfFieldTargetStrength(), fadeMs, resolveEasing(easing));
|
|
4330
|
+
this.depthOfFieldNearStrength.retarget(this.depthOfFieldTargetNearStrength(), fadeMs, resolveEasing(easing));
|
|
3961
4331
|
this.requestRender();
|
|
3962
4332
|
return done;
|
|
3963
4333
|
}
|
|
3964
4334
|
depthOfFieldTargetStrength() {
|
|
3965
4335
|
return this.depthOfField.enabled ? this.depthOfField.strength : 0;
|
|
3966
4336
|
}
|
|
4337
|
+
depthOfFieldTargetNearStrength() {
|
|
4338
|
+
const { enabled, nearStrength, strength } = this.depthOfField;
|
|
4339
|
+
return enabled ? nearStrength ?? strength : 0;
|
|
4340
|
+
}
|
|
3967
4341
|
getNodePresentation(nodeId) {
|
|
3968
4342
|
return this.nodePresentations.get(nodeId) ?? null;
|
|
3969
4343
|
}
|
|
@@ -4030,6 +4404,51 @@ var Graphraum = class {
|
|
|
4030
4404
|
listener();
|
|
4031
4405
|
return () => this.viewListeners.delete(listener);
|
|
4032
4406
|
}
|
|
4407
|
+
/**
|
|
4408
|
+
* Turns node dragging on or off (default off). A drag starts when the pointer moves past a small
|
|
4409
|
+
* tolerance after a press on a node (a finger must rest on the node first, so a swipe still pans
|
|
4410
|
+
* the camera), moves the node on a plane facing the camera, and ends on release. Escape or a
|
|
4411
|
+
* cancelled pointer put the node back. Not available while the density LOD shows clusters.
|
|
4412
|
+
*/
|
|
4413
|
+
setNodeDrag(options) {
|
|
4414
|
+
if (typeof options?.enabled !== "boolean") throw new Error("setNodeDrag option enabled must be a boolean.");
|
|
4415
|
+
this.nodeDrag.setEnabled(options.enabled);
|
|
4416
|
+
}
|
|
4417
|
+
/** Subscribes to node drag steps (`start`, `move`, `end`, `cancel`). Returns an unsubscribe function. */
|
|
4418
|
+
onNodeDrag(listener) {
|
|
4419
|
+
this.nodeDragListeners.add(listener);
|
|
4420
|
+
return () => this.nodeDragListeners.delete(listener);
|
|
4421
|
+
}
|
|
4422
|
+
createNodeDragHost() {
|
|
4423
|
+
const element = this.renderer.domElement;
|
|
4424
|
+
return {
|
|
4425
|
+
canStart: () => !this.destroyed && this.nodeMesh !== null && !this.densityLodActive,
|
|
4426
|
+
createSurface: (anchor) => createNodeDragSurface({
|
|
4427
|
+
anchor,
|
|
4428
|
+
bounds: () => element.getBoundingClientRect(),
|
|
4429
|
+
camera: this.camera,
|
|
4430
|
+
raycaster: this.raycaster
|
|
4431
|
+
}),
|
|
4432
|
+
document: element.ownerDocument,
|
|
4433
|
+
element,
|
|
4434
|
+
emit: (event) => {
|
|
4435
|
+
for (const listener of this.nodeDragListeners) listener(event);
|
|
4436
|
+
},
|
|
4437
|
+
moveNode: (nodeId, position) => this.updateNodes([{
|
|
4438
|
+
id: nodeId,
|
|
4439
|
+
position
|
|
4440
|
+
}]),
|
|
4441
|
+
nodePosition: (nodeId) => {
|
|
4442
|
+
const index = this.nodeIndices.get(nodeId);
|
|
4443
|
+
const node = index === void 0 ? void 0 : this.data.nodes[index];
|
|
4444
|
+
return node ? { ...node.position } : null;
|
|
4445
|
+
},
|
|
4446
|
+
pickNode: (clientX, clientY) => this.pickNode(clientX, clientY),
|
|
4447
|
+
setControlsEnabled: (enabled) => {
|
|
4448
|
+
this.controls.enabled = enabled;
|
|
4449
|
+
}
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4033
4452
|
/** Subscribes to selection and hover changes that affect overlay focus chrome. */
|
|
4034
4453
|
onFocusChange(listener) {
|
|
4035
4454
|
this.focusListeners.add(listener);
|
|
@@ -4040,6 +4459,7 @@ var Graphraum = class {
|
|
|
4040
4459
|
for (const listener of this.focusListeners) listener();
|
|
4041
4460
|
}
|
|
4042
4461
|
handlePointerMove = (event) => {
|
|
4462
|
+
if (this.nodeDrag.active) return;
|
|
4043
4463
|
const nodeId = this.pickNode(event.clientX, event.clientY);
|
|
4044
4464
|
if (nodeId === this.lastHoveredNodeId) return;
|
|
4045
4465
|
this.lastHoveredNodeId = nodeId;
|
|
@@ -4396,6 +4816,7 @@ var Graphraum = class {
|
|
|
4396
4816
|
destroy() {
|
|
4397
4817
|
this.destroyed = true;
|
|
4398
4818
|
this.cancelFlight();
|
|
4819
|
+
this.nodeDrag.dispose();
|
|
4399
4820
|
this.stopAutoOrbit();
|
|
4400
4821
|
this.resizeObserver.disconnect();
|
|
4401
4822
|
this.renderer.domElement.removeEventListener("pointermove", this.handlePointerMove);
|
|
@@ -4407,6 +4828,7 @@ var Graphraum = class {
|
|
|
4407
4828
|
if (this.gpuTimer) for (const query of this.gpuTimer.pending) this.gpuTimer.context.deleteQuery(query);
|
|
4408
4829
|
this.disposeBackgroundTexture();
|
|
4409
4830
|
this.depthOfFieldStrength.cancel();
|
|
4831
|
+
this.depthOfFieldNearStrength.cancel();
|
|
4410
4832
|
this.disposeDepthOfFieldPass();
|
|
4411
4833
|
this.disposeGraphObjects();
|
|
4412
4834
|
this.renderer.dispose();
|
|
@@ -4457,19 +4879,22 @@ var Graphraum = class {
|
|
|
4457
4879
|
prepareDepthOfField(nowMs) {
|
|
4458
4880
|
const options = this.depthOfField;
|
|
4459
4881
|
const strengthTween = this.depthOfFieldStrength;
|
|
4460
|
-
|
|
4882
|
+
const nearStrengthTween = this.depthOfFieldNearStrength;
|
|
4883
|
+
if (!options.enabled && strengthTween.settled && nearStrengthTween.settled) {
|
|
4461
4884
|
this.depthOfFieldStatus = "off";
|
|
4462
4885
|
this.disposeDepthOfFieldPass();
|
|
4463
4886
|
return null;
|
|
4464
4887
|
}
|
|
4465
4888
|
if (!(this.camera instanceof PerspectiveCamera)) {
|
|
4466
4889
|
strengthTween.jump();
|
|
4890
|
+
nearStrengthTween.jump();
|
|
4467
4891
|
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4468
4892
|
this.disposeDepthOfFieldPass();
|
|
4469
4893
|
return null;
|
|
4470
4894
|
}
|
|
4471
4895
|
if (!DepthOfFieldPass.isSupported(this.renderer)) {
|
|
4472
4896
|
strengthTween.jump();
|
|
4897
|
+
nearStrengthTween.jump();
|
|
4473
4898
|
this.depthOfFieldStatus = options.enabled ? "unsupported" : "off";
|
|
4474
4899
|
if (options.enabled && !warnedDepthOfFieldUnsupported) {
|
|
4475
4900
|
warnedDepthOfFieldUnsupported = true;
|
|
@@ -4478,7 +4903,8 @@ var Graphraum = class {
|
|
|
4478
4903
|
return null;
|
|
4479
4904
|
}
|
|
4480
4905
|
const strength = strengthTween.sample(nowMs);
|
|
4481
|
-
|
|
4906
|
+
const nearStrength = nearStrengthTween.sample(nowMs);
|
|
4907
|
+
if (!strengthTween.settled || !nearStrengthTween.settled) this.requestRender();
|
|
4482
4908
|
this.camera.updateMatrixWorld();
|
|
4483
4909
|
let focusDistance = typeof options.focus === "number" ? options.focus : null;
|
|
4484
4910
|
let amount = 1;
|
|
@@ -4494,9 +4920,11 @@ var Graphraum = class {
|
|
|
4494
4920
|
}
|
|
4495
4921
|
focusDistance = Math.max(focusDistance, this.camera.near);
|
|
4496
4922
|
const pixelRatio = this.renderer.getPixelRatio();
|
|
4497
|
-
const
|
|
4923
|
+
const pixelsPerStrength = amount * distanceFade(focusDistance, options.nearDistance, options.farDistance) * 24 * pixelRatio;
|
|
4924
|
+
const aperturePixels = strength * pixelsPerStrength;
|
|
4925
|
+
const nearAperturePixels = nearStrength * pixelsPerStrength;
|
|
4498
4926
|
const maxBlurPixels = options.maxBlur * pixelRatio;
|
|
4499
|
-
if (aperturePixels < .25 || maxBlurPixels < .25) {
|
|
4927
|
+
if (Math.max(aperturePixels, nearAperturePixels) < .25 || maxBlurPixels < .25) {
|
|
4500
4928
|
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4501
4929
|
return null;
|
|
4502
4930
|
}
|
|
@@ -4506,7 +4934,8 @@ var Graphraum = class {
|
|
|
4506
4934
|
aperturePixels,
|
|
4507
4935
|
focusDistance,
|
|
4508
4936
|
maxBlurPixels,
|
|
4509
|
-
minBlurPixels: options.minBlur * pixelRatio
|
|
4937
|
+
minBlurPixels: options.minBlur * pixelRatio,
|
|
4938
|
+
nearAperturePixels
|
|
4510
4939
|
};
|
|
4511
4940
|
}
|
|
4512
4941
|
/** Mean world position of the selected nodes, read every frame so a running layout keeps them in focus. */
|
|
@@ -4540,6 +4969,7 @@ var Graphraum = class {
|
|
|
4540
4969
|
* without a loop.
|
|
4541
4970
|
*/
|
|
4542
4971
|
stepAutoOrbit = (deltaSeconds) => {
|
|
4972
|
+
if (this.nodeDrag.active) return;
|
|
4543
4973
|
try {
|
|
4544
4974
|
this.controls.autoRotate = true;
|
|
4545
4975
|
this.orbitUpdating = true;
|
|
@@ -4631,7 +5061,7 @@ var Graphraum = class {
|
|
|
4631
5061
|
return null;
|
|
4632
5062
|
}
|
|
4633
5063
|
pickEdge(clientX, clientY) {
|
|
4634
|
-
if (!this.edgeMesh || this.
|
|
5064
|
+
if (!this.edgeMesh || this.visibleEdgeSegmentCount === 0) return null;
|
|
4635
5065
|
const bounds = this.renderer.domElement.getBoundingClientRect();
|
|
4636
5066
|
if (bounds.width === 0 || bounds.height === 0) return null;
|
|
4637
5067
|
this.pointer.set((clientX - bounds.left) / bounds.width * 2 - 1, -((clientY - bounds.top) / bounds.height) * 2 + 1);
|
|
@@ -4799,35 +5229,20 @@ var Graphraum = class {
|
|
|
4799
5229
|
const visibleNodeIndices = materializedNodes.map(({ index }) => index);
|
|
4800
5230
|
const edgeCandidates = bounds ? collectIncidentEdges(visibleNodeIndices, this.incidentEdgeIndices) : this.data.edges.map((_, index) => index);
|
|
4801
5231
|
const visibleEdgeIndices = applyEdgeBudget(edgeCandidates, this.maxVisibleEdges);
|
|
4802
|
-
const matrix = new Matrix4();
|
|
4803
5232
|
this.visibleNodeSlots = /* @__PURE__ */ new Map();
|
|
4804
5233
|
this.visibleNodeIndices = visibleNodeIndices;
|
|
4805
5234
|
this.nodeMesh.instanceMatrix.clearUpdateRanges();
|
|
4806
5235
|
this.nodePickMesh.instanceMatrix.clearUpdateRanges();
|
|
4807
5236
|
this.nodeMesh.instanceColor?.clearUpdateRanges();
|
|
4808
|
-
const
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
nodeStrokeWidth.clearUpdateRanges();
|
|
4813
|
-
nodeStrokeColor.clearUpdateRanges();
|
|
5237
|
+
const targets = this.nodeInstanceTargets();
|
|
5238
|
+
targets.shape.clearUpdateRanges();
|
|
5239
|
+
targets.strokeWidth.clearUpdateRanges();
|
|
5240
|
+
targets.strokeColor.clearUpdateRanges();
|
|
4814
5241
|
for (const [slot, materialized] of materializedNodes.entries()) {
|
|
4815
|
-
const
|
|
4816
|
-
const node = this.data.nodes[nodeIndex];
|
|
5242
|
+
const node = this.data.nodes[materialized.index];
|
|
4817
5243
|
if (!node) continue;
|
|
4818
|
-
this.visibleNodeSlots.set(
|
|
4819
|
-
|
|
4820
|
-
const maxAxis = Math.max(width, height);
|
|
4821
|
-
matrix.makeScale(width, height, maxAxis);
|
|
4822
|
-
matrix.setPosition(materialized.x, materialized.y, materialized.z);
|
|
4823
|
-
this.nodeMesh.setMatrixAt(slot, matrix);
|
|
4824
|
-
const pickScale = maxAxis * Math.SQRT2;
|
|
4825
|
-
matrix.makeScale(pickScale, pickScale, pickScale);
|
|
4826
|
-
matrix.setPosition(materialized.x, materialized.y, materialized.z);
|
|
4827
|
-
this.nodePickMesh.setMatrixAt(slot, matrix);
|
|
4828
|
-
this.nodeMesh.setColorAt(slot, this.getNodeColor(node));
|
|
4829
|
-
setNodeShapeAt(nodeShape, slot, node.shape);
|
|
4830
|
-
setNodeStrokeAt(nodeStrokeWidth, nodeStrokeColor, slot, resolveNodeStroke(node, this.theme));
|
|
5244
|
+
this.visibleNodeSlots.set(materialized.index, slot);
|
|
5245
|
+
this.writeNodeInstance(targets, slot, materialized, node);
|
|
4831
5246
|
}
|
|
4832
5247
|
this.nodeMesh.count = materializedNodes.length;
|
|
4833
5248
|
this.nodePickMesh.count = materializedNodes.length;
|
|
@@ -4839,12 +5254,12 @@ var Graphraum = class {
|
|
|
4839
5254
|
this.nodeMesh.instanceColor.addUpdateRange(0, materializedNodes.length * 3);
|
|
4840
5255
|
this.nodeMesh.instanceColor.needsUpdate = true;
|
|
4841
5256
|
}
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
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;
|
|
4848
5263
|
const lodLevel = resolveLodLevel(this.densityLodActive, edgeCandidates.length, visibleEdgeIndices.length);
|
|
4849
5264
|
this.lastEdgeLodTier = edgeTierFromDiagnosticsLod(lodLevel);
|
|
4850
5265
|
const worldPerPixel = this.worldUnitsPerPixel();
|
|
@@ -4863,42 +5278,19 @@ var Graphraum = class {
|
|
|
4863
5278
|
endpointAttach: this.theme.endpointAttach,
|
|
4864
5279
|
edgeNodeIndices: this.edgeNodeIndices,
|
|
4865
5280
|
mode: this.mode,
|
|
4866
|
-
nodeOutlines: this.data.nodes
|
|
4867
|
-
height: node.height,
|
|
4868
|
-
shape: node.shape,
|
|
4869
|
-
size: node.size,
|
|
4870
|
-
strokeWidth: node.strokeWidth,
|
|
4871
|
-
width: node.width
|
|
4872
|
-
}))
|
|
5281
|
+
nodeOutlines: this.data.nodes
|
|
4873
5282
|
});
|
|
4874
5283
|
const edgeGeometry = this.edgeMesh.geometry;
|
|
4875
5284
|
this.visibleEdgeSlots = /* @__PURE__ */ new Map();
|
|
4876
5285
|
this.visibleEdgeLayouts = buildVisibleEdgeLayouts(packed.segments, packed.markers, packed.segments.length);
|
|
4877
5286
|
this.pickableEdgeSegments = [];
|
|
4878
5287
|
this.visibleEdgeSegmentEdgeIndices = [];
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
distinctEdges.add(segment.edgeIndex);
|
|
4886
|
-
this.visibleEdgeSegmentEdgeIndices.push(segment.edgeIndex);
|
|
4887
|
-
this.pickableEdgeSegments.push({
|
|
4888
|
-
edgeIndex: segment.edgeIndex,
|
|
4889
|
-
hitSlop: Math.max(minHitSlop, segment.width * worldPerPixel / 2 + 2 * worldPerPixel),
|
|
4890
|
-
x1: segment.x1,
|
|
4891
|
-
y1: segment.y1,
|
|
4892
|
-
x2: segment.x2,
|
|
4893
|
-
y2: segment.y2
|
|
4894
|
-
});
|
|
4895
|
-
edgeSlot += 1;
|
|
4896
|
-
}
|
|
4897
|
-
const segmentCount = edgeSlot;
|
|
4898
|
-
for (const marker of packed.markers) {
|
|
4899
|
-
writeEdgeMarkerInstance(edgeGeometry, edgeSlot, marker);
|
|
4900
|
-
edgeSlot += 1;
|
|
4901
|
-
}
|
|
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;
|
|
4902
5294
|
this.edgeMesh.count = edgeSlot;
|
|
4903
5295
|
for (const name of EDGE_INSTANCE_ATTRIBUTES) {
|
|
4904
5296
|
const attribute = edgeGeometry.getAttribute(name);
|
|
@@ -4910,28 +5302,85 @@ var Graphraum = class {
|
|
|
4910
5302
|
this.visibleNodeCount = materializedNodes.length;
|
|
4911
5303
|
this.visibleEdgeCandidateCount = edgeCandidates.length;
|
|
4912
5304
|
this.visibleEdgeSegmentCount = segmentCount;
|
|
4913
|
-
this.visibleEdgeCount =
|
|
5305
|
+
this.visibleEdgeCount = distinctEdgeCount;
|
|
4914
5306
|
this.visibleEdgeMarkerCount = packed.markers.length;
|
|
4915
5307
|
this.syncGlow();
|
|
4916
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
|
+
}
|
|
4917
5380
|
materializeNodes(indices, bounds) {
|
|
4918
5381
|
if (!this.densityLodActive || !bounds) return indices.slice(0, this.maxVisibleNodes).flatMap((index) => {
|
|
4919
|
-
const
|
|
4920
|
-
|
|
4921
|
-
const { height, width } = resolveNodeAxes({
|
|
4922
|
-
height: node.height,
|
|
4923
|
-
nodeId: node.id,
|
|
4924
|
-
size: node.size,
|
|
4925
|
-
width: node.width
|
|
4926
|
-
});
|
|
4927
|
-
return [{
|
|
4928
|
-
height,
|
|
4929
|
-
index,
|
|
4930
|
-
width,
|
|
4931
|
-
x: node.position.x,
|
|
4932
|
-
y: node.position.y,
|
|
4933
|
-
z: node.position.z ?? 0
|
|
4934
|
-
}];
|
|
5382
|
+
const materialized = this.plainMaterializedNode(index);
|
|
5383
|
+
return materialized ? [materialized] : [];
|
|
4935
5384
|
});
|
|
4936
5385
|
const aspect = Math.max(this.container.clientWidth, 1) / Math.max(this.container.clientHeight, 1);
|
|
4937
5386
|
const columns = Math.max(1, Math.floor(Math.sqrt(this.maxVisibleNodes * aspect)));
|
|
@@ -5033,6 +5482,13 @@ function setOrthographicFrustum(camera, aspect, visibleHeight) {
|
|
|
5033
5482
|
function hasNodeGlow(node) {
|
|
5034
5483
|
return (node.glow ?? 0) > 0;
|
|
5035
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
|
+
}
|
|
5036
5492
|
function nextCapacity(length) {
|
|
5037
5493
|
let capacity = 1;
|
|
5038
5494
|
while (capacity < length) capacity *= 2;
|