@fusion.dev/helix-graph 0.126.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.
- package/README.md +70 -38
- package/dist/connect-graph/arrange.d.ts +4 -2
- package/dist/connect-graph/arrange.js +5 -22
- package/dist/connect-graph/connect-graph.d.ts +18 -6
- package/dist/connect-graph/connect-graph.js +76 -32
- package/dist/connect-graph/diff.d.ts +7 -0
- package/dist/connect-graph/looks.d.ts +8 -0
- package/dist/connect-graph/looks.js +6 -3
- package/dist/connect-graph/seed.d.ts +9 -0
- package/dist/connect-graph/seed.js +21 -15
- package/dist/connect-graph/stencil.d.ts +3 -1
- package/dist/connect-graph/stencil.js +32 -10
- package/dist/cosmos/cosmos-driver.d.ts +5 -4
- package/dist/cosmos/cosmos-driver.js +122 -39
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.js +1 -0
- package/dist/{shapes → model}/model.d.ts +4 -3
- package/dist/{shapes → model}/model.js +10 -5
- package/dist/overlay/overlay.d.ts +14 -8
- package/dist/overlay/overlay.js +64 -62
- package/dist/shapes/flat.d.ts +1 -1
- package/dist/shapes/flat.js +9 -5
- package/dist/shapes/index.d.ts +3 -4
- package/dist/shapes/index.js +3 -4
- package/dist/{connect-graph → shapes}/shape-field.d.ts +7 -3
- package/dist/{connect-graph → shapes}/shape-field.js +4 -2
- package/dist/shapes/solids.d.ts +3 -3
- package/dist/shapes/solids.js +25 -9
- package/dist/{three → shapes}/surface.d.ts +1 -1
- package/dist/shapes/triangles.d.ts +2 -2
- package/dist/shapes/triangles.js +49 -30
- package/dist/sigma/sigma-driver.d.ts +33 -17
- package/dist/sigma/sigma-driver.js +69 -17
- package/dist/three/three-driver.d.ts +12 -3
- package/dist/three/three-driver.js +376 -201
- package/package.json +9 -5
- /package/dist/{three → shapes}/surface.js +0 -0
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import {
|
|
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 '
|
|
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.
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
400
|
-
const lit = new
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
830
|
+
shapesPlaced = false;
|
|
787
831
|
};
|
|
788
|
-
|
|
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
|
|
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
|
|
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
|
|
814
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
910
|
+
colors.push(...color, ...color);
|
|
835
911
|
}
|
|
836
|
-
|
|
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('
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
1147
|
+
const noteAt = (clientX, clientY) => {
|
|
1005
1148
|
const box = canvas.getBoundingClientRect();
|
|
1006
|
-
pointer.set(((
|
|
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
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
paint();
|
|
1166
|
+
point(next);
|
|
1167
|
+
wake();
|
|
1025
1168
|
}
|
|
1026
1169
|
});
|
|
1027
1170
|
};
|
|
1028
1171
|
const leave = () => {
|
|
1029
1172
|
if (hovered !== undefined) {
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
},
|