@fusion.dev/helix-graph 0.125.0 → 0.127.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.
Files changed (41) hide show
  1. package/README.md +84 -35
  2. package/dist/connect-graph/arrange.d.ts +4 -2
  3. package/dist/connect-graph/arrange.js +5 -22
  4. package/dist/connect-graph/connect-graph.d.ts +18 -6
  5. package/dist/connect-graph/connect-graph.js +76 -32
  6. package/dist/connect-graph/diff.d.ts +7 -0
  7. package/dist/connect-graph/looks.d.ts +8 -0
  8. package/dist/connect-graph/looks.js +6 -3
  9. package/dist/connect-graph/seed.d.ts +9 -0
  10. package/dist/connect-graph/seed.js +21 -15
  11. package/dist/connect-graph/stencil.d.ts +11 -1
  12. package/dist/connect-graph/stencil.js +109 -11
  13. package/dist/cosmos/cosmos-driver.d.ts +5 -4
  14. package/dist/cosmos/cosmos-driver.js +122 -39
  15. package/dist/index.d.ts +4 -3
  16. package/dist/index.js +2 -2
  17. package/dist/model/index.d.ts +1 -0
  18. package/dist/model/index.js +1 -0
  19. package/dist/model/model.d.ts +16 -0
  20. package/dist/model/model.js +50 -0
  21. package/dist/overlay/overlay.d.ts +14 -8
  22. package/dist/overlay/overlay.js +64 -62
  23. package/dist/shapes/flat.d.ts +3 -2
  24. package/dist/shapes/flat.js +59 -6
  25. package/dist/shapes/index.d.ts +3 -4
  26. package/dist/shapes/index.js +3 -4
  27. package/dist/{connect-graph → shapes}/shape-field.d.ts +12 -3
  28. package/dist/{connect-graph → shapes}/shape-field.js +4 -2
  29. package/dist/shapes/solids.d.ts +3 -3
  30. package/dist/shapes/solids.js +25 -9
  31. package/dist/{three → shapes}/surface.d.ts +1 -1
  32. package/dist/shapes/triangles.d.ts +12 -3
  33. package/dist/shapes/triangles.js +83 -2
  34. package/dist/sigma/sigma-driver.d.ts +33 -17
  35. package/dist/sigma/sigma-driver.js +69 -17
  36. package/dist/three/three-driver.d.ts +12 -3
  37. package/dist/three/three-driver.js +376 -201
  38. package/package.json +9 -5
  39. package/dist/shapes/model.d.ts +0 -12
  40. package/dist/shapes/model.js +0 -31
  41. /package/dist/{three → shapes}/surface.js +0 -0
@@ -1,4 +1,4 @@
1
- import type { ShapeField } from '../connect-graph/shape-field.js';
1
+ import type { ShapeField } from './shape-field.js';
2
2
  /**
3
3
  * The surface of a solid field where its distance is `at`, as triangles:
4
4
  * corners three numbers each in the field's box, and triangles three corners
@@ -1,7 +1,16 @@
1
- import { type ShapeField } from '../connect-graph/shape-field.js';
1
+ import { type ShapeField } from './shape-field.js';
2
2
  /** How a solid is read from triangles. */
3
3
  export interface TriangleOptions {
4
- /** How many cells the field has along the solid's longest side. 96 by default. */
4
+ /**
5
+ * The named parts the triangles make: each part's name, and which part
6
+ * each triangle is in, by its place in `names`. Gives the field
7
+ * `regionOf`, the part a point is in: the part of the triangle nearest it.
8
+ */
9
+ regions?: {
10
+ names: readonly string[];
11
+ ofTriangle: ArrayLike<number>;
12
+ } | undefined;
13
+ /** How many cells the field has along the solid's longest side. 144 by default. */
5
14
  resolution?: number | undefined;
6
15
  }
7
16
  /**
@@ -17,4 +26,4 @@ export interface TriangleOptions {
17
26
  * minutes. A surface with gaps is read as closed where it can be, and a gap
18
27
  * shows as a column filled or emptied through.
19
28
  */
