@fusion.dev/helix-graph 0.126.0 → 0.128.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 (39) hide show
  1. package/README.md +70 -38
  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 +3 -1
  12. package/dist/connect-graph/stencil.js +32 -10
  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/{shapes → model}/model.d.ts +4 -3
  20. package/dist/{shapes → model}/model.js +10 -5
  21. package/dist/overlay/overlay.d.ts +14 -8
  22. package/dist/overlay/overlay.js +64 -62
  23. package/dist/shapes/flat.d.ts +1 -1
  24. package/dist/shapes/flat.js +9 -5
  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 +7 -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 +2 -2
  33. package/dist/shapes/triangles.js +49 -30
  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/{three → shapes}/surface.js +0 -0
@@ -1,21 +1,25 @@
1
- import { forceCenter, forceLink, forceManyBody, forceSimulation, } from 'd3-force-3d';
2
- import { AdditiveBlending, BoxGeometry, BufferAttribute, BufferGeometry, DoubleSide, Euler, InstancedBufferAttribute, InstancedMesh, LineSegments, Matrix4, Mesh, NormalBlending, OctahedronGeometry, PerspectiveCamera, Points, Quaternion, Raycaster, Scene, ShaderMaterial, TOUCH, Vector2, Vector3, WebGLRenderer, } from 'three';
1
+ import { AdditiveBlending, BoxGeometry, BufferAttribute, BufferGeometry, DoubleSide, Euler, InstancedBufferAttribute, InstancedMesh, Matrix4, Mesh, NormalBlending, OctahedronGeometry, PerspectiveCamera, Points, Quaternion, Raycaster, Scene, ShaderMaterial, TOUCH, Vector2, Vector3, WebGLRenderer, } from 'three';
3
2
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3
+ import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
4
+ import { LineSegments2 } from 'three/addons/lines/LineSegments2.js';
5
+ import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
4
6
  import { mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';
7
+ import { linkKey } from '../connect-graph/diff.js';
8
+ import { rgbOf } from '../connect-graph/looks.js';
5
9
  import { ease, gesturesHeld, groupsOver, isLight, namesOver, signalsOver, } from '../overlay/overlay.js';
6
- import { surfaceOf } from './surface.js';
10
+ import { surfaceOf } from '../shapes/surface.js';
7
11
  const SHAPES = { circle: 0, diamond: 2, ring: 0, square: 1 };
8
12
  const REACH = 100;
9
13
  const STEP = 250;
10
14
  const MORPH = 1400;
11
15
  const ZOOM = 1.5;
12
16
  const SAMPLE = 96;
13
- const LINKS = { dark: { close: 0.3, far: 0.05 }, light: { close: 0.55, far: 0.12 } };
17
+ const LINKS = { dark: { close: 0.16, far: 0.025 }, light: { close: 0.36, far: 0.07 } };
14
18
  const REST = 8000;
15
19
  const WASH = { dark: 0.14, full: 1.5, light: 0.18, notes: 6, reach: 3.5 };
16
20
  const BLOOM = { dark: 0.28, light: 0 };
17
21
  const LIGHT = new Vector3(-0.4, 0.8, 0.45).normalize();
18
- const LIFT = 0.42;
22
+ const WIDTH = { lit: 1.8, quiet: 0.8 };
19
23
  const FOV = 40;
20
24
  const FADE = `
21
25
  uniform float middle;
@@ -46,9 +50,9 @@ vec4 orb(vec3 base, vec3 n) {
46
50
  frost *= 0.86 + 0.14 * diffuse;
47
51
  frost = mix(frost, base * 0.78, rim * 0.75);
48
52
  frost += vec3(glint * 0.6);
49
- return dark > 0.5
50
- ? vec4(energy, mix(0.5, 1.0, max(rim, core)))
51
- : vec4(frost, mix(0.78, 1.0, rim));
53
+ /* Opaque, since a body writes its depth: a clear one hid what is behind it
54
+ and showed the ground through itself, a notch where it met another. */
55
+ return dark > 0.5 ? vec4(energy, 1.0) : vec4(frost, 1.0);
52
56
  }
53
57
  `;
54
58
  const VERTEX = `
@@ -62,6 +66,7 @@ uniform float scale;
62
66
  uniform float unit;
63
67
  uniform float bloom;
64
68
  uniform float halo;
69
+ uniform float most;
65
70
 
66
71
  varying vec4 vTint;
67
72
  varying float vShape;
@@ -79,10 +84,13 @@ void main() {
79
84
  where the ground is dark, and more for a glowing one; the body's pass
80
85
  is the note and no more. */
81
86
  vRoom = halo > 0.5 ? 1.0 + (bloom > 0.0 ? 0.9 : 0.0) + glow * 1.4 : 1.0;
82
- /* As large as the note is, at the distance it is: nearer is larger. */
83
- gl_PointSize = size * unit * scale / depth * vRoom;
87
+ /* As large as the note is, at the distance it is: nearer is larger. No
88
+ larger than the GPU draws a point, and a note that would be is drawn
89
+ whole and smaller, where cut to the largest point it lost its edge. */
90
+ float wanted = size * unit * scale / depth * vRoom;
91
+ gl_PointSize = min(wanted, most);
84
92
  vCenter = view.xyz;
85
- vRadius = size * unit * 0.5;
93
+ vRadius = size * unit * 0.5 * gl_PointSize / max(wanted, 0.001);
86
94
  vFade = fade(depth);
87
95
  vTint = tint;
88
96
  vShape = shape;
@@ -174,28 +182,6 @@ void main() {
174
182
  gl_FragColor = vec4(mix(face, edge, line), alpha);
175
183
  }
176
184
  `;
177
- const LINE_VERTEX = `
178
- ${FADE}
179
- attribute vec3 color;
180
- varying vec3 vColor;
181
- varying float vFade;
182
-
183
- void main() {
184
- vec4 view = modelViewMatrix * vec4(position, 1.0);
185
- gl_Position = projectionMatrix * view;
186
- vColor = color;
187
- vFade = fade(max(-view.z, 0.001));
188
- }
189
- `;
190
- const LINE_FRAGMENT = `
191
- uniform float opacity;
192
- varying vec3 vColor;
193
- varying float vFade;
194
-
195
- void main() {
196
- gl_FragColor = vec4(vColor, opacity * vFade);
197
- }
198
- `;
199
185
  const SOLID_VERTEX = `
200
186
  attribute vec4 wash;
201
187
  varying vec3 vNormal;
@@ -292,7 +278,7 @@ const averaged = (values, neighbors, share) => {
292
278
  return out;
293
279
  };
294
280
  const channels = (color) => {
295
- const [red = 128, green = 128, blue = 128] = color.match(/[\d.]+/g)?.map(Number) ?? [];
281
+ const [red, green, blue] = rgbOf(color);
296
282
  return [red / 255, green / 255, blue / 255];
297
283
  };
298
284
  const depthOf = (id) => {
@@ -303,9 +289,9 @@ const depthOf = (id) => {
303
289
  return ((hash >>> 0) / 4294967296 - 0.5) * 2;
304
290
  };
305
291
  export const threeDriver = (container, options = {}) => {
306
- const still = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
292
+ const still = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
307
293
  const renderer = new WebGLRenderer({ alpha: true, antialias: true });
308
- renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
294
+ renderer.setPixelRatio(options.density ?? Math.min(Math.max((window.devicePixelRatio || 1) * 1.5, 2), 3));
309
295
  renderer.setClearColor(0x000000, 0);
310
296
  const canvas = renderer.domElement;
311
297
  canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
@@ -314,10 +300,10 @@ export const threeDriver = (container, options = {}) => {
314
300
  const camera = new PerspectiveCamera(FOV, 1, 0.1, REACH * 40);
315
301
  camera.position.set(0, 0, REACH * 3);
316
302
  const controls = new OrbitControls(camera, canvas);
317
- controls.enableDamping = !still;
303
+ controls.enableDamping = !still();
318
304
  controls.dampingFactor = 0.08;
319
305
  controls.rotateSpeed = 0.6;
320
- controls.autoRotate = !still && options.autoRotate !== false;
306
+ controls.autoRotate = !still() && options.autoRotate !== false;
321
307
  controls.autoRotateSpeed = 0.5;
322
308
  let ids = [];
323
309
  let index = new Map();
@@ -338,23 +324,31 @@ export const threeDriver = (container, options = {}) => {
338
324
  dark: { value: 1 },
339
325
  light: { value: new Vector3(-0.4, 0.8, 0.45).normalize() },
340
326
  middle: { value: 1 },
327
+ most: {
328
+ value: Number(renderer.getContext().getParameter(renderer.getContext().ALIASED_POINT_SIZE_RANGE)?.[1] ?? 256),
329
+ },
341
330
  radius: { value: 1 },
342
331
  scale: { value: 1 },
343
332
  steep: { value: 0 },
344
333
  unit: { value: 1 },
345
334
  };
346
- const lines = (opacity) => new ShaderMaterial({
347
- depthWrite: false,
348
- fragmentShader: LINE_FRAGMENT,
349
- transparent: true,
350
- uniforms: {
351
- middle: uniforms.middle,
352
- opacity: { value: opacity },
353
- radius: uniforms.radius,
354
- steep: uniforms.steep,
355
- },
356
- vertexShader: LINE_VERTEX,
357
- });
335
+ const lines = (opacity, width) => {
336
+ const material = new LineMaterial({
337
+ depthWrite: false,
338
+ linewidth: width,
339
+ opacity,
340
+ transparent: true,
341
+ vertexColors: true,
342
+ });
343
+ material.onBeforeCompile = (shader) => {
344
+ shader.uniforms.middle = uniforms.middle;
345
+ shader.uniforms.radius = uniforms.radius;
346
+ shader.uniforms.steep = uniforms.steep;
347
+ shader.vertexShader = `${FADE}\nvarying float vLinkFade;\n${shader.vertexShader.replace('vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );', 'vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );\nvLinkFade = fade(max(-(position.y < 0.5 ? start.z : end.z), 0.001));')}`;
348
+ shader.fragmentShader = `varying float vLinkFade;\n${shader.fragmentShader.replace('gl_FragColor = vec4( diffuseColor.rgb, alpha );', 'gl_FragColor = vec4( diffuseColor.rgb, alpha * vLinkFade );')}`;
349
+ };
350
+ return material;
351
+ };
358
352
  const points = new Points(new BufferGeometry(), new ShaderMaterial({
359
353
  fragmentShader: FRAGMENT,
360
354
  transparent: true,
@@ -389,15 +383,17 @@ export const threeDriver = (container, options = {}) => {
389
383
  geometry: framed(new BoxGeometry(1.5, 1.5, 1.5)),
390
384
  mesh: undefined,
391
385
  spots: [],
386
+ turns: [],
392
387
  },
393
388
  octahedron: {
394
389
  geometry: framed(new OctahedronGeometry(1.15)),
395
390
  mesh: undefined,
396
391
  spots: [],
392
+ turns: [],
397
393
  },
398
394
  };
399
- const quiet = new LineSegments(new BufferGeometry(), lines(LINKS.dark.far));
400
- const lit = new LineSegments(new BufferGeometry(), lines(0.9));
395
+ const quiet = new LineSegments2(new LineSegmentsGeometry(), lines(LINKS.dark.far, WIDTH.quiet));
396
+ const lit = new LineSegments2(new LineSegmentsGeometry(), lines(0.9, WIDTH.lit));
401
397
  quiet.frustumCulled = false;
402
398
  lit.frustumCulled = false;
403
399
  const solid = new Mesh(new BufferGeometry(), new ShaderMaterial({
@@ -494,10 +490,18 @@ export const threeDriver = (container, options = {}) => {
494
490
  const nearest = [];
495
491
  for (let step = 0; step < sample; step++) {
496
492
  const one = Math.floor((step * ids.length) / sample);
493
+ const [ox, oy, oz] = [
494
+ positions[one * 3],
495
+ positions[one * 3 + 1],
496
+ positions[one * 3 + 2],
497
+ ];
497
498
  let best = Number.POSITIVE_INFINITY;
498
499
  for (let other = 0; other < ids.length; other++) {
499
500
  if (other !== one) {
500
- best = Math.min(best, at(one).distanceToSquared(at(other)));
501
+ const far = (positions[other * 3] - ox) ** 2 +
502
+ (positions[other * 3 + 1] - oy) ** 2 +
503
+ (positions[other * 3 + 2] - oz) ** 2;
504
+ best = Math.min(best, far);
501
505
  }
502
506
  }
503
507
  nearest.push(Math.sqrt(best));
@@ -505,9 +509,10 @@ export const threeDriver = (container, options = {}) => {
505
509
  nearest.sort((one, other) => one - other);
506
510
  const reach = (nearest[Math.floor(nearest.length / 2)] ?? 1) * WASH.reach;
507
511
  const cells = new Map();
508
- const key = (x, y, z) => `${Math.floor(x / reach)},${Math.floor(y / reach)},${Math.floor(z / reach)}`;
512
+ const SIDE = 1 << 16;
513
+ const key = (cx, cy, cz) => ((cx + SIDE / 2) * SIDE + (cy + SIDE / 2)) * SIDE + (cz + SIDE / 2);
509
514
  for (let spot = 0; spot < ids.length; spot++) {
510
- const k = key(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
515
+ const k = key(Math.floor(positions[spot * 3] / reach), Math.floor(positions[spot * 3 + 1] / reach), Math.floor(positions[spot * 3 + 2] / reach));
511
516
  const list = cells.get(k);
512
517
  if (list) {
513
518
  list.push(spot);
@@ -532,7 +537,7 @@ export const threeDriver = (container, options = {}) => {
532
537
  for (let dx = -1; dx <= 1; dx++) {
533
538
  for (let dy = -1; dy <= 1; dy++) {
534
539
  for (let dz = -1; dz <= 1; dz++) {
535
- for (const spot of cells.get(`${cx + dx},${cy + dy},${cz + dz}`) ?? []) {
540
+ for (const spot of cells.get(key(cx + dx, cy + dy, cz + dz)) ?? []) {
536
541
  const far = (positions[spot * 3] - x) ** 2 +
537
542
  (positions[spot * 3 + 1] - y) ** 2 +
538
543
  (positions[spot * 3 + 2] - z) ** 2;
@@ -609,30 +614,7 @@ export const threeDriver = (container, options = {}) => {
609
614
  };
610
615
  };
611
616
  const held = gesturesHeld(container);
612
- const arc = (from, to) => {
613
- const one = index.get(from);
614
- const other = index.get(to);
615
- if (one === undefined || other === undefined) {
616
- return undefined;
617
- }
618
- const a = at(one);
619
- const b = at(other);
620
- const middle = a.clone().add(b).multiplyScalar(0.5);
621
- const center = points.geometry.boundingSphere?.center ?? controls.target;
622
- const out = middle.clone().sub(center);
623
- if (out.lengthSq() < 1e-9) {
624
- out.copy(camera.up);
625
- }
626
- const peak = middle.add(out.normalize().multiplyScalar(a.distanceTo(b) * LIFT * 2));
627
- const depth = peak.clone().applyMatrix4(camera.matrixWorldInverse).z;
628
- if (depth > 0) {
629
- return undefined;
630
- }
631
- peak.project(camera);
632
- const { clientHeight: height, clientWidth: width } = container;
633
- return { x: ((peak.x + 1) / 2) * width, y: ((1 - peak.y) / 2) * height };
634
- };
635
- const signals = signalsOver(container, (id) => nearOnScreen(id), arc);
617
+ const signals = signalsOver(container, (id) => nearOnScreen(id));
636
618
  const nearOnScreen = (id) => {
637
619
  const spot = index.get(id);
638
620
  if (spot === undefined) {
@@ -643,7 +625,7 @@ export const threeDriver = (container, options = {}) => {
643
625
  ? undefined
644
626
  : onScreen(id);
645
627
  };
646
- const groups = groupsOver(container, nearOnScreen);
628
+ const groups = groupsOver(container, nearOnScreen, () => names.boxes());
647
629
  const names = namesOver(container, onScreen);
648
630
  const drawNames = () => {
649
631
  const pointed = hovered === undefined ? undefined : ids[hovered];
@@ -656,7 +638,7 @@ export const threeDriver = (container, options = {}) => {
656
638
  const glideTo = (to, distance, duration) => {
657
639
  const from = controls.target.clone();
658
640
  const fromDistance = camera.position.distanceTo(controls.target);
659
- if (duration <= 0 || still) {
641
+ if (duration <= 0 || still()) {
660
642
  const way = camera.position.clone().sub(controls.target).normalize();
661
643
  controls.target.copy(to);
662
644
  camera.position.copy(to.clone().add(way.multiplyScalar(distance)));
@@ -677,11 +659,12 @@ export const threeDriver = (container, options = {}) => {
677
659
  }
678
660
  return { center, radius: Math.max(radius, 1) };
679
661
  };
680
- const frame = (spots, duration) => {
662
+ const frame = (spots, duration, least = 0) => {
681
663
  if (spots.length === 0) {
682
664
  return;
683
665
  }
684
- const { center, radius } = sphereOf(spots);
666
+ const { center, radius: reach } = sphereOf(spots);
667
+ const radius = Math.max(reach, least);
685
668
  const fov = ((camera.fov / 2) * Math.PI) / 180;
686
669
  const fits = radius / Math.tan(Math.min(fov, Math.atan(Math.tan(fov) * camera.aspect)));
687
670
  glideTo(center, fits * 1.02, duration);
@@ -694,24 +677,62 @@ export const threeDriver = (container, options = {}) => {
694
677
  let crowding = 1;
695
678
  let dark = true;
696
679
  const sizeOf = (size) => 1.6 + 1.35 * Math.max(size, 0);
680
+ let tints = new Float32Array();
681
+ let shapes = new Float32Array();
682
+ let glows = new Float32Array();
683
+ let grownBy = new Float32Array();
684
+ const sizeNote = (spot) => {
685
+ const pointed = spot === hovered;
686
+ drawnSizes[spot] = (sizes[spot] ?? 1) * Math.max(grownBy[spot] ?? 1, pointed ? 1.25 : 1);
687
+ if (!(glows[spot] === 1)) {
688
+ glows[spot] = pointed ? 0.6 : 0;
689
+ }
690
+ };
691
+ const changed = (name) => {
692
+ const attribute = points.geometry.getAttribute(name);
693
+ if (attribute) {
694
+ attribute.needsUpdate = true;
695
+ }
696
+ };
697
+ const attribute = (name, array, size) => {
698
+ const was = points.geometry.getAttribute(name);
699
+ if (was?.array === array) {
700
+ was.needsUpdate = true;
701
+ }
702
+ else {
703
+ points.geometry.setAttribute(name, new BufferAttribute(array, size));
704
+ }
705
+ };
706
+ let blended;
697
707
  const paint = () => {
698
708
  if (!looks) {
699
709
  return;
700
710
  }
701
- const tints = new Float32Array(ids.length * 4);
702
- const drawn = new Float32Array(ids.length);
703
- const shapes = new Float32Array(ids.length);
704
- const glows = new Float32Array(ids.length);
711
+ if (tints.length !== ids.length * 4) {
712
+ tints = new Float32Array(ids.length * 4);
713
+ drawnSizes = new Float32Array(ids.length);
714
+ shapes = new Float32Array(ids.length);
715
+ glows = new Float32Array(ids.length);
716
+ grownBy = new Float32Array(ids.length);
717
+ }
718
+ let tinted = false;
705
719
  selected = undefined;
706
720
  near = new Set();
707
721
  ids.forEach((id, spot) => {
708
722
  const look = looks.node(id);
709
723
  const [red, green, blue] = channels(look.color);
710
- tints.set([red, green, blue, look.hidden ? 0 : 1], spot * 4);
711
- drawn[spot] =
712
- (sizes[spot] ?? 1) * (look.selected ? 1.5 : look.raised || spot === hovered ? 1.25 : 1);
724
+ const alpha = look.hidden ? 0 : 1;
725
+ if (tints[spot * 4] !== red ||
726
+ tints[spot * 4 + 1] !== green ||
727
+ tints[spot * 4 + 2] !== blue ||
728
+ tints[spot * 4 + 3] !== alpha) {
729
+ tints.set([red, green, blue, alpha], spot * 4);
730
+ tinted = true;
731
+ }
732
+ grownBy[spot] = look.selected ? 1.5 : look.raised ? 1.25 : 1;
733
+ glows[spot] = look.selected ? 1 : 0;
734
+ sizeNote(spot);
713
735
  shapes[spot] = SHAPES[look.shape ?? options.shape ?? 'circle'];
714
- glows[spot] = look.selected ? 1 : spot === hovered ? 0.6 : 0;
715
736
  if (look.selected) {
716
737
  selected = id;
717
738
  }
@@ -719,14 +740,14 @@ export const threeDriver = (container, options = {}) => {
719
740
  near.add(id);
720
741
  }
721
742
  });
722
- const geometry = points.geometry;
723
- geometry.setAttribute('tint', new BufferAttribute(tints, 4));
724
- noteTints = tints;
725
- washSurface();
726
- geometry.setAttribute('size', new BufferAttribute(drawn, 1));
727
- geometry.setAttribute('shape', new BufferAttribute(shapes, 1));
728
- geometry.setAttribute('glow', new BufferAttribute(glows, 1));
729
- drawnSizes = drawn;
743
+ attribute('tint', tints, 4);
744
+ attribute('size', drawnSizes, 1);
745
+ attribute('shape', shapes, 1);
746
+ attribute('glow', glows, 1);
747
+ if (tinted || noteTints !== tints) {
748
+ noteTints = tints;
749
+ washSurface();
750
+ }
730
751
  meshShapes(shapes, tints);
731
752
  dark = !isLight(looks.ground);
732
753
  const solidUniforms = solid.material.uniforms;
@@ -736,15 +757,34 @@ export const threeDriver = (container, options = {}) => {
736
757
  uniforms.bloom.value = dark ? BLOOM.dark : BLOOM.light;
737
758
  uniforms.dark.value = dark ? 1 : 0;
738
759
  const blending = dark ? AdditiveBlending : NormalBlending;
739
- halos.material.blending = blending;
740
- shapeMaterial.blending = blending;
741
- shapeMaterial.needsUpdate = true;
742
- quiet.material.blending = blending;
743
- lit.material.blending = blending;
744
- halos.material.needsUpdate = true;
745
- quiet.material.needsUpdate = true;
746
- lit.material.needsUpdate = true;
747
- paintLinks();
760
+ if (blending !== blended) {
761
+ blended = blending;
762
+ for (const material of [halos.material, shapeMaterial, quiet.material, lit.material]) {
763
+ material.blending = blending;
764
+ material.needsUpdate = true;
765
+ }
766
+ }
767
+ paintQuiet();
768
+ paintLit();
769
+ invalidate();
770
+ };
771
+ const point = (next) => {
772
+ const was = hovered;
773
+ hovered = next;
774
+ canvas.style.cursor = next === undefined ? '' : 'pointer';
775
+ if (!looks || drawnSizes.length !== ids.length) {
776
+ return;
777
+ }
778
+ for (const spot of [was, next]) {
779
+ if (spot !== undefined) {
780
+ sizeNote(spot);
781
+ }
782
+ }
783
+ changed('size');
784
+ changed('glow');
785
+ shapesPlaced = false;
786
+ paintLit();
787
+ invalidate();
748
788
  };
749
789
  let drawnSizes = new Float32Array();
750
790
  const meshShapes = (shapes, tints) => {
@@ -759,6 +799,10 @@ export const threeDriver = (container, options = {}) => {
759
799
  }
760
800
  });
761
801
  solidShape.spots = spots;
802
+ solidShape.turns = spots.map((spot) => {
803
+ const twist = depthOf(ids[spot] ?? '');
804
+ return new Quaternion().setFromEuler(new Euler(0.45 + twist * 0.3, 0.6 + twist, 0.2 * twist));
805
+ });
762
806
  if (!solidShape.mesh || solidShape.mesh.count !== spots.length) {
763
807
  if (solidShape.mesh) {
764
808
  scene.remove(solidShape.mesh);
@@ -783,13 +827,19 @@ export const threeDriver = (container, options = {}) => {
783
827
  mesh.geometry.setAttribute('tone', new InstancedBufferAttribute(tone, 4));
784
828
  }
785
829
  }
786
- placeShapes();
830
+ shapesPlaced = false;
787
831
  };
788
- const turned = new Quaternion();
832
+ let shapesPlaced = false;
833
+ let placedUnit = Number.NaN;
789
834
  const placed = new Matrix4();
790
835
  const where = new Vector3();
791
836
  const scaled = new Vector3();
792
837
  const placeShapes = () => {
838
+ if (shapesPlaced && placedUnit === uniforms.unit.value) {
839
+ return;
840
+ }
841
+ shapesPlaced = true;
842
+ placedUnit = uniforms.unit.value;
793
843
  for (const solidShape of [solids.cube, solids.octahedron]) {
794
844
  const mesh = solidShape.mesh;
795
845
  if (!mesh) {
@@ -797,8 +847,7 @@ export const threeDriver = (container, options = {}) => {
797
847
  }
798
848
  solidShape.spots.forEach((spot, at) => {
799
849
  const size = (drawnSizes[spot] ?? 1) * uniforms.unit.value * 0.5;
800
- const twist = depthOf(ids[spot] ?? '');
801
- turned.setFromEuler(new Euler(0.45 + twist * 0.3, 0.6 + twist, 0.2 * twist));
850
+ const turned = solidShape.turns[at];
802
851
  where.set(positions[spot * 3], positions[spot * 3 + 1], positions[spot * 3 + 2]);
803
852
  scaled.setScalar(size);
804
853
  mesh.setMatrixAt(at, placed.compose(where, turned, scaled));
@@ -806,56 +855,74 @@ export const threeDriver = (container, options = {}) => {
806
855
  mesh.instanceMatrix.needsUpdate = true;
807
856
  }
808
857
  };
809
- const paintLinks = () => {
858
+ const fill = (line, ends, colors) => {
859
+ const place = new Float32Array(ends.length * 3);
860
+ ends.forEach((spot, step) => {
861
+ place.set(positions.subarray(spot * 3, spot * 3 + 3), step * 3);
862
+ });
863
+ line.geometry.dispose();
864
+ line.geometry = new LineSegmentsGeometry();
865
+ if (ends.length > 0) {
866
+ line.geometry.setPositions(place);
867
+ line.geometry.setColors(colors);
868
+ }
869
+ line.userData.ends = ends;
870
+ };
871
+ const paintQuiet = () => {
810
872
  if (!looks) {
811
873
  return;
812
874
  }
813
- const on = hovered === undefined ? undefined : ids[hovered];
814
- const quietEnds = [];
815
- const quietTints = [];
816
- const litEnds = [];
817
- const litTints = [];
875
+ const ends = [];
876
+ const colors = [];
818
877
  for (const link of links) {
819
878
  const from = index.get(link.source);
820
879
  const to = index.get(link.target);
821
- if (from === undefined || to === undefined) {
880
+ const look = looks.link(link.source, link.target);
881
+ if (from === undefined || to === undefined || look.hidden || look.raised) {
822
882
  continue;
823
883
  }
884
+ const color = channels(look.color);
885
+ ends.push(from, to);
886
+ colors.push(...color, ...color);
887
+ }
888
+ fill(quiet, ends, colors);
889
+ };
890
+ const paintLit = () => {
891
+ if (!looks) {
892
+ return;
893
+ }
894
+ const on = hovered === undefined ? undefined : ids[hovered];
895
+ const ends = [];
896
+ const colors = [];
897
+ for (const link of links) {
898
+ const touches = on !== undefined && (link.source === on || link.target === on);
824
899
  const look = looks.link(link.source, link.target);
825
- if (look.hidden) {
900
+ if (look.hidden || !(look.raised || touches)) {
901
+ continue;
902
+ }
903
+ const from = index.get(link.source);
904
+ const to = index.get(link.target);
905
+ if (from === undefined || to === undefined) {
826
906
  continue;
827
907
  }
828
- const touches = on !== undefined && (link.source === on || link.target === on);
829
- const whole = look.raised || touches;
830
908
  const color = channels(touches ? looks.node(on).color : look.color);
831
- const ends = whole ? litEnds : quietEnds;
832
- const tints = whole ? litTints : quietTints;
833
909
  ends.push(from, to);
834
- tints.push(...color, ...color);
910
+ colors.push(...color, ...color);
835
911
  }
836
- const fill = (line, ends, tints) => {
837
- const place = new Float32Array(ends.length * 3);
838
- ends.forEach((spot, step) => {
839
- place.set(positions.subarray(spot * 3, spot * 3 + 3), step * 3);
840
- });
841
- line.geometry.setAttribute('position', new BufferAttribute(place, 3));
842
- line.geometry.setAttribute('color', new BufferAttribute(new Float32Array(tints), 3));
843
- line.userData.ends = ends;
844
- };
845
- fill(quiet, quietEnds, quietTints);
846
- fill(lit, litEnds, litTints);
912
+ fill(lit, ends, colors);
847
913
  };
848
914
  const followLinks = () => {
849
915
  for (const line of [quiet, lit]) {
850
916
  const ends = line.userData.ends;
851
- const place = line.geometry.getAttribute('position');
917
+ const place = line.geometry.getAttribute('instanceStart');
852
918
  if (!ends || !place) {
853
919
  continue;
854
920
  }
921
+ const array = place.data.array;
855
922
  ends.forEach((spot, step) => {
856
- place.array.set(positions.subarray(spot * 3, spot * 3 + 3), step * 3);
923
+ array.set(positions.subarray(spot * 3, spot * 3 + 3), step * 3);
857
924
  });
858
- place.needsUpdate = true;
925
+ place.data.needsUpdate = true;
859
926
  }
860
927
  };
861
928
  const load = (nodes, next, places) => {
@@ -866,9 +933,12 @@ export const threeDriver = (container, options = {}) => {
866
933
  sizes = nodes.map((node) => sizeOf(node.size));
867
934
  links = next.filter((link) => index.has(link.source) && index.has(link.target));
868
935
  positions = places;
936
+ morph = undefined;
869
937
  points.geometry.setAttribute('position', new BufferAttribute(positions, 3));
870
938
  points.geometry.computeBoundingSphere();
871
939
  hovered = undefined;
940
+ canvas.style.cursor = '';
941
+ shapesPlaced = false;
872
942
  paint();
873
943
  if (solid.visible) {
874
944
  tieSurface();
@@ -904,15 +974,21 @@ export const threeDriver = (container, options = {}) => {
904
974
  renderer.domElement.height / 2 / Math.tan(((camera.fov / 2) * Math.PI) / 180);
905
975
  const closeness = Math.min(Math.max(1 - distance / (radius * 3), 0), 1);
906
976
  const shade = LINKS[dark ? 'dark' : 'light'];
907
- quiet.material.uniforms.opacity.value =
908
- shade.far + (shade.close - shade.far) * closeness;
977
+ quiet.material.opacity = shade.far + (shade.close - shade.far) * closeness;
909
978
  };
979
+ let dirty = true;
980
+ const invalidate = () => {
981
+ dirty = true;
982
+ };
983
+ let visible = true;
910
984
  let pending = 0;
911
985
  const tick = (now) => {
912
- if (stopped) {
986
+ pending = 0;
987
+ if (stopped || !visible || !container.isConnected) {
913
988
  return;
914
989
  }
915
990
  pending = requestAnimationFrame(tick);
991
+ const moving = glide !== undefined || morph !== undefined || layout !== undefined;
916
992
  if (glide) {
917
993
  const through = Math.min((now - glide.start) / glide.duration, 1);
918
994
  const eased = ease(through);
@@ -926,7 +1002,7 @@ export const threeDriver = (container, options = {}) => {
926
1002
  }
927
1003
  }
928
1004
  if (morph) {
929
- const through = still ? 1 : Math.min((now - morph.start) / MORPH, 1);
1005
+ const through = still() ? 1 : Math.min((now - morph.start) / MORPH, 1);
930
1006
  const eased = ease(through);
931
1007
  for (let step = 0; step < positions.length; step++) {
932
1008
  positions[step] =
@@ -936,6 +1012,7 @@ export const threeDriver = (container, options = {}) => {
936
1012
  ;
937
1013
  points.geometry.getAttribute('position').needsUpdate = true;
938
1014
  followLinks();
1015
+ shapesPlaced = false;
939
1016
  if (through >= 1) {
940
1017
  morph = undefined;
941
1018
  points.geometry.computeBoundingSphere();
@@ -953,6 +1030,7 @@ export const threeDriver = (container, options = {}) => {
953
1030
  points.geometry.getAttribute('position').needsUpdate = true;
954
1031
  points.geometry.computeBoundingSphere();
955
1032
  followLinks();
1033
+ shapesPlaced = false;
956
1034
  if (layout.simulation.alpha() <= layout.simulation.alphaMin() || now > layout.limit) {
957
1035
  layout.done();
958
1036
  }
@@ -961,26 +1039,47 @@ export const threeDriver = (container, options = {}) => {
961
1039
  }
962
1040
  }
963
1041
  controls.autoRotate =
964
- !still &&
1042
+ !still() &&
965
1043
  options.autoRotate !== false &&
966
1044
  hovered === undefined &&
967
1045
  selected === undefined &&
968
1046
  now - handled > REST;
969
- controls.update();
1047
+ controls.enableDamping = !still();
1048
+ const turned = controls.update();
1049
+ if (!(moving || turned || dirty)) {
1050
+ return;
1051
+ }
1052
+ dirty = false;
970
1053
  cue();
971
1054
  if (solids.cube.mesh || solids.octahedron.mesh) {
972
1055
  placeShapes();
973
1056
  }
974
1057
  renderer.render(scene, camera);
975
- groups.draw();
976
1058
  drawNames();
1059
+ groups.draw();
1060
+ };
1061
+ const wake = () => {
1062
+ invalidate();
1063
+ if (!pending && !stopped && visible && container.isConnected) {
1064
+ pending = requestAnimationFrame(tick);
1065
+ }
977
1066
  };
1067
+ const seen = typeof IntersectionObserver === 'function'
1068
+ ? new IntersectionObserver((entries) => {
1069
+ visible = entries.at(-1)?.isIntersecting ?? true;
1070
+ if (visible) {
1071
+ wake();
1072
+ }
1073
+ })
1074
+ : undefined;
1075
+ seen?.observe(container);
978
1076
  const resize = () => {
979
1077
  const width = Math.max(container.clientWidth, 1);
980
1078
  const height = Math.max(container.clientHeight, 1);
981
1079
  renderer.setSize(width, height, false);
982
1080
  camera.aspect = width / height;
983
1081
  camera.updateProjectionMatrix();
1082
+ invalidate();
984
1083
  };
985
1084
  let resizing = 0;
986
1085
  const sized = new ResizeObserver(() => {
@@ -994,42 +1093,85 @@ export const threeDriver = (container, options = {}) => {
994
1093
  moved = true;
995
1094
  handled = Number.POSITIVE_INFINITY;
996
1095
  glide = undefined;
1096
+ wake();
997
1097
  });
998
1098
  controls.addEventListener('end', () => {
999
1099
  handled = performance.now();
1000
1100
  });
1101
+ const frontOf = (ray) => {
1102
+ const frame = shapeFrame;
1103
+ if (!frame) {
1104
+ return Number.POSITIVE_INFINITY;
1105
+ }
1106
+ const { depth, distance, height, width } = frame.field;
1107
+ const by = frame.scale * scale;
1108
+ const toBox = (x, y, z) => [
1109
+ (x / scale - frame.middle.x) / frame.scale + width / 2,
1110
+ -(y / scale - frame.middle.y) / frame.scale + height / 2,
1111
+ (z / scale - frame.middle.z) / frame.scale + depth / 2,
1112
+ ];
1113
+ const [ox, oy, oz] = toBox(ray.origin.x, ray.origin.y, ray.origin.z);
1114
+ const way = [ray.direction.x, -ray.direction.y, ray.direction.z];
1115
+ let enter = 0;
1116
+ let leave = Number.POSITIVE_INFINITY;
1117
+ for (const [from, along, size] of [
1118
+ [ox, way[0], width],
1119
+ [oy, way[1], height],
1120
+ [oz, way[2], depth],
1121
+ ]) {
1122
+ if (Math.abs(along) < 1e-9) {
1123
+ if (from < 0 || from > size) {
1124
+ return Number.POSITIVE_INFINITY;
1125
+ }
1126
+ continue;
1127
+ }
1128
+ const one = (0 - from) / along;
1129
+ const other = (size - from) / along;
1130
+ enter = Math.max(enter, Math.min(one, other));
1131
+ leave = Math.min(leave, Math.max(one, other));
1132
+ }
1133
+ const inset = frame.shell * 1.15;
1134
+ const least = Math.max(width, height, depth) / 400;
1135
+ for (let t = enter, step = 0; t <= leave && step < 128; step++) {
1136
+ const away = distance(ox + way[0] * t, oy + way[1] * t, oz + way[2] * t) + inset;
1137
+ if (away <= least) {
1138
+ return t * by;
1139
+ }
1140
+ t += Math.max(away, least);
1141
+ }
1142
+ return Number.POSITIVE_INFINITY;
1143
+ };
1001
1144
  const raycaster = new Raycaster();
1002
1145
  const pointer = new Vector2();
1003
1146
  let hovering = 0;
1004
- const hover = (event) => {
1147
+ const noteAt = (clientX, clientY) => {
1005
1148
  const box = canvas.getBoundingClientRect();
1006
- pointer.set(((event.clientX - box.left) / box.width) * 2 - 1, -((event.clientY - box.top) / box.height) * 2 + 1);
1149
+ pointer.set(((clientX - box.left) / box.width) * 2 - 1, -((clientY - box.top) / box.height) * 2 + 1);
1150
+ raycaster.setFromCamera(pointer, camera);
1151
+ raycaster.params.Points = { threshold: uniforms.unit.value * 2.4 };
1152
+ const front = solid.visible ? frontOf(raycaster.ray) : Number.POSITIVE_INFINITY;
1153
+ return raycaster.intersectObject(points, false).find((one) => {
1154
+ const id = one.index === undefined ? undefined : ids[one.index];
1155
+ return (id !== undefined &&
1156
+ looks?.node(id).hidden !== true &&
1157
+ one.distance < front + uniforms.unit.value * 3);
1158
+ })?.index;
1159
+ };
1160
+ const hover = (event) => {
1161
+ const { clientX, clientY } = event;
1007
1162
  cancelAnimationFrame(hovering);
1008
1163
  hovering = requestAnimationFrame(() => {
1009
- raycaster.setFromCamera(pointer, camera);
1010
- raycaster.params.Points = { threshold: uniforms.unit.value * 2.4 };
1011
- const front = solid.visible
1012
- ? (raycaster.intersectObject(solid, false)[0]?.distance ?? Number.POSITIVE_INFINITY)
1013
- : Number.POSITIVE_INFINITY;
1014
- const hit = raycaster.intersectObject(points, false).find((one) => {
1015
- const id = one.index === undefined ? undefined : ids[one.index];
1016
- return (id !== undefined &&
1017
- looks?.node(id).hidden !== true &&
1018
- one.distance < front + uniforms.unit.value * 3);
1019
- });
1020
- const next = hit?.index;
1164
+ const next = noteAt(clientX, clientY);
1021
1165
  if (next !== hovered) {
1022
- hovered = next;
1023
- canvas.style.cursor = next === undefined ? '' : 'pointer';
1024
- paint();
1166
+ point(next);
1167
+ wake();
1025
1168
  }
1026
1169
  });
1027
1170
  };
1028
1171
  const leave = () => {
1029
1172
  if (hovered !== undefined) {
1030
- hovered = undefined;
1031
- canvas.style.cursor = '';
1032
- paint();
1173
+ point(undefined);
1174
+ wake();
1033
1175
  }
1034
1176
  };
1035
1177
  canvas.addEventListener('pointermove', hover);
@@ -1038,11 +1180,16 @@ export const threeDriver = (container, options = {}) => {
1038
1180
  center: (id, animate) => {
1039
1181
  const spot = index.get(id);
1040
1182
  if (spot !== undefined) {
1183
+ moved = true;
1041
1184
  glideTo(at(spot), camera.position.distanceTo(controls.target), animate ? STEP * 2 : 0);
1185
+ wake();
1042
1186
  }
1043
1187
  },
1044
1188
  draw: ({ links: next, nodes }) => {
1045
- const reach = Math.max(1e-6, ...nodes.map((node) => Math.max(Math.abs(node.x), Math.abs(node.y), Math.abs(node.z ?? 0))));
1189
+ let reach = 1e-6;
1190
+ for (const node of nodes) {
1191
+ reach = Math.max(reach, Math.abs(node.x), Math.abs(node.y), Math.abs(node.z ?? 0));
1192
+ }
1046
1193
  scale = REACH / reach;
1047
1194
  const places = new Float32Array(nodes.length * 3);
1048
1195
  nodes.forEach((node, spot) => {
@@ -1052,28 +1199,34 @@ export const threeDriver = (container, options = {}) => {
1052
1199
  load(nodes, next, places);
1053
1200
  shapeSolid();
1054
1201
  fitAll(0);
1202
+ wake();
1055
1203
  },
1056
1204
  fit: (fitted, animate) => {
1205
+ moved = true;
1206
+ wake();
1057
1207
  frame(fitted.flatMap((id) => {
1058
1208
  const spot = index.get(id);
1059
1209
  return spot === undefined ? [] : [spot];
1060
- }), animate ? STEP * 2 : 0);
1210
+ }), animate ? STEP * 2 : 0, (points.geometry.boundingSphere?.radius ?? REACH) * 0.35);
1061
1211
  },
1062
1212
  gestures: (next) => {
1063
1213
  held.set(next);
1064
1214
  controls.enabled = next !== 'off';
1065
1215
  controls.touches.ONE = next === 'cooperative' ? null : TOUCH.ROTATE;
1216
+ wake();
1066
1217
  },
1067
1218
  groups: (next) => {
1068
1219
  if (options.groupLabels !== false) {
1069
1220
  groups.set(next.map((group) => ({ ...group, ids: group.ids.slice(0, SAMPLE) })));
1070
1221
  }
1222
+ wake();
1071
1223
  },
1072
1224
  look: (next) => {
1073
1225
  looks = next;
1074
1226
  signals.look(next);
1075
1227
  groups.look(next);
1076
1228
  paint();
1229
+ wake();
1077
1230
  },
1078
1231
  onPress: (press) => {
1079
1232
  let down;
@@ -1085,12 +1238,9 @@ export const threeDriver = (container, options = {}) => {
1085
1238
  down = undefined;
1086
1239
  if (was &&
1087
1240
  Math.hypot(event.clientX - was.x, event.clientY - was.y) < 5 &&
1088
- performance.now() - was.time < 500 &&
1089
- hovered !== undefined) {
1090
- const id = ids[hovered];
1091
- if (id !== undefined) {
1092
- press(id);
1093
- }
1241
+ performance.now() - was.time < 500) {
1242
+ const spot = noteAt(event.clientX, event.clientY);
1243
+ press(spot === undefined ? undefined : ids[spot]);
1094
1244
  }
1095
1245
  };
1096
1246
  canvas.addEventListener('pointerdown', start);
@@ -1108,7 +1258,9 @@ export const threeDriver = (container, options = {}) => {
1108
1258
  const side = new Vector3().crossVectors(turn, offset).normalize();
1109
1259
  offset.applyAxisAngle(side, (y / Math.max(height, 1)) * (Math.PI / 2));
1110
1260
  const to = controls.target.clone().add(offset);
1111
- if (!animate || still) {
1261
+ moved = true;
1262
+ invalidate();
1263
+ if (!animate || still()) {
1112
1264
  camera.position.copy(to);
1113
1265
  return;
1114
1266
  }
@@ -1124,35 +1276,55 @@ export const threeDriver = (container, options = {}) => {
1124
1276
  requestAnimationFrame(step);
1125
1277
  },
1126
1278
  settle: (unplaced, placed) => {
1279
+ if (morph) {
1280
+ positions.set(morph.to);
1281
+ morph = undefined;
1282
+ points.geometry.getAttribute('position').needsUpdate = true;
1283
+ points.geometry.computeBoundingSphere();
1284
+ }
1127
1285
  const nodes = ids.map((id, spot) => {
1128
1286
  const x = positions[spot * 3] / scale;
1129
1287
  const y = positions[spot * 3 + 1] / scale;
1130
1288
  const z = positions[spot * 3 + 2] / scale;
1131
1289
  return unplaced.has(id) ? { id, x, y, z } : { fx: x, fy: y, fz: z, id, x, y, z };
1132
1290
  });
1133
- const simulation = forceSimulation(nodes, 3)
1134
- .force('link', forceLink(links.map((link) => ({ source: link.source, target: link.target })))
1135
- .id((node) => node.id)
1136
- .distance(1.6))
1137
- .force('charge', forceManyBody().strength(-1.2).theta(1.2))
1138
- .force('center', forceCenter(0, 0, 0))
1139
- .alphaDecay(0.03)
1140
- .stop();
1141
1291
  let done = false;
1142
- layout = {
1143
- done: () => {
1144
- if (done) {
1145
- return;
1146
- }
1292
+ void import('d3-force-3d')
1293
+ .then(({ forceCenter, forceLink, forceManyBody, forceSimulation }) => {
1294
+ if (done || stopped) {
1295
+ return;
1296
+ }
1297
+ const simulation = forceSimulation(nodes, 3)
1298
+ .force('link', forceLink(links.map((link) => ({ source: link.source, target: link.target })))
1299
+ .id((node) => node.id)
1300
+ .distance(1.6))
1301
+ .force('charge', forceManyBody().strength(-1.2).theta(1.2))
1302
+ .force('center', forceCenter(0, 0, 0))
1303
+ .alphaDecay(0.03)
1304
+ .stop();
1305
+ layout = {
1306
+ done: () => {
1307
+ if (done) {
1308
+ return;
1309
+ }
1310
+ done = true;
1311
+ layout = undefined;
1312
+ fitAll(STEP * 2);
1313
+ placed(new Map(nodes.map((node) => [node.id, { x: node.x, y: node.y, z: node.z }])));
1314
+ },
1315
+ limit: performance.now() + (options.layoutLimit ?? 10000),
1316
+ nodes,
1317
+ simulation,
1318
+ };
1319
+ wake();
1320
+ })
1321
+ .catch((cause) => {
1322
+ console.error(new Error('The three.js driver could not load d3-force-3d to lay out the notes. Install d3-force-3d alongside @fusion.dev/helix-graph.', { cause }));
1323
+ if (!done && !stopped) {
1147
1324
  done = true;
1148
- layout = undefined;
1149
- fitAll(STEP * 2);
1150
1325
  placed(new Map(nodes.map((node) => [node.id, { x: node.x, y: node.y, z: node.z }])));
1151
- },
1152
- limit: performance.now() + (options.layoutLimit ?? 10000),
1153
- nodes,
1154
- simulation,
1155
- };
1326
+ }
1327
+ });
1156
1328
  return () => {
1157
1329
  done = true;
1158
1330
  layout = undefined;
@@ -1165,6 +1337,7 @@ export const threeDriver = (container, options = {}) => {
1165
1337
  solidAt = Number.NaN;
1166
1338
  }
1167
1339
  uniforms.steep.value = surfaced && !frame ? 1 : 0;
1340
+ wake();
1168
1341
  },
1169
1342
  signal: (from, to, colors, animate) => {
1170
1343
  signals.signal(from, to, colors, animate);
@@ -1175,6 +1348,7 @@ export const threeDriver = (container, options = {}) => {
1175
1348
  cancelAnimationFrame(hovering);
1176
1349
  cancelAnimationFrame(resizing);
1177
1350
  sized.disconnect();
1351
+ seen?.disconnect();
1178
1352
  canvas.removeEventListener('pointermove', hover);
1179
1353
  canvas.removeEventListener('pointerleave', leave);
1180
1354
  controls.dispose();
@@ -1188,6 +1362,7 @@ export const threeDriver = (container, options = {}) => {
1188
1362
  object.geometry.dispose();
1189
1363
  object.material.dispose();
1190
1364
  }
1365
+ renderer.forceContextLoss();
1191
1366
  renderer.dispose();
1192
1367
  canvas.remove();
1193
1368
  signals.stop();
@@ -1228,11 +1403,8 @@ export const threeDriver = (container, options = {}) => {
1228
1403
  for (const node of changes.addNodes) {
1229
1404
  kept.set(node.id, { node, world: toWorld(node) });
1230
1405
  }
1231
- const removed = new Set(changes.removeLinks.map((link) => `${link.source}\u0000${link.target}`));
1232
- const next = [
1233
- ...links.filter((link) => !removed.has(`${link.source}\u0000${link.target}`)),
1234
- ...changes.addLinks,
1235
- ];
1406
+ const removed = new Set(changes.removeLinks.map(linkKey));
1407
+ const next = [...links.filter((link) => !removed.has(linkKey(link))), ...changes.addLinks];
1236
1408
  const entries = [...kept.values()];
1237
1409
  const from = new Float32Array(entries.length * 3);
1238
1410
  const to = new Float32Array(entries.length * 3);
@@ -1247,8 +1419,11 @@ export const threeDriver = (container, options = {}) => {
1247
1419
  if (shifted) {
1248
1420
  fitAll(animate ? MORPH : 0);
1249
1421
  }
1422
+ wake();
1250
1423
  },
1251
1424
  zoomBy: (direction, animate) => {
1425
+ moved = true;
1426
+ wake();
1252
1427
  const distance = camera.position.distanceTo(controls.target);
1253
1428
  glideTo(controls.target.clone(), direction > 0 ? distance / ZOOM : distance * ZOOM, animate ? STEP : 0);
1254
1429
  },