@yggy-tech/graphraum 0.34.1 → 0.36.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 +134 -2
- package/dist/index.js +763 -17
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -72,6 +72,40 @@ interface GraphraumData<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
72
72
|
nodes: readonly GraphraumNode<NodeAttributes>[];
|
|
73
73
|
edges: readonly GraphraumEdge<EdgeAttributes>[];
|
|
74
74
|
}
|
|
75
|
+
interface GraphraumSetDataOptions {
|
|
76
|
+
/**
|
|
77
|
+
* Fit the camera to the new data (default `true`). `false` replaces the data and leaves the camera
|
|
78
|
+
* where it is, for hosts that pass a full snapshot on every change. On a graph that never had a
|
|
79
|
+
* fit the camera keeps its initial pose, so call `fitView()` once after the first data.
|
|
80
|
+
*/
|
|
81
|
+
fit?: boolean;
|
|
82
|
+
}
|
|
83
|
+
interface GraphraumCameraPoint3d {
|
|
84
|
+
x: number;
|
|
85
|
+
y: number;
|
|
86
|
+
z: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A 2D view: where the camera looks and how much of the world it shows from top to bottom. The
|
|
90
|
+
* visible width follows the canvas aspect, so a state read on one canvas size fits another.
|
|
91
|
+
*/
|
|
92
|
+
interface GraphraumCameraState2d {
|
|
93
|
+
center: {
|
|
94
|
+
x: number;
|
|
95
|
+
y: number;
|
|
96
|
+
};
|
|
97
|
+
mode: "2d";
|
|
98
|
+
/** World units from the top to the bottom edge of the canvas, with the current zoom applied. */
|
|
99
|
+
visibleHeight: number;
|
|
100
|
+
}
|
|
101
|
+
/** A 3D view: the camera position and the point it orbits and looks at. */
|
|
102
|
+
interface GraphraumCameraState3d {
|
|
103
|
+
mode: "3d";
|
|
104
|
+
position: GraphraumCameraPoint3d;
|
|
105
|
+
target: GraphraumCameraPoint3d;
|
|
106
|
+
}
|
|
107
|
+
/** The camera pose as plain JSON, for saving a view and restoring it with `setCameraState()`. */
|
|
108
|
+
type GraphraumCameraState = GraphraumCameraState2d | GraphraumCameraState3d;
|
|
75
109
|
interface GraphraumDataPatch<NodeAttributes = undefined, EdgeAttributes = undefined> {
|
|
76
110
|
addedNodes?: readonly GraphraumNode<NodeAttributes>[];
|
|
77
111
|
addedEdges?: readonly GraphraumEdge<EdgeAttributes>[];
|
|
@@ -232,8 +266,60 @@ type GraphraumPickHit = {
|
|
|
232
266
|
kind: "node";
|
|
233
267
|
id: string;
|
|
234
268
|
};
|
|
269
|
+
/** `linear`, cubic `easeIn` / `easeOut` / `easeInOut`, or a function from progress 0..1 to eased 0..1. */
|
|
270
|
+
type GraphraumEasing = "easeIn" | "easeInOut" | "easeOut" | "linear" | ((t: number) => number);
|
|
271
|
+
/** How depth of field animates focus moves and blur fades. */
|
|
272
|
+
interface GraphraumDepthOfFieldTransition {
|
|
273
|
+
/** Default 400; 0 jumps. `prefers-reduced-motion: reduce` always jumps. */
|
|
274
|
+
durationMs?: number;
|
|
275
|
+
/** Default `"easeInOut"`. */
|
|
276
|
+
easing?: GraphraumEasing;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Opt-in 3D depth of field. The focal plane follows the selection, so selected nodes stay sharp and
|
|
280
|
+
* the rest of the graph blurs with its distance from them, in front and behind, like a thin lens:
|
|
281
|
+
* the closer the camera is to the focus, the shallower the depth of field. A no-op in 2D. See
|
|
282
|
+
* `Graphraum.setDepthOfField`.
|
|
283
|
+
*/
|
|
284
|
+
interface GraphraumDepthOfFieldOptions {
|
|
285
|
+
/** Default `false`. While disabled no extra passes or render targets exist. Toggling fades the blur. */
|
|
286
|
+
enabled?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* `"selection"` (default) focuses the mean position of the selected nodes and fades the blur out
|
|
289
|
+
* when nothing is selected. A number fixes the focal plane at that camera distance in world units.
|
|
290
|
+
*/
|
|
291
|
+
focus?: "selection" | number;
|
|
292
|
+
/**
|
|
293
|
+
* Aperture, 0..1, default 0.5. A surface blurs by `strength * 24 * |d - f| / d` CSS pixels, where
|
|
294
|
+
* `d` is its camera distance and `f` the focal distance, clamped to `maxBlur`.
|
|
295
|
+
*/
|
|
296
|
+
strength?: number;
|
|
297
|
+
/** Largest blur radius in CSS pixels. Default 12. */
|
|
298
|
+
maxBlur?: number;
|
|
299
|
+
/** Blur radii below this many CSS pixels render sharp, which widens the in-focus band. Default 0. */
|
|
300
|
+
minBlur?: number;
|
|
301
|
+
/** Camera-to-focus distance in world units up to which `strength` applies fully. Default 0. */
|
|
302
|
+
nearDistance?: number;
|
|
303
|
+
/** Camera-to-focus distance in world units at which the blur has faded out. Default `Infinity` (no fade). */
|
|
304
|
+
farDistance?: number;
|
|
305
|
+
/** Animation for focus moves between selections and for blur fades. */
|
|
306
|
+
transition?: GraphraumDepthOfFieldTransition;
|
|
307
|
+
}
|
|
308
|
+
/** Options for one `Graphraum.setDepthOfField` call. */
|
|
309
|
+
interface GraphraumDepthOfFieldUpdate {
|
|
310
|
+
/** Default `true`: fade the blur to the new strength over the transition. `false` jumps. */
|
|
311
|
+
animate?: boolean;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* `off`: disabled. `inactive`: enabled with nothing to blur (2D mode, or no selection with
|
|
315
|
+
* `focus: "selection"`), so frames render directly. `active`: the last frame ran the blur passes.
|
|
316
|
+
* `unsupported`: enabled, but the WebGL context cannot run it, so frames render directly.
|
|
317
|
+
*/
|
|
318
|
+
type GraphraumDepthOfFieldStatus = "active" | "inactive" | "off" | "unsupported";
|
|
235
319
|
interface GraphraumOptions<NodeAttributes = undefined, EdgeAttributes = undefined> {
|
|
236
320
|
antialias?: boolean;
|
|
321
|
+
/** Opt-in 3D depth of field; off by default. Change it later with `setDepthOfField`. */
|
|
322
|
+
depthOfField?: GraphraumDepthOfFieldOptions;
|
|
237
323
|
maxVisibleEdges?: number;
|
|
238
324
|
maxVisibleNodes?: number;
|
|
239
325
|
maxPixelRatio?: number;
|
|
@@ -266,6 +352,7 @@ interface GraphraumDiagnostics {
|
|
|
266
352
|
aggregatedNodeClusters: number;
|
|
267
353
|
autoOrbit: GraphraumAutoOrbitStatus;
|
|
268
354
|
cpuFrameMilliseconds: number;
|
|
355
|
+
depthOfField: GraphraumDepthOfFieldStatus;
|
|
269
356
|
gpuFrameMilliseconds: number | null;
|
|
270
357
|
gpuDrawCalls: number;
|
|
271
358
|
gpuGeometries: number;
|
|
@@ -524,14 +611,24 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
524
611
|
/** True only during the orbit's own `controls.update(dt)`; see `handleViewChange`. */
|
|
525
612
|
private orbitUpdating;
|
|
526
613
|
private destroyed;
|
|
614
|
+
private canvasWidth;
|
|
615
|
+
private canvasHeight;
|
|
527
616
|
private cpuFrameMilliseconds;
|
|
528
617
|
private gpuFrameMilliseconds;
|
|
529
618
|
private gpuTimer;
|
|
530
619
|
private readonly viewListeners;
|
|
531
620
|
private readonly focusListeners;
|
|
532
621
|
private lastHoveredNodeId;
|
|
622
|
+
private readonly antialias;
|
|
623
|
+
private depthOfField;
|
|
624
|
+
private readonly depthOfFieldFocus;
|
|
625
|
+
/** Strength actually applied: fades toward `strength` while enabled and toward 0 when disabled. */
|
|
626
|
+
private readonly depthOfFieldStrength;
|
|
627
|
+
private depthOfFieldPass;
|
|
628
|
+
private depthOfFieldStatus;
|
|
533
629
|
constructor(container: HTMLElement, options?: GraphraumOptions<NodeAttributes, EdgeAttributes>);
|
|
534
|
-
|
|
630
|
+
/** Replaces the whole graph. Fits the camera unless `options.fit` is `false`. */
|
|
631
|
+
setData(data: GraphraumData<NodeAttributes, EdgeAttributes>, options?: GraphraumSetDataOptions): void;
|
|
535
632
|
/** Merge a topology patch without fitting or resetting the current camera. */
|
|
536
633
|
applyDataPatch(patch: GraphraumDataPatch<NodeAttributes, EdgeAttributes>): void;
|
|
537
634
|
private canAppendDataPatch;
|
|
@@ -587,6 +684,19 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
587
684
|
* before enabling it. Throws in 2D mode; `setMode("2d")` turns it off. Does nothing after `destroy()`.
|
|
588
685
|
*/
|
|
589
686
|
setAutoOrbit(options: GraphraumAutoOrbitOptions | false): void;
|
|
687
|
+
/**
|
|
688
|
+
* Updates 3D depth of field; omitted fields keep their current values. Enabling, disabling and a
|
|
689
|
+
* new `strength` fade the blur over `transition` unless `update.animate` is `false`; a change
|
|
690
|
+
* during a fade continues from the blur shown last. With `focus: "selection"` the focal plane
|
|
691
|
+
* follows the mean position of the selected nodes, re-measured from the camera every frame, and
|
|
692
|
+
* moves over `transition` when the selection changes; with nothing selected the blur fades out.
|
|
693
|
+
* `prefers-reduced-motion: reduce` jumps. Resolves `true` when the blur reaches the new strength
|
|
694
|
+
* and `false` when a later call or `destroy()` interrupts the fade. While it blurs each frame adds
|
|
695
|
+
* three draw calls; disabled, or with nothing to blur, it costs nothing. A no-op in 2D. Picking
|
|
696
|
+
* and DOM overlays are unaffected. Throws for invalid values.
|
|
697
|
+
*/
|
|
698
|
+
setDepthOfField(options: GraphraumDepthOfFieldOptions, update?: GraphraumDepthOfFieldUpdate): Promise<boolean>;
|
|
699
|
+
private depthOfFieldTargetStrength;
|
|
590
700
|
getNodePresentation(nodeId: string): CompiledGraphraumPresentation | null;
|
|
591
701
|
/** Returns canvas-relative screen coordinates for a node, or null when the node does not exist. */
|
|
592
702
|
getNodeScreenPosition(nodeId: string): GraphraumScreenPosition | null;
|
|
@@ -635,6 +745,20 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
635
745
|
* reduce` or a zero duration it jumps. Throws for an invalid duration or distance scale.
|
|
636
746
|
*/
|
|
637
747
|
flyTo(options: GraphraumFlyToOptions): Promise<boolean>;
|
|
748
|
+
/**
|
|
749
|
+
* The camera pose as plain JSON. 2D reports the centre and the world height the canvas shows; 3D
|
|
750
|
+
* reports the camera position and the point it orbits. Pass it to `setCameraState()` to return
|
|
751
|
+
* to the same view, for example after reloading a saved one.
|
|
752
|
+
*/
|
|
753
|
+
getCameraState(): GraphraumCameraState;
|
|
754
|
+
/**
|
|
755
|
+
* Puts the camera on a pose from `getCameraState()` at once, without fitting or animating, and
|
|
756
|
+
* cancels a running `flyTo`. Throws for a state of the other mode (switch with `setMode()` first)
|
|
757
|
+
* or with a non-finite number. 2D keeps the saved height and takes the width from the canvas, so a
|
|
758
|
+
* view saved on one canvas size fits another. 3D derives the clip planes from the pose so the whole
|
|
759
|
+
* graph stays in view. Apply it after the canvas has its size: a 2D resize to a new size refits.
|
|
760
|
+
*/
|
|
761
|
+
setCameraState(state: GraphraumCameraState): void;
|
|
638
762
|
/** Nodes whose id is in `nodeIds`, in graph order; every node when `nodeIds` is omitted. */
|
|
639
763
|
private nodesWithIds;
|
|
640
764
|
/** The closest front-view camera that frames `nodes` (issue #124); applies the container aspect. */
|
|
@@ -664,6 +788,14 @@ declare class Graphraum<NodeAttributes = undefined, EdgeAttributes = undefined>
|
|
|
664
788
|
/** Renders a pending frame now instead of on the next animation frame. */
|
|
665
789
|
private flushRender;
|
|
666
790
|
private renderFrame;
|
|
791
|
+
/**
|
|
792
|
+
* Decides whether this frame blurs and records why in the diagnostics status. Returns null to
|
|
793
|
+
* render directly. Requests another frame while a fade or focus move runs; both end on their own.
|
|
794
|
+
*/
|
|
795
|
+
private prepareDepthOfField;
|
|
796
|
+
/** Mean world position of the selected nodes, read every frame so a running layout keeps them in focus. */
|
|
797
|
+
private selectionFocusPoint;
|
|
798
|
+
private disposeDepthOfFieldPass;
|
|
667
799
|
private stopAutoOrbit;
|
|
668
800
|
/**
|
|
669
801
|
* One orbit frame. `autoRotate` is on only for this `update(dt)` call: OrbitControls also calls
|
|
@@ -851,4 +983,4 @@ declare function isTransparentGraphraumBackground(background: GraphraumBackgroun
|
|
|
851
983
|
/** Defines typed, serializable graph visuals without coupling application data to the render loop. */
|
|
852
984
|
declare function defineVisuals<NodeAttributes = undefined, EdgeAttributes = undefined>(visuals: GraphraumVisualMapper<NodeAttributes, EdgeAttributes>): GraphraumVisualMapper<NodeAttributes, EdgeAttributes>;
|
|
853
985
|
//#endregion
|
|
854
|
-
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 GraphraumColor, type GraphraumData, type GraphraumDataPatch, type GraphraumDiagnostics, 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 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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdditiveBlending, Box3, CanvasTexture, Color, InstancedBufferAttribute, InstancedMesh, MOUSE, Matrix4, MeshBasicMaterial, NormalBlending, OrthographicCamera, PerspectiveCamera, Plane, PlaneGeometry, Raycaster, RepeatWrapping, SRGBColorSpace, Scene, ShaderMaterial, SphereGeometry, TOUCH, Texture, Vector2, Vector3, WebGLRenderer } from "three";
|
|
1
|
+
import { AdditiveBlending, Box3, BufferGeometry, CanvasTexture, Color, DepthTexture, Float32BufferAttribute, GLSL3, InstancedBufferAttribute, InstancedMesh, MOUSE, Matrix4, Mesh, MeshBasicMaterial, NoBlending, NormalBlending, OrthographicCamera, PerspectiveCamera, Plane, PlaneGeometry, Raycaster, RepeatWrapping, SRGBColorSpace, Scene, ShaderMaterial, SphereGeometry, TOUCH, Texture, UnsignedByteType, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
|
|
2
2
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
3
3
|
//#region src/node-axes.ts
|
|
4
4
|
function assertPositiveFinite(value, axis, nodeId) {
|
|
@@ -904,6 +904,63 @@ function interpolateScale(from, to, t) {
|
|
|
904
904
|
return from * (to / from) ** t;
|
|
905
905
|
}
|
|
906
906
|
//#endregion
|
|
907
|
+
//#region src/camera-state.ts
|
|
908
|
+
/**
|
|
909
|
+
* Checks a camera state before it touches the camera: an object of the graph's mode with finite
|
|
910
|
+
* numbers, a positive visible height in 2D, and a position apart from the target in 3D. The state
|
|
911
|
+
* is usually parsed JSON, so nothing about its shape is taken on trust.
|
|
912
|
+
*/
|
|
913
|
+
function validateCameraState(state, mode) {
|
|
914
|
+
if (typeof state !== "object" || state === null) throw new Error("A camera state must be an object returned by getCameraState().");
|
|
915
|
+
const stateMode = state.mode;
|
|
916
|
+
if (stateMode !== "2d" && stateMode !== "3d") throw new Error(`A camera state needs mode "2d" or "3d"; received ${JSON.stringify(stateMode)}.`);
|
|
917
|
+
if (state.mode !== mode) throw new Error(`Cannot apply a ${state.mode} camera state to a graph in ${mode} mode; call setMode("${state.mode}") first.`);
|
|
918
|
+
if (state.mode === "2d") {
|
|
919
|
+
const x = finite(state.center?.x, "center.x");
|
|
920
|
+
const y = finite(state.center?.y, "center.y");
|
|
921
|
+
const visibleHeight = finite(state.visibleHeight, "visibleHeight");
|
|
922
|
+
if (visibleHeight <= 0) throw new Error(`Camera state visibleHeight must be above 0; received ${visibleHeight}.`);
|
|
923
|
+
return {
|
|
924
|
+
center: {
|
|
925
|
+
x,
|
|
926
|
+
y
|
|
927
|
+
},
|
|
928
|
+
mode: "2d",
|
|
929
|
+
visibleHeight
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const position = point3d(state.position, "position");
|
|
933
|
+
const target = point3d(state.target, "target");
|
|
934
|
+
if (position.x === target.x && position.y === target.y && position.z === target.z) throw new Error("Camera state position and target must differ.");
|
|
935
|
+
return {
|
|
936
|
+
mode: "3d",
|
|
937
|
+
position,
|
|
938
|
+
target
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
function point3d(value, name) {
|
|
942
|
+
return {
|
|
943
|
+
x: finite(value?.x, `${name}.x`),
|
|
944
|
+
y: finite(value?.y, `${name}.y`),
|
|
945
|
+
z: finite(value?.z, `${name}.z`)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function finite(value, name) {
|
|
949
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Camera state ${name} must be a finite number; received ${String(value)}.`);
|
|
950
|
+
return value;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Clip planes for a perspective camera `distance` from its target with the graph reaching
|
|
954
|
+
* `graphRadius` around it: the far plane holds the whole graph with the slack a fit leaves for
|
|
955
|
+
* zooming out, and the near plane stays close enough not to cut nodes in front of the camera.
|
|
956
|
+
*/
|
|
957
|
+
function perspectiveClipRange(distance, graphRadius) {
|
|
958
|
+
return {
|
|
959
|
+
far: Math.max(distance * 10, (distance + graphRadius) * 1.1),
|
|
960
|
+
near: Math.min(Math.max(distance / 1e4, .1), distance / 2)
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
//#endregion
|
|
907
964
|
//#region src/theme.ts
|
|
908
965
|
/** Dark canvas: Ink Black, Turf Green, Porcelain. */
|
|
909
966
|
const graphraumThemeDark = Object.freeze({
|
|
@@ -1203,6 +1260,501 @@ function isAppendOnlyDataPatch(patch) {
|
|
|
1203
1260
|
return (patch.removedNodeIds?.length ?? 0) === 0 && (patch.removedEdgeIds?.length ?? 0) === 0;
|
|
1204
1261
|
}
|
|
1205
1262
|
//#endregion
|
|
1263
|
+
//#region src/depth-of-field.ts
|
|
1264
|
+
const DEPTH_OF_FIELD_DEFAULTS = {
|
|
1265
|
+
enabled: false,
|
|
1266
|
+
farDistance: Number.POSITIVE_INFINITY,
|
|
1267
|
+
focus: "selection",
|
|
1268
|
+
maxBlur: 12,
|
|
1269
|
+
minBlur: 0,
|
|
1270
|
+
nearDistance: 0,
|
|
1271
|
+
strength: .5,
|
|
1272
|
+
transition: {
|
|
1273
|
+
durationMs: 400,
|
|
1274
|
+
easing: "easeInOut"
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
const EASINGS = {
|
|
1278
|
+
easeIn: (t) => t * t * t,
|
|
1279
|
+
easeInOut: easeInOutCubic,
|
|
1280
|
+
easeOut: (t) => 1 - (1 - t) ** 3,
|
|
1281
|
+
linear: (t) => t
|
|
1282
|
+
};
|
|
1283
|
+
function resolveEasing(easing) {
|
|
1284
|
+
return typeof easing === "function" ? easing : EASINGS[easing];
|
|
1285
|
+
}
|
|
1286
|
+
function isEasing(value) {
|
|
1287
|
+
return typeof value === "function" || typeof value === "string" && Object.hasOwn(EASINGS, value);
|
|
1288
|
+
}
|
|
1289
|
+
function isNonNegativeNumber(value) {
|
|
1290
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
1291
|
+
}
|
|
1292
|
+
/** Validates `options` and merges them onto `base`. Throws an actionable error for invalid values. */
|
|
1293
|
+
function resolveDepthOfFieldOptions(options, base = DEPTH_OF_FIELD_DEFAULTS) {
|
|
1294
|
+
if (typeof options !== "object" || options === null) throw new Error("Depth of field options must be an object, e.g. { enabled: true, strength: 0.5 }.");
|
|
1295
|
+
if (options.transition !== void 0 && (typeof options.transition !== "object" || options.transition === null)) throw new Error("Depth of field transition must be an object, e.g. { durationMs: 400, easing: \"easeInOut\" }.");
|
|
1296
|
+
const resolved = {
|
|
1297
|
+
enabled: options.enabled ?? base.enabled,
|
|
1298
|
+
farDistance: options.farDistance ?? base.farDistance,
|
|
1299
|
+
focus: options.focus ?? base.focus,
|
|
1300
|
+
maxBlur: options.maxBlur ?? base.maxBlur,
|
|
1301
|
+
minBlur: options.minBlur ?? base.minBlur,
|
|
1302
|
+
nearDistance: options.nearDistance ?? base.nearDistance,
|
|
1303
|
+
strength: options.strength ?? base.strength,
|
|
1304
|
+
transition: {
|
|
1305
|
+
durationMs: options.transition?.durationMs ?? base.transition.durationMs,
|
|
1306
|
+
easing: options.transition?.easing ?? base.transition.easing
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
const { enabled, farDistance, focus, maxBlur, minBlur, nearDistance, strength, transition } = resolved;
|
|
1310
|
+
if (typeof enabled !== "boolean") throw new Error(`Depth of field enabled must be a boolean; received ${String(enabled)}.`);
|
|
1311
|
+
if (!isNonNegativeNumber(strength) || strength > 1) throw new Error(`Depth of field strength must be a number from 0 to 1; received ${String(strength)}.`);
|
|
1312
|
+
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
|
+
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
|
+
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)}.`);
|
|
1315
|
+
if (!isNonNegativeNumber(transition.durationMs)) throw new Error(`Depth of field transition durationMs must be a non-negative finite number of milliseconds; received ${String(transition.durationMs)}.`);
|
|
1316
|
+
if (!isEasing(transition.easing)) throw new Error(`Depth of field transition easing must be "linear", "easeIn", "easeOut", "easeInOut" or a function; received ${String(transition.easing)}.`);
|
|
1317
|
+
return resolved;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* How much of `strength` applies when the focus is `focusDistance` from the camera: 1 up to
|
|
1321
|
+
* `nearDistance`, easing to 0 at `farDistance`, so a zoomed-out overview stays sharp.
|
|
1322
|
+
*/
|
|
1323
|
+
function distanceFade(focusDistance, nearDistance, farDistance) {
|
|
1324
|
+
if (focusDistance <= nearDistance) return 1;
|
|
1325
|
+
if (focusDistance >= farDistance) return 0;
|
|
1326
|
+
const t = (focusDistance - nearDistance) / (farDistance - nearDistance);
|
|
1327
|
+
return 1 - t * t * (3 - 2 * t);
|
|
1328
|
+
}
|
|
1329
|
+
/** Distance in front of the camera along its view axis, from a column-major view matrix (`matrixWorldInverse`). */
|
|
1330
|
+
function viewDistance(point, viewMatrix) {
|
|
1331
|
+
const m = (index) => viewMatrix[index] ?? 0;
|
|
1332
|
+
return -(m(2) * point.x + m(6) * point.y + m(10) * point.z + m(14));
|
|
1333
|
+
}
|
|
1334
|
+
/** Mean position of `points`, or null when there are none. */
|
|
1335
|
+
function meanPoint(points) {
|
|
1336
|
+
let count = 0;
|
|
1337
|
+
const sum = {
|
|
1338
|
+
x: 0,
|
|
1339
|
+
y: 0,
|
|
1340
|
+
z: 0
|
|
1341
|
+
};
|
|
1342
|
+
for (const point of points) {
|
|
1343
|
+
count += 1;
|
|
1344
|
+
sum.x += point.x;
|
|
1345
|
+
sum.y += point.y;
|
|
1346
|
+
sum.z += point.z;
|
|
1347
|
+
}
|
|
1348
|
+
return count === 0 ? null : {
|
|
1349
|
+
x: sum.x / count,
|
|
1350
|
+
y: sum.y / count,
|
|
1351
|
+
z: sum.z / count
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* A scalar animated toward a target. Retargeting starts from the value shown last, so an interrupted
|
|
1356
|
+
* transition continues smoothly instead of restarting. Each retarget returns a promise that resolves
|
|
1357
|
+
* `true` when the value arrives and `false` when another retarget or `cancel` replaces it.
|
|
1358
|
+
*/
|
|
1359
|
+
var Tween = class {
|
|
1360
|
+
from;
|
|
1361
|
+
to;
|
|
1362
|
+
value;
|
|
1363
|
+
startMs = null;
|
|
1364
|
+
durationMs = 0;
|
|
1365
|
+
easing = EASINGS.linear;
|
|
1366
|
+
settle = null;
|
|
1367
|
+
constructor(value) {
|
|
1368
|
+
this.from = value;
|
|
1369
|
+
this.to = value;
|
|
1370
|
+
this.value = value;
|
|
1371
|
+
}
|
|
1372
|
+
get target() {
|
|
1373
|
+
return this.to;
|
|
1374
|
+
}
|
|
1375
|
+
get settled() {
|
|
1376
|
+
return this.settle === null && this.value === this.to;
|
|
1377
|
+
}
|
|
1378
|
+
retarget(to, durationMs, easing) {
|
|
1379
|
+
this.settle?.(false);
|
|
1380
|
+
this.settle = null;
|
|
1381
|
+
this.from = this.value;
|
|
1382
|
+
this.to = to;
|
|
1383
|
+
this.startMs = null;
|
|
1384
|
+
this.durationMs = durationMs;
|
|
1385
|
+
this.easing = easing;
|
|
1386
|
+
if (durationMs === 0 || this.from === to) {
|
|
1387
|
+
this.value = to;
|
|
1388
|
+
return Promise.resolve(true);
|
|
1389
|
+
}
|
|
1390
|
+
return new Promise((resolve) => {
|
|
1391
|
+
this.settle = resolve;
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
/** Value at `nowMs`; the first sample after a retarget starts its clock. */
|
|
1395
|
+
sample(nowMs) {
|
|
1396
|
+
if (this.settle === null) return this.value;
|
|
1397
|
+
this.startMs ??= nowMs;
|
|
1398
|
+
const progress = flightProgress(nowMs - this.startMs, this.durationMs);
|
|
1399
|
+
this.value = progress >= 1 ? this.to : this.from + (this.to - this.from) * this.easing(progress);
|
|
1400
|
+
if (progress >= 1) this.finish(true);
|
|
1401
|
+
return this.value;
|
|
1402
|
+
}
|
|
1403
|
+
/** Arrives at the target now, e.g. for reduced motion or when nothing renders the frames. */
|
|
1404
|
+
jump() {
|
|
1405
|
+
this.value = this.to;
|
|
1406
|
+
this.finish(true);
|
|
1407
|
+
}
|
|
1408
|
+
cancel() {
|
|
1409
|
+
this.finish(false);
|
|
1410
|
+
}
|
|
1411
|
+
finish(arrived) {
|
|
1412
|
+
const settle = this.settle;
|
|
1413
|
+
this.settle = null;
|
|
1414
|
+
settle?.(arrived);
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
/**
|
|
1418
|
+
* Animates the focus between selections. The focus is a world point rather than a depth, so its
|
|
1419
|
+
* distance is re-measured from the camera every frame and the selected nodes stay sharp while the
|
|
1420
|
+
* camera orbits, pans or zooms; only a selection change animates. With no target the blur fades out
|
|
1421
|
+
* around the last focus point. A retarget mid-transition continues from the focus shown last.
|
|
1422
|
+
*/
|
|
1423
|
+
var DepthOfFieldFocus = class {
|
|
1424
|
+
transition = null;
|
|
1425
|
+
lastAmount = 0;
|
|
1426
|
+
lastPoint = null;
|
|
1427
|
+
/** Starts a transition from the focus shown in the last frame, e.g. after a selection change. */
|
|
1428
|
+
retarget(durationMs, easing) {
|
|
1429
|
+
this.transition = {
|
|
1430
|
+
durationMs,
|
|
1431
|
+
easing,
|
|
1432
|
+
fromAmount: this.lastAmount,
|
|
1433
|
+
fromPoint: this.lastAmount > 0 ? this.lastPoint : null,
|
|
1434
|
+
startMs: null
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
/** Focus for the frame at `nowMs`. `jump` (reduced motion) skips the animation. */
|
|
1438
|
+
evaluate(nowMs, target, jump) {
|
|
1439
|
+
const transition = this.transition;
|
|
1440
|
+
const toAmount = target ? 1 : 0;
|
|
1441
|
+
let eased = 1;
|
|
1442
|
+
let progress = 1;
|
|
1443
|
+
if (transition && !jump && transition.durationMs > 0) {
|
|
1444
|
+
transition.startMs ??= nowMs;
|
|
1445
|
+
progress = flightProgress(nowMs - transition.startMs, transition.durationMs);
|
|
1446
|
+
eased = progress >= 1 ? 1 : transition.easing(progress);
|
|
1447
|
+
}
|
|
1448
|
+
const fromAmount = transition?.fromAmount ?? toAmount;
|
|
1449
|
+
const fromPoint = transition?.fromPoint ?? null;
|
|
1450
|
+
const amount = fromAmount + (toAmount - fromAmount) * eased;
|
|
1451
|
+
const point = target ? fromPoint ? lerpPoint(fromPoint, target, eased) : target : fromPoint ?? this.lastPoint;
|
|
1452
|
+
const settled = progress >= 1;
|
|
1453
|
+
if (settled) this.transition = null;
|
|
1454
|
+
this.lastAmount = amount;
|
|
1455
|
+
this.lastPoint = point;
|
|
1456
|
+
return {
|
|
1457
|
+
amount,
|
|
1458
|
+
point,
|
|
1459
|
+
settled
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
function lerpPoint(from, to, t) {
|
|
1464
|
+
return {
|
|
1465
|
+
x: from.x + (to.x - from.x) * t,
|
|
1466
|
+
y: from.y + (to.y - from.y) * t,
|
|
1467
|
+
z: from.z + (to.z - from.z) * t
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
//#endregion
|
|
1471
|
+
//#region src/shader-output.ts
|
|
1472
|
+
/**
|
|
1473
|
+
* Encodes `gl_FragColor.rgb` from the linear working space into the renderer's output color space:
|
|
1474
|
+
* sRGB on the canvas (so a `#8a9096` theme color draws as `#8a9096`), unchanged on linear render
|
|
1475
|
+
* targets. Alpha is left untouched, so anti-aliased coverage and blending are unaffected.
|
|
1476
|
+
*
|
|
1477
|
+
* Append once to every display fragment shader, after its final `gl_FragColor` assignment.
|
|
1478
|
+
* Requires a `ShaderMaterial` (not `RawShaderMaterial`), whose prefix defines `linearToOutputTexel`.
|
|
1479
|
+
*/
|
|
1480
|
+
const OUTPUT_COLOR_SPACE_FRAGMENT = "#include <colorspace_fragment>";
|
|
1481
|
+
//#endregion
|
|
1482
|
+
//#region src/depth-of-field-pass.ts
|
|
1483
|
+
/** Layer the edge mesh joins so the edge depth pass can draw it alone. Picking raycasts layer 0. */
|
|
1484
|
+
const EDGE_DEPTH_LAYER = 1;
|
|
1485
|
+
/** Disk taps around the centre of each half-resolution blur texel. */
|
|
1486
|
+
const BLUR_TAPS = 48;
|
|
1487
|
+
const vertexShader$3 = `
|
|
1488
|
+
varying vec2 vUv;
|
|
1489
|
+
|
|
1490
|
+
void main() {
|
|
1491
|
+
vUv = uv;
|
|
1492
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
1493
|
+
}
|
|
1494
|
+
`;
|
|
1495
|
+
const blurRadiusChunk = `
|
|
1496
|
+
#include <packing>
|
|
1497
|
+
uniform sampler2D nodeDepth;
|
|
1498
|
+
uniform sampler2D edgeDepth;
|
|
1499
|
+
uniform ivec2 depthSize;
|
|
1500
|
+
uniform float cameraNear;
|
|
1501
|
+
uniform float cameraFar;
|
|
1502
|
+
uniform float focusDistance;
|
|
1503
|
+
uniform float aperture;
|
|
1504
|
+
uniform float minBlur;
|
|
1505
|
+
uniform float maxBlur;
|
|
1506
|
+
|
|
1507
|
+
float surfaceDistance(ivec2 pixel) {
|
|
1508
|
+
pixel = clamp(pixel, ivec2(0), depthSize - 1);
|
|
1509
|
+
float depth = texelFetch(nodeDepth, pixel, 0).x;
|
|
1510
|
+
if (depth >= 1.0) depth = texelFetch(edgeDepth, pixel, 0).x;
|
|
1511
|
+
return -perspectiveDepthToViewZ(depth, cameraNear, cameraFar);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
float radiusAt(float surface) {
|
|
1515
|
+
float radius = aperture * abs(surface - focusDistance) / max(surface, 1e-6);
|
|
1516
|
+
return radius < minBlur ? 0.0 : min(radius, maxBlur);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
float blurRadius(ivec2 pixel) {
|
|
1520
|
+
return radiusAt(surfaceDistance(pixel));
|
|
1521
|
+
}
|
|
1522
|
+
`;
|
|
1523
|
+
const blurFragmentShader = `
|
|
1524
|
+
${blurRadiusChunk}
|
|
1525
|
+
uniform sampler2D colorTexture;
|
|
1526
|
+
layout(location = 0) out highp vec4 blurColor;
|
|
1527
|
+
layout(location = 1) out highp vec4 nearColor;
|
|
1528
|
+
|
|
1529
|
+
const float GOLDEN_ANGLE = 2.39996323;
|
|
1530
|
+
|
|
1531
|
+
void main() {
|
|
1532
|
+
ivec2 base = ivec2(gl_FragCoord.xy) * 2;
|
|
1533
|
+
float centreSurface = surfaceDistance(base);
|
|
1534
|
+
vec4 sum = texelFetch(colorTexture, base, 0);
|
|
1535
|
+
float total = 1.0;
|
|
1536
|
+
vec4 near = vec4(0.0);
|
|
1537
|
+
// Interleaved gradient noise turns the spiral's fixed tap pattern into fine noise that the
|
|
1538
|
+
// 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))));
|
|
1540
|
+
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);
|
|
1544
|
+
float surface = surfaceDistance(pixel);
|
|
1545
|
+
float radius = radiusAt(surface);
|
|
1546
|
+
float weight = clamp(radius - distanceFromCentre + 1.0, 0.0, 1.0);
|
|
1547
|
+
vec4 color = texelFetch(colorTexture, pixel, 0);
|
|
1548
|
+
sum += color * weight;
|
|
1549
|
+
total += weight;
|
|
1550
|
+
if (surface < focusDistance && surface < centreSurface) {
|
|
1551
|
+
near += color * weight * (maxBlur * maxBlur / max(radius * radius, 1.0));
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
blurColor = sum / total;
|
|
1555
|
+
near /= ${BLUR_TAPS}.0;
|
|
1556
|
+
nearColor = near.a > 1.0 ? near / near.a : near;
|
|
1557
|
+
}
|
|
1558
|
+
`;
|
|
1559
|
+
const compositeFragmentShader = `
|
|
1560
|
+
${blurRadiusChunk}
|
|
1561
|
+
uniform sampler2D sharpTexture;
|
|
1562
|
+
uniform sampler2D blurTexture;
|
|
1563
|
+
uniform sampler2D nearTexture;
|
|
1564
|
+
varying vec2 vUv;
|
|
1565
|
+
|
|
1566
|
+
void main() {
|
|
1567
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
1568
|
+
vec4 near = texture2D(nearTexture, vUv);
|
|
1569
|
+
vec4 sharp = texelFetch(sharpTexture, pixel, 0);
|
|
1570
|
+
sharp = near + sharp * (1.0 - near.a);
|
|
1571
|
+
vec4 blurred = texture2D(blurTexture, vUv);
|
|
1572
|
+
gl_FragColor = mix(sharp, blurred, smoothstep(0.5, 2.0, blurRadius(pixel)));
|
|
1573
|
+
${OUTPUT_COLOR_SPACE_FRAGMENT}
|
|
1574
|
+
}
|
|
1575
|
+
`;
|
|
1576
|
+
function blurUniforms() {
|
|
1577
|
+
return {
|
|
1578
|
+
cameraFar: { value: 1 },
|
|
1579
|
+
aperture: { value: 0 },
|
|
1580
|
+
cameraNear: { value: .1 },
|
|
1581
|
+
depthSize: { value: new Vector2() },
|
|
1582
|
+
edgeDepth: { value: null },
|
|
1583
|
+
focusDistance: { value: 1 },
|
|
1584
|
+
maxBlur: { value: 0 },
|
|
1585
|
+
minBlur: { value: 0 },
|
|
1586
|
+
nodeDepth: { value: null }
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function fullscreenMaterial(fragmentShader, uniforms, glslVersion = null) {
|
|
1590
|
+
return new ShaderMaterial({
|
|
1591
|
+
blending: NoBlending,
|
|
1592
|
+
depthTest: false,
|
|
1593
|
+
depthWrite: false,
|
|
1594
|
+
fragmentShader,
|
|
1595
|
+
glslVersion,
|
|
1596
|
+
uniforms,
|
|
1597
|
+
vertexShader: vertexShader$3
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
/** One triangle that covers the viewport. */
|
|
1601
|
+
function fullscreenTriangle() {
|
|
1602
|
+
const geometry = new BufferGeometry();
|
|
1603
|
+
geometry.setAttribute("position", new Float32BufferAttribute([
|
|
1604
|
+
-1,
|
|
1605
|
+
-1,
|
|
1606
|
+
0,
|
|
1607
|
+
3,
|
|
1608
|
+
-1,
|
|
1609
|
+
0,
|
|
1610
|
+
-1,
|
|
1611
|
+
3,
|
|
1612
|
+
0
|
|
1613
|
+
], 3));
|
|
1614
|
+
geometry.setAttribute("uv", new Float32BufferAttribute([
|
|
1615
|
+
0,
|
|
1616
|
+
0,
|
|
1617
|
+
2,
|
|
1618
|
+
0,
|
|
1619
|
+
0,
|
|
1620
|
+
2
|
|
1621
|
+
], 2));
|
|
1622
|
+
return geometry;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Renders the scene into a target, the edges into a depth-only target, blurs at half resolution and
|
|
1626
|
+
* composites onto the canvas: three extra draw calls per frame while it runs.
|
|
1627
|
+
* Created only while depth of field has something to blur; `dispose` releases every GPU resource.
|
|
1628
|
+
*/
|
|
1629
|
+
var DepthOfFieldPass = class {
|
|
1630
|
+
sceneTarget;
|
|
1631
|
+
edgeDepthTarget;
|
|
1632
|
+
blurTarget;
|
|
1633
|
+
blurMaterial = fullscreenMaterial(blurFragmentShader, {
|
|
1634
|
+
...blurUniforms(),
|
|
1635
|
+
colorTexture: { value: null }
|
|
1636
|
+
}, GLSL3);
|
|
1637
|
+
compositeMaterial = fullscreenMaterial(compositeFragmentShader, {
|
|
1638
|
+
...blurUniforms(),
|
|
1639
|
+
blurTexture: { value: null },
|
|
1640
|
+
nearTexture: { value: null },
|
|
1641
|
+
sharpTexture: { value: null }
|
|
1642
|
+
});
|
|
1643
|
+
quad = new Mesh(fullscreenTriangle(), this.blurMaterial);
|
|
1644
|
+
quadScene = new Scene().add(this.quad);
|
|
1645
|
+
quadCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
1646
|
+
size = new Vector2();
|
|
1647
|
+
/** Depth of field needs WebGL 2 for depth textures and sRGB render targets. */
|
|
1648
|
+
static isSupported(renderer) {
|
|
1649
|
+
return typeof WebGL2RenderingContext !== "undefined" && renderer.getContext() instanceof WebGL2RenderingContext;
|
|
1650
|
+
}
|
|
1651
|
+
constructor(antialias) {
|
|
1652
|
+
const color = {
|
|
1653
|
+
colorSpace: SRGBColorSpace,
|
|
1654
|
+
type: UnsignedByteType
|
|
1655
|
+
};
|
|
1656
|
+
this.sceneTarget = new WebGLRenderTarget(1, 1, {
|
|
1657
|
+
...color,
|
|
1658
|
+
depthTexture: new DepthTexture(1, 1),
|
|
1659
|
+
samples: antialias ? 4 : 0
|
|
1660
|
+
});
|
|
1661
|
+
this.edgeDepthTarget = new WebGLRenderTarget(1, 1, { depthTexture: new DepthTexture(1, 1) });
|
|
1662
|
+
this.blurTarget = new WebGLRenderTarget(1, 1, {
|
|
1663
|
+
...color,
|
|
1664
|
+
count: 2
|
|
1665
|
+
});
|
|
1666
|
+
this.quad.frustumCulled = false;
|
|
1667
|
+
}
|
|
1668
|
+
render(renderer, scene, camera, edgeMesh, frame) {
|
|
1669
|
+
const { x: width, y: height } = renderer.getDrawingBufferSize(this.size);
|
|
1670
|
+
this.setSize(width, height);
|
|
1671
|
+
const previousTarget = renderer.getRenderTarget();
|
|
1672
|
+
renderer.setRenderTarget(this.sceneTarget);
|
|
1673
|
+
renderer.render(scene, camera);
|
|
1674
|
+
this.renderEdgeDepth(renderer, scene, camera, edgeMesh);
|
|
1675
|
+
const shared = {
|
|
1676
|
+
aperture: frame.aperturePixels,
|
|
1677
|
+
cameraFar: camera.far,
|
|
1678
|
+
cameraNear: camera.near,
|
|
1679
|
+
depthSize: this.size,
|
|
1680
|
+
edgeDepth: this.edgeDepthTarget.depthTexture,
|
|
1681
|
+
focusDistance: frame.focusDistance,
|
|
1682
|
+
maxBlur: frame.maxBlurPixels,
|
|
1683
|
+
minBlur: frame.minBlurPixels,
|
|
1684
|
+
nodeDepth: this.sceneTarget.depthTexture
|
|
1685
|
+
};
|
|
1686
|
+
this.quad.material = this.blurMaterial;
|
|
1687
|
+
this.setUniforms(this.blurMaterial, {
|
|
1688
|
+
...shared,
|
|
1689
|
+
colorTexture: this.sceneTarget.texture
|
|
1690
|
+
});
|
|
1691
|
+
this.drawQuad(renderer, this.blurTarget);
|
|
1692
|
+
this.quad.material = this.compositeMaterial;
|
|
1693
|
+
this.setUniforms(this.compositeMaterial, {
|
|
1694
|
+
...shared,
|
|
1695
|
+
blurTexture: this.blurTarget.textures[0],
|
|
1696
|
+
nearTexture: this.blurTarget.textures[1],
|
|
1697
|
+
sharpTexture: this.sceneTarget.texture
|
|
1698
|
+
});
|
|
1699
|
+
this.drawQuad(renderer, previousTarget);
|
|
1700
|
+
}
|
|
1701
|
+
dispose() {
|
|
1702
|
+
this.sceneTarget.depthTexture?.dispose();
|
|
1703
|
+
this.sceneTarget.dispose();
|
|
1704
|
+
this.edgeDepthTarget.depthTexture?.dispose();
|
|
1705
|
+
this.edgeDepthTarget.dispose();
|
|
1706
|
+
this.blurTarget.dispose();
|
|
1707
|
+
this.blurMaterial.dispose();
|
|
1708
|
+
this.compositeMaterial.dispose();
|
|
1709
|
+
this.quad.geometry.dispose();
|
|
1710
|
+
}
|
|
1711
|
+
/** Draws the edges alone with depth writes on and colour writes off, then restores their material. */
|
|
1712
|
+
renderEdgeDepth(renderer, scene, camera, edgeMesh) {
|
|
1713
|
+
renderer.setRenderTarget(this.edgeDepthTarget);
|
|
1714
|
+
if (!edgeMesh || edgeMesh.count === 0 || Array.isArray(edgeMesh.material)) {
|
|
1715
|
+
renderer.clear();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
const material = edgeMesh.material;
|
|
1719
|
+
const { colorWrite, depthTest, depthWrite } = material;
|
|
1720
|
+
const layerMask = camera.layers.mask;
|
|
1721
|
+
const background = scene.background;
|
|
1722
|
+
edgeMesh.layers.enable(EDGE_DEPTH_LAYER);
|
|
1723
|
+
material.colorWrite = false;
|
|
1724
|
+
material.depthTest = true;
|
|
1725
|
+
material.depthWrite = true;
|
|
1726
|
+
camera.layers.set(EDGE_DEPTH_LAYER);
|
|
1727
|
+
scene.background = null;
|
|
1728
|
+
try {
|
|
1729
|
+
renderer.render(scene, camera);
|
|
1730
|
+
} finally {
|
|
1731
|
+
material.colorWrite = colorWrite;
|
|
1732
|
+
material.depthTest = depthTest;
|
|
1733
|
+
material.depthWrite = depthWrite;
|
|
1734
|
+
camera.layers.mask = layerMask;
|
|
1735
|
+
scene.background = background;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
drawQuad(renderer, target) {
|
|
1739
|
+
renderer.setRenderTarget(target);
|
|
1740
|
+
renderer.render(this.quadScene, this.quadCamera);
|
|
1741
|
+
}
|
|
1742
|
+
setUniforms(material, values) {
|
|
1743
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1744
|
+
const uniform = material.uniforms[name];
|
|
1745
|
+
if (uniform) uniform.value = value;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
setSize(width, height) {
|
|
1749
|
+
if (this.sceneTarget.width === width && this.sceneTarget.height === height) return;
|
|
1750
|
+
this.sceneTarget.setSize(width, height);
|
|
1751
|
+
this.edgeDepthTarget.setSize(width, height);
|
|
1752
|
+
const halfWidth = Math.max(1, Math.ceil(width / 2));
|
|
1753
|
+
const halfHeight = Math.max(1, Math.ceil(height / 2));
|
|
1754
|
+
this.blurTarget.setSize(halfWidth, halfHeight);
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
//#endregion
|
|
1206
1758
|
//#region src/edge-paths.ts
|
|
1207
1759
|
const AUTO_OFFSET_FRACTION = .18;
|
|
1208
1760
|
function segmentCountForPath(path, tier) {
|
|
@@ -1492,17 +2044,6 @@ function pickClosestEdgeIndex(point, segments) {
|
|
|
1492
2044
|
return nearestEdgeIndex;
|
|
1493
2045
|
}
|
|
1494
2046
|
//#endregion
|
|
1495
|
-
//#region src/shader-output.ts
|
|
1496
|
-
/**
|
|
1497
|
-
* Encodes `gl_FragColor.rgb` from the linear working space into the renderer's output color space:
|
|
1498
|
-
* sRGB on the canvas (so a `#8a9096` theme color draws as `#8a9096`), unchanged on linear render
|
|
1499
|
-
* targets. Alpha is left untouched, so anti-aliased coverage and blending are unaffected.
|
|
1500
|
-
*
|
|
1501
|
-
* Append once to every display fragment shader, after its final `gl_FragColor` assignment.
|
|
1502
|
-
* Requires a `ShaderMaterial` (not `RawShaderMaterial`), whose prefix defines `linearToOutputTexel`.
|
|
1503
|
-
*/
|
|
1504
|
-
const OUTPUT_COLOR_SPACE_FRAGMENT = "#include <colorspace_fragment>";
|
|
1505
|
-
//#endregion
|
|
1506
2047
|
//#region src/edge-rendering.ts
|
|
1507
2048
|
const vertexShader$2 = `
|
|
1508
2049
|
attribute float instanceKind;
|
|
@@ -2756,6 +3297,8 @@ function prefersReducedMotion() {
|
|
|
2756
3297
|
function disposeMaterial(material) {
|
|
2757
3298
|
for (const item of Array.isArray(material) ? material : [material]) item.dispose();
|
|
2758
3299
|
}
|
|
3300
|
+
/** One warning per page when a context cannot run depth of field; diagnostics report it per instance. */
|
|
3301
|
+
let warnedDepthOfFieldUnsupported = false;
|
|
2759
3302
|
/** Canvas input that cancels a running `flyTo`, so people take over the camera at once. */
|
|
2760
3303
|
const FLIGHT_CANCEL_EVENTS = [
|
|
2761
3304
|
"keydown",
|
|
@@ -2836,12 +3379,21 @@ var Graphraum = class {
|
|
|
2836
3379
|
/** True only during the orbit's own `controls.update(dt)`; see `handleViewChange`. */
|
|
2837
3380
|
orbitUpdating = false;
|
|
2838
3381
|
destroyed = false;
|
|
3382
|
+
canvasWidth = 0;
|
|
3383
|
+
canvasHeight = 0;
|
|
2839
3384
|
cpuFrameMilliseconds = 0;
|
|
2840
3385
|
gpuFrameMilliseconds = null;
|
|
2841
3386
|
gpuTimer = null;
|
|
2842
3387
|
viewListeners = /* @__PURE__ */ new Set();
|
|
2843
3388
|
focusListeners = /* @__PURE__ */ new Set();
|
|
2844
3389
|
lastHoveredNodeId = null;
|
|
3390
|
+
antialias;
|
|
3391
|
+
depthOfField = DEPTH_OF_FIELD_DEFAULTS;
|
|
3392
|
+
depthOfFieldFocus = new DepthOfFieldFocus();
|
|
3393
|
+
/** Strength actually applied: fades toward `strength` while enabled and toward 0 when disabled. */
|
|
3394
|
+
depthOfFieldStrength = new Tween(0);
|
|
3395
|
+
depthOfFieldPass = null;
|
|
3396
|
+
depthOfFieldStatus = "off";
|
|
2845
3397
|
constructor(container, options = {}) {
|
|
2846
3398
|
this.container = container;
|
|
2847
3399
|
this.mode = options.mode ?? "2d";
|
|
@@ -2854,8 +3406,13 @@ var Graphraum = class {
|
|
|
2854
3406
|
if (!Number.isSafeInteger(this.maxVisibleNodes) || this.maxVisibleNodes < 1) throw new Error("Maximum visible nodes must be a positive integer.");
|
|
2855
3407
|
if (!Number.isFinite(this.viewportOverscan) || this.viewportOverscan < 0) throw new Error("Viewport overscan must be a non-negative finite number.");
|
|
2856
3408
|
this.theme = resolveGraphraumTheme(options.theme);
|
|
3409
|
+
if (options.depthOfField !== void 0) {
|
|
3410
|
+
this.depthOfField = resolveDepthOfFieldOptions(options.depthOfField);
|
|
3411
|
+
this.depthOfFieldStrength.retarget(this.depthOfFieldTargetStrength(), 0, resolveEasing("linear"));
|
|
3412
|
+
}
|
|
3413
|
+
this.antialias = options.antialias ?? false;
|
|
2857
3414
|
this.renderer = new WebGLRenderer({
|
|
2858
|
-
antialias:
|
|
3415
|
+
antialias: this.antialias,
|
|
2859
3416
|
alpha: true
|
|
2860
3417
|
});
|
|
2861
3418
|
this.applyBackground();
|
|
@@ -2882,8 +3439,11 @@ var Graphraum = class {
|
|
|
2882
3439
|
this.resizeObserver.observe(container);
|
|
2883
3440
|
this.resize();
|
|
2884
3441
|
}
|
|
2885
|
-
|
|
2886
|
-
|
|
3442
|
+
/** Replaces the whole graph. Fits the camera unless `options.fit` is `false`. */
|
|
3443
|
+
setData(data, options = {}) {
|
|
3444
|
+
const { fit = true } = options;
|
|
3445
|
+
if (typeof fit !== "boolean") throw new Error("setData option fit must be a boolean.");
|
|
3446
|
+
this.replaceData(data, fit);
|
|
2887
3447
|
}
|
|
2888
3448
|
/** Merge a topology patch without fitting or resetting the current camera. */
|
|
2889
3449
|
applyDataPatch(patch) {
|
|
@@ -3309,6 +3869,10 @@ var Graphraum = class {
|
|
|
3309
3869
|
const changedNodeIds = new Set([...currentSelection, ...nextSelection].filter((nodeId) => currentSelection.has(nodeId) !== nextSelection.has(nodeId)));
|
|
3310
3870
|
if (changedNodeIds.size === 0) return;
|
|
3311
3871
|
this.setNodeStateIds(state, nextSelection);
|
|
3872
|
+
if (state === "selected" && this.depthOfField.enabled) {
|
|
3873
|
+
const { durationMs, easing } = this.depthOfField.transition;
|
|
3874
|
+
this.depthOfFieldFocus.retarget(durationMs, resolveEasing(easing));
|
|
3875
|
+
}
|
|
3312
3876
|
this.applySelectionColors(changedNodeIds);
|
|
3313
3877
|
this.requestRender();
|
|
3314
3878
|
if (state === "selected" || state === "hovered") this.notifyFocusChange();
|
|
@@ -3373,6 +3937,33 @@ var Graphraum = class {
|
|
|
3373
3937
|
if (this.autoOrbit && this.autoOrbit.status !== "off") this.autoOrbit.setResumeAfterMs(resumeAfterMs);
|
|
3374
3938
|
else this.autoOrbit = new AutoOrbitLoop(this.renderer.domElement, this.controls, resumeAfterMs, this.stepAutoOrbit);
|
|
3375
3939
|
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Updates 3D depth of field; omitted fields keep their current values. Enabling, disabling and a
|
|
3942
|
+
* new `strength` fade the blur over `transition` unless `update.animate` is `false`; a change
|
|
3943
|
+
* during a fade continues from the blur shown last. With `focus: "selection"` the focal plane
|
|
3944
|
+
* follows the mean position of the selected nodes, re-measured from the camera every frame, and
|
|
3945
|
+
* moves over `transition` when the selection changes; with nothing selected the blur fades out.
|
|
3946
|
+
* `prefers-reduced-motion: reduce` jumps. Resolves `true` when the blur reaches the new strength
|
|
3947
|
+
* and `false` when a later call or `destroy()` interrupts the fade. While it blurs each frame adds
|
|
3948
|
+
* three draw calls; disabled, or with nothing to blur, it costs nothing. A no-op in 2D. Picking
|
|
3949
|
+
* and DOM overlays are unaffected. Throws for invalid values.
|
|
3950
|
+
*/
|
|
3951
|
+
setDepthOfField(options, update = {}) {
|
|
3952
|
+
const animate = update.animate ?? true;
|
|
3953
|
+
if (typeof animate !== "boolean") throw new Error(`setDepthOfField option animate must be a boolean; received ${String(animate)}.`);
|
|
3954
|
+
const previous = this.depthOfField;
|
|
3955
|
+
this.depthOfField = resolveDepthOfFieldOptions(options, previous);
|
|
3956
|
+
if (this.destroyed) return Promise.resolve(false);
|
|
3957
|
+
const { durationMs, easing } = this.depthOfField.transition;
|
|
3958
|
+
if (this.depthOfField.enabled && !previous.enabled) this.depthOfFieldFocus.retarget(0, resolveEasing(easing));
|
|
3959
|
+
const fadeMs = animate && !prefersReducedMotion() ? durationMs : 0;
|
|
3960
|
+
const done = this.depthOfFieldStrength.retarget(this.depthOfFieldTargetStrength(), fadeMs, resolveEasing(easing));
|
|
3961
|
+
this.requestRender();
|
|
3962
|
+
return done;
|
|
3963
|
+
}
|
|
3964
|
+
depthOfFieldTargetStrength() {
|
|
3965
|
+
return this.depthOfField.enabled ? this.depthOfField.strength : 0;
|
|
3966
|
+
}
|
|
3376
3967
|
getNodePresentation(nodeId) {
|
|
3377
3968
|
return this.nodePresentations.get(nodeId) ?? null;
|
|
3378
3969
|
}
|
|
@@ -3467,6 +4058,7 @@ var Graphraum = class {
|
|
|
3467
4058
|
aggregatedNodeClusters: this.densityLodActive ? this.visibleNodeCount : 0,
|
|
3468
4059
|
autoOrbit: this.autoOrbit?.status ?? "off",
|
|
3469
4060
|
cpuFrameMilliseconds: this.cpuFrameMilliseconds,
|
|
4061
|
+
depthOfField: this.depthOfFieldStatus,
|
|
3470
4062
|
gpuFrameMilliseconds: this.gpuFrameMilliseconds,
|
|
3471
4063
|
gpuDrawCalls: this.renderer.info.render.calls,
|
|
3472
4064
|
gpuGeometries: this.renderer.info.memory.geometries,
|
|
@@ -3575,6 +4167,71 @@ var Graphraum = class {
|
|
|
3575
4167
|
this.requestRender();
|
|
3576
4168
|
});
|
|
3577
4169
|
}
|
|
4170
|
+
/**
|
|
4171
|
+
* The camera pose as plain JSON. 2D reports the centre and the world height the canvas shows; 3D
|
|
4172
|
+
* reports the camera position and the point it orbits. Pass it to `setCameraState()` to return
|
|
4173
|
+
* to the same view, for example after reloading a saved one.
|
|
4174
|
+
*/
|
|
4175
|
+
getCameraState() {
|
|
4176
|
+
const camera = this.camera;
|
|
4177
|
+
if (camera instanceof OrthographicCamera) return {
|
|
4178
|
+
center: {
|
|
4179
|
+
x: camera.position.x,
|
|
4180
|
+
y: camera.position.y
|
|
4181
|
+
},
|
|
4182
|
+
mode: "2d",
|
|
4183
|
+
visibleHeight: (camera.top - camera.bottom) / camera.zoom
|
|
4184
|
+
};
|
|
4185
|
+
const { target } = this.controls;
|
|
4186
|
+
return {
|
|
4187
|
+
mode: "3d",
|
|
4188
|
+
position: {
|
|
4189
|
+
x: camera.position.x,
|
|
4190
|
+
y: camera.position.y,
|
|
4191
|
+
z: camera.position.z
|
|
4192
|
+
},
|
|
4193
|
+
target: {
|
|
4194
|
+
x: target.x,
|
|
4195
|
+
y: target.y,
|
|
4196
|
+
z: target.z
|
|
4197
|
+
}
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
/**
|
|
4201
|
+
* Puts the camera on a pose from `getCameraState()` at once, without fitting or animating, and
|
|
4202
|
+
* cancels a running `flyTo`. Throws for a state of the other mode (switch with `setMode()` first)
|
|
4203
|
+
* or with a non-finite number. 2D keeps the saved height and takes the width from the canvas, so a
|
|
4204
|
+
* view saved on one canvas size fits another. 3D derives the clip planes from the pose so the whole
|
|
4205
|
+
* graph stays in view. Apply it after the canvas has its size: a 2D resize to a new size refits.
|
|
4206
|
+
*/
|
|
4207
|
+
setCameraState(state) {
|
|
4208
|
+
const next = validateCameraState(state, this.mode);
|
|
4209
|
+
this.cancelFlight();
|
|
4210
|
+
const camera = this.camera;
|
|
4211
|
+
const width = Math.max(this.container.clientWidth, 1);
|
|
4212
|
+
const height = Math.max(this.container.clientHeight, 1);
|
|
4213
|
+
if (next.mode === "2d" && camera instanceof OrthographicCamera) {
|
|
4214
|
+
setOrthographicFrustum(camera, width / height, next.visibleHeight);
|
|
4215
|
+
camera.position.set(next.center.x, next.center.y, camera.position.z);
|
|
4216
|
+
this.controls.target.set(next.center.x, next.center.y, this.controls.target.z);
|
|
4217
|
+
camera.lookAt(this.controls.target);
|
|
4218
|
+
camera.updateProjectionMatrix();
|
|
4219
|
+
this.controls.update();
|
|
4220
|
+
this.materializeViewport();
|
|
4221
|
+
} else if (next.mode === "3d" && camera instanceof PerspectiveCamera) {
|
|
4222
|
+
const target = new Vector3(next.target.x, next.target.y, next.target.z);
|
|
4223
|
+
camera.position.set(next.position.x, next.position.y, next.position.z);
|
|
4224
|
+
this.controls.target.copy(target);
|
|
4225
|
+
camera.lookAt(target);
|
|
4226
|
+
applyPerspectiveContainerAspect(camera, width, height);
|
|
4227
|
+
const clip = perspectiveClipRange(camera.position.distanceTo(target), this.graphRadiusAround(target));
|
|
4228
|
+
camera.near = clip.near;
|
|
4229
|
+
camera.far = clip.far;
|
|
4230
|
+
camera.updateProjectionMatrix();
|
|
4231
|
+
this.controls.update();
|
|
4232
|
+
}
|
|
4233
|
+
this.requestRender();
|
|
4234
|
+
}
|
|
3578
4235
|
/** Nodes whose id is in `nodeIds`, in graph order; every node when `nodeIds` is omitted. */
|
|
3579
4236
|
nodesWithIds(nodeIds) {
|
|
3580
4237
|
if (!nodeIds) return this.data.nodes;
|
|
@@ -3722,11 +4379,14 @@ var Graphraum = class {
|
|
|
3722
4379
|
resize() {
|
|
3723
4380
|
const width = Math.max(this.container.clientWidth, 1);
|
|
3724
4381
|
const height = Math.max(this.container.clientHeight, 1);
|
|
4382
|
+
const sizeChanged = this.canvasWidth !== width || this.canvasHeight !== height;
|
|
4383
|
+
this.canvasWidth = width;
|
|
4384
|
+
this.canvasHeight = height;
|
|
3725
4385
|
this.renderer.setSize(width, height, false);
|
|
3726
4386
|
if (this.camera instanceof PerspectiveCamera) {
|
|
3727
4387
|
applyPerspectiveContainerAspect(this.camera, width, height);
|
|
3728
4388
|
this.camera.updateProjectionMatrix();
|
|
3729
|
-
} else if (this.camera instanceof OrthographicCamera) {
|
|
4389
|
+
} else if (this.camera instanceof OrthographicCamera && sizeChanged) {
|
|
3730
4390
|
this.fitView();
|
|
3731
4391
|
return;
|
|
3732
4392
|
}
|
|
@@ -3746,6 +4406,8 @@ var Graphraum = class {
|
|
|
3746
4406
|
if (this.frameRequest !== null) cancelAnimationFrame(this.frameRequest);
|
|
3747
4407
|
if (this.gpuTimer) for (const query of this.gpuTimer.pending) this.gpuTimer.context.deleteQuery(query);
|
|
3748
4408
|
this.disposeBackgroundTexture();
|
|
4409
|
+
this.depthOfFieldStrength.cancel();
|
|
4410
|
+
this.disposeDepthOfFieldPass();
|
|
3749
4411
|
this.disposeGraphObjects();
|
|
3750
4412
|
this.renderer.dispose();
|
|
3751
4413
|
this.renderer.domElement.remove();
|
|
@@ -3771,7 +4433,16 @@ var Graphraum = class {
|
|
|
3771
4433
|
const query = timer?.context.createQuery() ?? null;
|
|
3772
4434
|
if (timer && query) timer.context.beginQuery(timer.extension.TIME_ELAPSED_EXT, query);
|
|
3773
4435
|
const startedAt = performance.now();
|
|
3774
|
-
this.
|
|
4436
|
+
const depthOfField = this.prepareDepthOfField(startedAt);
|
|
4437
|
+
if (depthOfField && this.depthOfFieldPass && this.camera instanceof PerspectiveCamera) {
|
|
4438
|
+
this.renderer.info.autoReset = false;
|
|
4439
|
+
this.renderer.info.reset();
|
|
4440
|
+
try {
|
|
4441
|
+
this.depthOfFieldPass.render(this.renderer, this.scene, this.camera, this.edgeMesh, depthOfField);
|
|
4442
|
+
} finally {
|
|
4443
|
+
this.renderer.info.autoReset = true;
|
|
4444
|
+
}
|
|
4445
|
+
} else this.renderer.render(this.scene, this.camera);
|
|
3775
4446
|
this.cpuFrameMilliseconds = performance.now() - startedAt;
|
|
3776
4447
|
if (timer && query) {
|
|
3777
4448
|
timer.context.endQuery(timer.extension.TIME_ELAPSED_EXT);
|
|
@@ -3779,6 +4450,81 @@ var Graphraum = class {
|
|
|
3779
4450
|
}
|
|
3780
4451
|
for (const listener of this.viewListeners) listener();
|
|
3781
4452
|
}
|
|
4453
|
+
/**
|
|
4454
|
+
* Decides whether this frame blurs and records why in the diagnostics status. Returns null to
|
|
4455
|
+
* render directly. Requests another frame while a fade or focus move runs; both end on their own.
|
|
4456
|
+
*/
|
|
4457
|
+
prepareDepthOfField(nowMs) {
|
|
4458
|
+
const options = this.depthOfField;
|
|
4459
|
+
const strengthTween = this.depthOfFieldStrength;
|
|
4460
|
+
if (!options.enabled && strengthTween.settled) {
|
|
4461
|
+
this.depthOfFieldStatus = "off";
|
|
4462
|
+
this.disposeDepthOfFieldPass();
|
|
4463
|
+
return null;
|
|
4464
|
+
}
|
|
4465
|
+
if (!(this.camera instanceof PerspectiveCamera)) {
|
|
4466
|
+
strengthTween.jump();
|
|
4467
|
+
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4468
|
+
this.disposeDepthOfFieldPass();
|
|
4469
|
+
return null;
|
|
4470
|
+
}
|
|
4471
|
+
if (!DepthOfFieldPass.isSupported(this.renderer)) {
|
|
4472
|
+
strengthTween.jump();
|
|
4473
|
+
this.depthOfFieldStatus = options.enabled ? "unsupported" : "off";
|
|
4474
|
+
if (options.enabled && !warnedDepthOfFieldUnsupported) {
|
|
4475
|
+
warnedDepthOfFieldUnsupported = true;
|
|
4476
|
+
console.warn("graphraum: depth of field needs a WebGL 2 context and is turned off for this canvas. getDiagnostics().depthOfField reports \"unsupported\".");
|
|
4477
|
+
}
|
|
4478
|
+
return null;
|
|
4479
|
+
}
|
|
4480
|
+
const strength = strengthTween.sample(nowMs);
|
|
4481
|
+
if (!strengthTween.settled) this.requestRender();
|
|
4482
|
+
this.camera.updateMatrixWorld();
|
|
4483
|
+
let focusDistance = typeof options.focus === "number" ? options.focus : null;
|
|
4484
|
+
let amount = 1;
|
|
4485
|
+
if (options.focus === "selection") {
|
|
4486
|
+
const focus = this.depthOfFieldFocus.evaluate(nowMs, this.selectionFocusPoint(), prefersReducedMotion());
|
|
4487
|
+
if (!focus.settled) this.requestRender();
|
|
4488
|
+
amount = focus.amount;
|
|
4489
|
+
if (focus.point) focusDistance = viewDistance(focus.point, this.camera.matrixWorldInverse.elements);
|
|
4490
|
+
}
|
|
4491
|
+
if (focusDistance === null) {
|
|
4492
|
+
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4493
|
+
return null;
|
|
4494
|
+
}
|
|
4495
|
+
focusDistance = Math.max(focusDistance, this.camera.near);
|
|
4496
|
+
const pixelRatio = this.renderer.getPixelRatio();
|
|
4497
|
+
const aperturePixels = strength * amount * distanceFade(focusDistance, options.nearDistance, options.farDistance) * 24 * pixelRatio;
|
|
4498
|
+
const maxBlurPixels = options.maxBlur * pixelRatio;
|
|
4499
|
+
if (aperturePixels < .25 || maxBlurPixels < .25) {
|
|
4500
|
+
this.depthOfFieldStatus = options.enabled ? "inactive" : "off";
|
|
4501
|
+
return null;
|
|
4502
|
+
}
|
|
4503
|
+
this.depthOfFieldPass ??= new DepthOfFieldPass(this.antialias);
|
|
4504
|
+
this.depthOfFieldStatus = "active";
|
|
4505
|
+
return {
|
|
4506
|
+
aperturePixels,
|
|
4507
|
+
focusDistance,
|
|
4508
|
+
maxBlurPixels,
|
|
4509
|
+
minBlurPixels: options.minBlur * pixelRatio
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
/** Mean world position of the selected nodes, read every frame so a running layout keeps them in focus. */
|
|
4513
|
+
selectionFocusPoint() {
|
|
4514
|
+
return meanPoint([...this.selectedNodeIds].flatMap((id) => {
|
|
4515
|
+
const index = this.nodeIndices.get(id);
|
|
4516
|
+
const node = index === void 0 ? void 0 : this.data.nodes[index];
|
|
4517
|
+
return node ? [{
|
|
4518
|
+
x: node.position.x,
|
|
4519
|
+
y: node.position.y,
|
|
4520
|
+
z: node.position.z ?? 0
|
|
4521
|
+
}] : [];
|
|
4522
|
+
}));
|
|
4523
|
+
}
|
|
4524
|
+
disposeDepthOfFieldPass() {
|
|
4525
|
+
this.depthOfFieldPass?.dispose();
|
|
4526
|
+
this.depthOfFieldPass = null;
|
|
4527
|
+
}
|
|
3782
4528
|
stopAutoOrbit() {
|
|
3783
4529
|
this.autoOrbit?.dispose();
|
|
3784
4530
|
this.autoOrbit = null;
|