20
- export declare const shapeFromTriangles: (positions: ArrayLike<number>, index?: ArrayLike<number> | null | undefined, { resolution }?: TriangleOptions) => ShapeField;
29
+ export declare const shapeFromTriangles: (positions: ArrayLike<number>, index?: ArrayLike<number> | null | undefined, { regions, resolution }?: TriangleOptions) => ShapeField;
@@ -1,6 +1,73 @@
1
- import { fieldFromCells, SOLID } from '../connect-graph/shape-field.js';
1
+ import { fieldFromCells, SOLID } from './shape-field.js';
2
2
  const NUDGE = 1e-6 * Math.PI;
3
- export const shapeFromTriangles = (positions, index, { resolution = SOLID } = {}) => {
3
+ const NEAREST = 40;
4
+ const nearestPart = (middles, parts, names, box) => {
5
+ const size = Math.max(box.width, box.height, box.depth) / NEAREST || 1;
6
+ const nx = Math.ceil(box.width / size) + 1;
7
+ const ny = Math.ceil(box.height / size) + 1;
8
+ const nz = Math.ceil(box.depth / size) + 1;
9
+ const clamp = (value, most) => Math.min(most - 1, Math.max(0, value));
10
+ const cellOf = (x, y, z) => clamp(Math.floor(x / size), nx) +
11
+ nx * (clamp(Math.floor(y / size), ny) + ny * clamp(Math.floor(z / size), nz));
12
+ const count = middles.length / 3;
13
+ const start = new Int32Array(nx * ny * nz + 1);
14
+ const cellAt = new Int32Array(count);
15
+ for (let at = 0; at < count; at++) {
16
+ const cell = cellOf(middles[at * 3], middles[at * 3 + 1], middles[at * 3 + 2]);
17
+ cellAt[at] = cell;
18
+ start[cell + 1] = start[cell + 1] + 1;
19
+ }
20
+ for (let cell = 0; cell < nx * ny * nz; cell++) {
21
+ start[cell + 1] = start[cell + 1] + start[cell];
22
+ }
23
+ const filled = start.slice(0, -1);
24
+ const order = new Int32Array(count);
25
+ for (let at = 0; at < count; at++) {
26
+ const cell = cellAt[at];
27
+ order[filled[cell]] = at;
28
+ filled[cell] = filled[cell] + 1;
29
+ }
30
+ const furthest = Math.max(nx, ny, nz);
31
+ return (x, y, z = box.depth / 2) => {
32
+ const cx = clamp(Math.floor(x / size), nx);
33
+ const cy = clamp(Math.floor(y / size), ny);
34
+ const cz = clamp(Math.floor(z / size), nz);
35
+ let best = -1;
36
+ let nearest = Number.POSITIVE_INFINITY;
37
+ const search = (ix, iy, iz) => {
38
+ if (ix < 0 || iy < 0 || iz < 0 || ix >= nx || iy >= ny || iz >= nz) {
39
+ return;
40
+ }
41
+ const cell = ix + nx * (iy + ny * iz);
42
+ for (let at = start[cell]; at < start[cell + 1]; at++) {
43
+ const one = order[at];
44
+ const far = (middles[one * 3] - x) ** 2 +
45
+ (middles[one * 3 + 1] - y) ** 2 +
46
+ (middles[one * 3 + 2] - z) ** 2;
47
+ if (far < nearest) {
48
+ nearest = far;
49
+ best = one;
50
+ }
51
+ }
52
+ };
53
+ for (let ring = 0; ring <= furthest; ring++) {
54
+ for (let dz = -ring; dz <= ring; dz++) {
55
+ const face = Math.abs(dz) === ring;
56
+ for (let dy = -ring; dy <= ring; dy++) {
57
+ const edge = face || Math.abs(dy) === ring;
58
+ for (let dx = -ring; dx <= ring; dx += edge || ring === 0 ? 1 : 2 * ring) {
59
+ search(cx + dx, cy + dy, cz + dz);
60
+ }
61
+ }
62
+ }
63
+ if (best >= 0 && Math.sqrt(nearest) <= ring * size) {
64
+ break;
65
+ }
66
+ }
67
+ return best < 0 ? undefined : names[parts[best]];
68
+ };
69
+ };
70
+ export const shapeFromTriangles = (positions, index, { regions, resolution = SOLID } = {}) => {
4
71
  const corners = Math.floor(positions.length / 3);
5
72
  const low = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
6
73
  const high = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
@@ -84,9 +151,23 @@ export const shapeFromTriangles = (positions, index, { resolution = SOLID } = {}
84
151
  height: ny * cell,
85
152
  width: nx * cell,
86
153
  });
154
+ let regionOf;
155
+ if (regions && regions.names.length > 0) {
156
+ const middles = new Float32Array(triangles * 3);
157
+ for (let triangle = 0; triangle < triangles; triangle++) {
158
+ for (let which = 0; which < 3; which++) {
159
+ const at = corner(triangle, which);
160
+ middles[triangle * 3] = middles[triangle * 3] + (at[0] * cell) / 3;
161
+ middles[triangle * 3 + 1] = middles[triangle * 3 + 1] + (at[1] * cell) / 3;
162
+ middles[triangle * 3 + 2] = middles[triangle * 3 + 2] + (at[2] * cell) / 3;
163
+ }
164
+ }
165
+ regionOf = nearestPart(middles, regions.ofTriangle, regions.names, field);
166
+ }
87
167
  return {
88
168
  ...field,
89
169
  distance: field.distance,
90
170
  mesh: { corners: meshCorners, triangles: meshTriangles },
171
+ ...(regionOf ? { regionOf } : {}),
91
172
  };
92
173
  };
@@ -1,12 +1,16 @@
1
1
  import type { GraphDriver } from '../connect-graph/connect-graph.js';
2
2
  import type { NodeLook } from '../connect-graph/looks.js';
3
- /** A graphology graph, as far as the driver calls it. */
3
+ /**
4
+ * A graphology graph, as far as the driver calls it. Each member is
5
+ * graphology's own, as its documentation describes it.
6
+ */
4
7
  export interface SigmaGraph {
5
8
  addEdge(source: string, target: string, attributes?: Record<string, unknown>): unknown;
6
9
  addNode(node: string, attributes?: Record<string, unknown>): unknown;
7
10
  clear(): void;
8
11
  dropEdge(source: string, target: string): void;
9
12
  dropNode(node: string): void;
13
+ edges(node: string): string[];
10
14
  forEachNode(callback: (node: string, attributes: Record<string, unknown>) => void): void;
11
15
  getNodeAttributes(node: string): Record<string, unknown>;
12
16
  hasEdge(source: string, target: string): boolean;
@@ -15,7 +19,7 @@ export interface SigmaGraph {
15
19
  readonly order: number;
16
20
  setNodeAttribute(node: string, name: string, value: unknown): unknown;
17
21
  }
18
- /** Sigma's camera, as far as the driver calls it. */
22
+ /** Sigma's camera, as far as the driver calls it. Each member is Sigma's own. */
19
23
  export interface SigmaCamera {
20
24
  animate(state: {
21
25
  ratio?: number;
@@ -36,15 +40,18 @@ export interface SigmaCamera {
36
40
  y?: number;
37
41
  }): unknown;
38
42
  }
39
- /** A Sigma renderer, as far as the driver calls it. */
40
- export interface SigmaRenderer {
43
+ /**
44
+ * A Sigma renderer, as far as the driver calls it, over a graph of type `G`.
45
+ * Each member is Sigma's own, as its documentation describes it.
46
+ */
47
+ export interface SigmaRenderer<G extends SigmaGraph = SigmaGraph> {
41
48
  getCamera(): SigmaCamera;
42
49
  getContainer(): HTMLElement;
43
50
  getDimensions(): {
44
51
  height: number;
45
52
  width: number;
46
53
  };
47
- getGraph(): SigmaGraph;
54
+ getGraph(): G;
48
55
  framedGraphToViewport(point: {
49
56
  x: number;
50
57
  y: number;
@@ -61,16 +68,21 @@ export interface SigmaRenderer {
61
68
  off(type: 'clickNode' | 'enterNode' | 'leaveNode', listener: (payload: {
62
69
  node: string;
63
70
  }) => void): unknown;
64
- off(type: 'afterRender' | 'beforeRender', listener: () => void): unknown;
71
+ off(type: 'afterRender' | 'beforeRender' | 'clickStage', listener: () => void): unknown;
65
72
  on(type: 'clickNode' | 'enterNode' | 'leaveNode', listener: (payload: {
66
73
  node: string;
67
74
  }) => void): unknown;
68
- on(type: 'afterRender' | 'beforeRender', listener: () => void): unknown;
69
- getSetting(name: 'nodeProgramClasses'): Record<string, unknown>;
70
- refresh(): unknown;
75
+ on(type: 'afterRender' | 'beforeRender' | 'clickStage', listener: () => void): unknown;
76
+ getSetting(name: SigmaSetting): unknown;
77
+ refresh(options?: {
78
+ partialGraph?: {
79
+ edges?: string[];
80
+ nodes?: string[];
81
+ };
82
+ }): unknown;
71
83
  resize(force?: boolean): unknown;
72
84
  scaleSize(size?: number): number;
73
- setSetting(name: 'defaultDrawNodeHover' | 'edgeReducer' | 'labelColor' | 'labelFont' | 'defaultDrawNodeLabel' | 'labelGridCellSize' | 'labelRenderedSizeThreshold' | 'labelSize' | 'minEdgeThickness' | 'nodeProgramClasses' | 'nodeReducer', value: unknown): unknown;
85
+ setSetting(name: SigmaSetting, value: unknown): unknown;
74
86
  viewportToFramedGraph(point: {
75
87
  x: number;
76
88
  y: number;
@@ -79,14 +91,16 @@ export interface SigmaRenderer {
79
91
  y: number;
80
92
  };
81
93
  }
94
+ /** The settings the driver sets, and puts back as they were when it stops. */
95
+ type SigmaSetting = 'defaultDrawNodeHover' | 'defaultDrawNodeLabel' | 'edgeReducer' | 'enableCameraPanning' | 'enableCameraZooming' | 'labelColor' | 'labelFont' | 'labelGridCellSize' | 'labelRenderedSizeThreshold' | 'labelSize' | 'minEdgeThickness' | 'nodeProgramClasses' | 'nodeReducer';
82
96
  /** A layout that runs over a graphology graph until it is stopped, as ForceAtlas2's worker does. */
83
97
  export interface SigmaLayout {
84
98
  kill(): void;
85
99
  start(): void;
86
100
  stop(): void;
87
101
  }
88
- /** How the Sigma driver behaves where the page does not say. */
89
- export interface SigmaDriverOptions {
102
+ /** How the Sigma driver behaves where the page does not say, over a graph of type `G`. */
103
+ export interface SigmaDriverOptions<G extends SigmaGraph = SigmaGraph> {
90
104
  /**
91
105
  * Makes a layout for the graph, which the driver runs where notes have no
92
106
  * position and stops once they have settled. ForceAtlas2's worker is the
@@ -103,18 +117,18 @@ export interface SigmaDriverOptions {
103
117
  *
104
118
  * Without one, notes stay where the connector started them.
105
119
  */
106
- layout?: ((graph: SigmaGraph) => SigmaLayout) | undefined;
120
+ layout?: ((graph: G) => SigmaLayout) | undefined;
107
121
  /**
108
122
  * How heavily a note is drawn where it does not say: `solid`, filled, by
109
123
  * default; `outlined`, a ring; `subtle`, a tinted fill with a ring.
110
124
  */
111
125
  emphasis?: NodeLook['emphasis'];
112
126
  /**
113
- * What a note is drawn as made of where it does not say: flat by default,
127
+ * What a note is drawn as made of where it does not say: `flat` by default,
114
128
  * `glass`, a shaded sphere with a highlight and a lit rim, or `frosted`, a
115
129
  * milky body with a soft fall of light.
116
130
  */
117
- material?: NodeLook['material'];
131
+ material?: NodeLook['material'] | 'flat';
118
132
  /** A note's shape where it does not say: `circle` by default, `square`, `diamond` or `ring`. */
119
133
  shape?: NodeLook['shape'];
120
134
  /**
@@ -142,6 +156,8 @@ export interface SigmaDriverOptions {
142
156
  *
143
157
  * **Neither Sigma nor graphology is a dependency.** The driver types the
144
158
  * renderer by the calls it makes on it, so a page brings whichever version it
145
- * has, and a spec holds those shapes to the real classes.
159
+ * has, and a spec holds those shapes to the real classes. The renderer is the
160
+ * page's: after `disconnect()`, `sigma.kill()` frees it.
146
161
  */
147
- export declare const sigmaDriver: (sigma: SigmaRenderer, options?: SigmaDriverOptions) => GraphDriver;
162
+ export declare const sigmaDriver: <G extends SigmaGraph>(sigma: SigmaRenderer<G>, options?: SigmaDriverOptions<G>) => GraphDriver;
163
+ export {};
@@ -1,5 +1,20 @@
1
1
  import { ease, gesturesHeld, groupsOver, signalsOver } from '../overlay/overlay.js';
2
2
  import { EMPHASES, HelixNodeProgram, MATERIALS, SHAPES } from './node-program.js';
3
+ const SETTINGS = [
4
+ 'defaultDrawNodeHover',
5
+ 'defaultDrawNodeLabel',
6
+ 'edgeReducer',
7
+ 'enableCameraPanning',
8
+ 'enableCameraZooming',
9
+ 'labelColor',
10
+ 'labelFont',
11
+ 'labelGridCellSize',
12
+ 'labelRenderedSizeThreshold',
13
+ 'labelSize',
14
+ 'minEdgeThickness',
15
+ 'nodeProgramClasses',
16
+ 'nodeReducer',
17
+ ];
3
18
  const STEP = 250;
4
19
  const ZOOM = 1.5;
5
20
  const SAMPLE = 250;
@@ -65,7 +80,8 @@ export const sigmaDriver = (sigma, options = {}) => {
65
80
  const graph = sigma.getGraph();
66
81
  const container = sigma.getContainer();
67
82
  const camera = sigma.getCamera();
68
- const programs = sigma.getSetting('nodeProgramClasses');
83
+ const before = new Map(SETTINGS.map((name) => [name, sigma.getSetting(name)]));
84
+ const programs = before.get('nodeProgramClasses');
69
85
  sigma.setSetting('nodeProgramClasses', { ...programs, helix: HelixNodeProgram });
70
86
  const sized = typeof ResizeObserver === 'function'
71
87
  ? new ResizeObserver(() => {
@@ -75,13 +91,22 @@ export const sigmaDriver = (sigma, options = {}) => {
75
91
  : undefined;
76
92
  sized?.observe(container);
77
93
  let hovered;
94
+ const cursor = container.style.cursor;
95
+ const relight = (from, to) => {
96
+ const edges = [from, to].flatMap((id) => id !== undefined && graph.hasNode(id) ? graph.edges(id) : []);
97
+ sigma.refresh({ partialGraph: { edges } });
98
+ };
78
99
  const enter = ({ node }) => {
100
+ const was = hovered;
79
101
  hovered = node;
80
- sigma.refresh();
102
+ container.style.cursor = 'pointer';
103
+ relight(was, node);
81
104
  };
82
105
  const leave = () => {
106
+ const was = hovered;
83
107
  hovered = undefined;
84
- sigma.refresh();
108
+ container.style.cursor = cursor;
109
+ relight(was, undefined);
85
110
  };
86
111
  sigma.on('enterNode', enter);
87
112
  sigma.on('leaveNode', leave);
@@ -111,22 +136,31 @@ export const sigmaDriver = (sigma, options = {}) => {
111
136
  return { radius: sigma.scaleSize(data.size), x: at.x, y: at.y };
112
137
  };
113
138
  const signals = signalsOver(container, onScreen);
114
- const groups = groupsOver(container, onScreen);
139
+ const groups = groupsOver(container, onScreen, () => taken);
115
140
  sigma.on('afterRender', groups.draw);
116
- const grow = (sizes) => {
141
+ let gliding = 0;
142
+ const glide = (sizes, moves) => {
143
+ cancelAnimationFrame(gliding);
117
144
  const started = performance.now();
118
- const step = (now) => {
119
- const along = Math.min((now - started) / GROW, 1);
145
+ const step = () => {
146
+ const along = Math.min((performance.now() - started) / GROW, 1);
147
+ const eased = ease(along);
120
148
  for (const [id, size] of sizes) {
121
149
  if (graph.hasNode(id)) {
122
- graph.setNodeAttribute(id, 'size', size * ease(along));
150
+ graph.setNodeAttribute(id, 'size', size * eased);
123
151
  }
124
152
  }
125
- if (along < 1) {
126
- requestAnimationFrame(step);
153
+ for (const [id, { from, to }] of moves) {
154
+ if (graph.hasNode(id)) {
155
+ graph.mergeNodeAttributes(id, {
156
+ x: from.x + (to.x - from.x) * eased,
157
+ y: from.y + (to.y - from.y) * eased,
158
+ });
159
+ }
127
160
  }
161
+ gliding = along < 1 ? requestAnimationFrame(step) : 0;
128
162
  };
129
- requestAnimationFrame(step);
163
+ gliding = requestAnimationFrame(step);
130
164
  };
131
165
  return {
132
166
  center: (id, animate) => {
@@ -176,7 +210,7 @@ export const sigmaDriver = (sigma, options = {}) => {
176
210
  return;
177
211
  }
178
212
  const state = {
179
- ratio: Math.min(Math.max(Math.max(high.x - low.x, high.y - low.y) * 2.4, 0.12), 1),
213
+ ratio: Math.min(Math.max(Math.max(high.x - low.x, high.y - low.y) * 2.4, 0.25), 1),
180
214
  x: (low.x + high.x) / 2,
181
215
  y: (low.y + high.y) / 2,
182
216
  };
@@ -189,6 +223,8 @@ export const sigmaDriver = (sigma, options = {}) => {
189
223
  },
190
224
  gestures: (next) => {
191
225
  held.set(next);
226
+ sigma.setSetting('enableCameraZooming', next !== 'off');
227
+ sigma.setSetting('enableCameraPanning', next !== 'off');
192
228
  },
193
229
  groups: (next) => {
194
230
  if (options.groupLabels !== false) {
@@ -245,9 +281,12 @@ export const sigmaDriver = (sigma, options = {}) => {
245
281
  },
246
282
  onPress: (press) => {
247
283
  const listener = ({ node }) => press(node);
284
+ const away = () => press(undefined);
248
285
  sigma.on('clickNode', listener);
286
+ sigma.on('clickStage', away);
249
287
  return () => {
250
288
  sigma.off('clickNode', listener);
289
+ sigma.off('clickStage', away);
251
290
  };
252
291
  },
253
292
  panBy: (x, y, animate) => {
@@ -270,7 +309,11 @@ export const sigmaDriver = (sigma, options = {}) => {
270
309
  signals.stop();
271
310
  sigma.off('enterNode', enter);
272
311
  sigma.off('leaveNode', leave);
273
- sigma.setSetting('nodeProgramClasses', programs);
312
+ cancelAnimationFrame(gliding);
313
+ container.style.cursor = cursor;
314
+ for (const [name, value] of before) {
315
+ sigma.setSetting(name, value);
316
+ }
274
317
  sized?.disconnect();
275
318
  held.stop();
276
319
  },
@@ -298,12 +341,18 @@ export const sigmaDriver = (sigma, options = {}) => {
298
341
  growing.set(node.id, size);
299
342
  }
300
343
  }
344
+ const moves = new Map();
301
345
  for (const node of changes.changeNodes) {
346
+ const was = graph.getNodeAttributes(node.id);
347
+ const from = { x: Number(was.x), y: Number(was.y) };
348
+ const moved = from.x !== node.x || from.y !== node.y;
349
+ if (animate && moved && Number.isFinite(from.x) && Number.isFinite(from.y)) {
350
+ moves.set(node.id, { from, to: { x: node.x, y: node.y } });
351
+ }
302
352
  graph.mergeNodeAttributes(node.id, {
303
353
  label: node.label,
304
354
  size: pixels(node.size),
305
- x: node.x,
306
- y: node.y,
355
+ ...(moves.has(node.id) ? {} : { x: node.x, y: node.y }),
307
356
  });
308
357
  }
309
358
  for (const link of changes.addLinks) {
@@ -318,8 +367,8 @@ export const sigmaDriver = (sigma, options = {}) => {
318
367
  });
319
368
  }
320
369
  }
321
- if (growing.size > 0) {
322
- grow(growing);
370
+ if (growing.size > 0 || moves.size > 0) {
371
+ glide(growing, moves);
323
372
  }
324
373
  },
325
374
  zoomBy: (direction, animate) => {
@@ -394,6 +443,9 @@ export const sigmaDriver = (sigma, options = {}) => {
394
443
  clearInterval(timer);
395
444
  layout.stop();
396
445
  layout.kill();
446
+ graph.forEachNode((node) => {
447
+ graph.setNodeAttribute(node, 'fixed', false);
448
+ });
397
449
  };
398
450
  },
399
451
  }
@@ -1,7 +1,13 @@
1
1
  import type { GraphDriver } from '../connect-graph/connect-graph.js';
2
- import type { NodeLook } from '../connect-graph/looks.js';
2
+ import { type NodeLook } from '../connect-graph/looks.js';
3
3
  /** How the three.js driver behaves where the page does not say. */
4
4
  export interface ThreeDriverOptions {
5
+ /**
6
+ * How many pixels the drawing has for each of the page's. By default half
7
+ * as many again as the screen has, at least two and at most three, which
8
+ * smooths every edge. Fewer is faster on a slow GPU.
9
+ */
10
+ density?: number | undefined;
5
11
  /**
6
12
  * Whether the graph turns slowly while nobody is handling it. On by
7
13
  * default, and never for a reader who asks for less motion.
@@ -11,7 +17,7 @@ export interface ThreeDriverOptions {
11
17
  groupLabels?: boolean | undefined;
12
18
  /** The longest a layout runs, in milliseconds, settled or not. 10 seconds by default. */
13
19
  layoutLimit?: number | undefined;
14
- /** A note's shape where it does not say: `circle` by default, `square` or `diamond`. */
20
+ /** A note's shape where it does not say: `circle` by default, `square` or `diamond`. A `ring` is drawn as a circle. */
15
21
  shape?: NodeLook['shape'];
16
22
  }
17
23
  /**
@@ -27,6 +33,9 @@ export interface ThreeDriverOptions {
27
33
  * place; with a solid `stencil`, the notes are drawn in its shape and none
28
34
  * runs. Dragging turns the graph, the wheel and a pinch bring it nearer, and
29
35
  * it turns slowly by itself while nobody is handling it, unless the reader
30
- * asks for less motion.
36
+ * asks for less motion. It draws only while something moves, and not at all
37
+ * while the box is out of view or off the page.
38
+ *
39
+ * The renderer is the driver's own, and `disconnect()` frees it.
31
40
  */
32
41
  export declare const threeDriver: (container: HTMLElement, options?: ThreeDriverOptions) => GraphDriver;