@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.
- package/README.md +84 -35
- 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 +11 -1
- package/dist/connect-graph/stencil.js +109 -11
- 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/model/model.d.ts +16 -0
- package/dist/model/model.js +50 -0
- package/dist/overlay/overlay.d.ts +14 -8
- package/dist/overlay/overlay.js +64 -62
- package/dist/shapes/flat.d.ts +3 -2
- package/dist/shapes/flat.js +59 -6
- package/dist/shapes/index.d.ts +3 -4
- package/dist/shapes/index.js +3 -4
- package/dist/{connect-graph → shapes}/shape-field.d.ts +12 -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 +12 -3
- package/dist/shapes/triangles.js +83 -2
- 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/shapes/model.d.ts +0 -12
- package/dist/shapes/model.js +0 -31
- /package/dist/{three → shapes}/surface.js +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** A point in the renderer's own units. */
|
|
2
2
|
export interface GraphPoint {
|
|
3
|
+
/** Across. */
|
|
3
4
|
x: number;
|
|
5
|
+
/** Up. */
|
|
4
6
|
y: number;
|
|
5
7
|
/** How near the viewer, for a renderer that draws in three dimensions. */
|
|
6
8
|
z?: number | undefined;
|
|
@@ -17,6 +19,13 @@ export interface SeedLink {
|
|
|
17
19
|
source: string;
|
|
18
20
|
target: string;
|
|
19
21
|
}
|
|
22
|
+
/** The angle between one place and the next on a spiral: the golden angle, which never lines two up. */
|
|
23
|
+
export declare const TURN: number;
|
|
24
|
+
/**
|
|
25
|
+
* Each note's neighbors across the links, both ways, kept to the links
|
|
26
|
+
* whose two ends `keep` allows where it is given.
|
|
27
|
+
*/
|
|
28
|
+
export declare const neighborsOf: (links: readonly SeedLink[], keep?: (id: string) => boolean) => Map<string, string[]>;
|
|
20
29
|
/**
|
|
21
30
|
* Where each note starts, and which of them a layout still has to place.
|
|
22
31
|
*
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
const TURN = Math.PI * (3 - Math.sqrt(5));
|
|
1
|
+
export const TURN = Math.PI * (3 - Math.sqrt(5));
|
|
2
|
+
export const neighborsOf = (links, keep) => {
|
|
3
|
+
const neighbors = new Map();
|
|
4
|
+
const add = (from, to) => {
|
|
5
|
+
const list = neighbors.get(from);
|
|
6
|
+
if (list) {
|
|
7
|
+
list.push(to);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
neighbors.set(from, [to]);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
for (const { source, target } of links) {
|
|
14
|
+
if (!keep || (keep(source) && keep(target))) {
|
|
15
|
+
add(source, target);
|
|
16
|
+
add(target, source);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return neighbors;
|
|
20
|
+
};
|
|
2
21
|
const NEAR = 0.5;
|
|
3
22
|
const nudge = (id) => {
|
|
4
23
|
let hash = 2166136261;
|
|
@@ -26,20 +45,7 @@ export const seedPositions = (nodes, links, known = new Map()) => {
|
|
|
26
45
|
}
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
|
-
const neighbors =
|
|
30
|
-
const add = (from, to) => {
|
|
31
|
-
const list = neighbors.get(from);
|
|
32
|
-
if (list) {
|
|
33
|
-
list.push(to);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
neighbors.set(from, [to]);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
for (const { source, target } of links) {
|
|
40
|
-
add(source, target);
|
|
41
|
-
add(target, source);
|
|
42
|
-
}
|
|
48
|
+
const neighbors = neighborsOf(links);
|
|
43
49
|
let spiral = positions.size;
|
|
44
50
|
for (const node of nodes) {
|
|
45
51
|
if (positions.has(node.id)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { type ShapeField } from '../shapes/shape-field.js';
|
|
1
2
|
import type { GraphPoint } from './seed.js';
|
|
2
|
-
import { type ShapeField } from './shape-field.js';
|
|
3
3
|
/**
|
|
4
4
|
* A shape the notes are drawn in. **Any shape field**, flat or solid, such as
|
|
5
5
|
* `inflate` makes; or, flat, an SVG path in a box `width` by `height`, as an
|
|
@@ -23,6 +23,14 @@ export type GraphStencil = (ShapeField | {
|
|
|
23
23
|
place?: 'fill' | 'surface' | undefined;
|
|
24
24
|
/** How thick the surface's shell is, as a share of the shape's size. 0.04 by default. */
|
|
25
25
|
shell?: number | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Which named part of the shape each group's notes go to, by group, such
|
|
28
|
+
* as `{ Vision: 'occipital' }`, where the shape has named parts: a model's
|
|
29
|
+
* named meshes, or an SVG's elements with an `id`. A group not named here,
|
|
30
|
+
* or naming a part the shape does not have, shares the parts no group
|
|
31
|
+
* claims.
|
|
32
|
+
*/
|
|
33
|
+
regions?: Readonly<Record<string, string>> | undefined;
|
|
26
34
|
};
|
|
27
35
|
/** The field a stencil is, or is made into. */
|
|
28
36
|
export declare const fieldOf: (stencil: GraphStencil) => ShapeField;
|
|
@@ -55,7 +63,9 @@ export declare const fitToStencil: (positions: ReadonlyMap<string, GraphPoint>,
|
|
|
55
63
|
* `middle.y - (y - height / 2) * scale` and `middle.z + (z - depth / 2) * scale`.
|
|
56
64
|
*/
|
|
57
65
|
export interface StencilFrame {
|
|
66
|
+
/** The stencil's shape, as a field, whatever form it was given in. */
|
|
58
67
|
field: ShapeField;
|
|
68
|
+
/** Where the middle of the stencil's box was put, in the places' units. */
|
|
59
69
|
middle: {
|
|
60
70
|
x: number;
|
|
61
71
|
y: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fieldFromInside, fieldFromPath } from '
|
|
1
|
+
import { fieldFromInside, fieldFromPath } from '../shapes/shape-field.js';
|
|
2
2
|
import { surfaceWeb } from './surface-web.js';
|
|
3
3
|
const PROBE = { flat: 64, solid: 32 };
|
|
4
4
|
const SHELL = 0.04;
|
|
@@ -78,6 +78,72 @@ export const placesIn = (field, count, { place = 'fill', shell = SHELL } = {}) =
|
|
|
78
78
|
}
|
|
79
79
|
return [];
|
|
80
80
|
};
|
|
81
|
+
const placesByRegion = (field, asked, options) => {
|
|
82
|
+
const regionOf = field.regionOf;
|
|
83
|
+
const count = [...asked.values()].reduce((sum, many) => sum + many, 0);
|
|
84
|
+
if (!regionOf) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
for (let times = 3; times <= 24; times *= 2) {
|
|
88
|
+
const pool = placesIn(field, count * times, options);
|
|
89
|
+
if (pool.length === 0) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const byRegion = new Map();
|
|
93
|
+
const inRegion = pool.map((place) => regionOf(place.x, place.y, place.z));
|
|
94
|
+
pool.forEach((place, at) => {
|
|
95
|
+
const region = inRegion[at];
|
|
96
|
+
const list = byRegion.get(region);
|
|
97
|
+
if (list) {
|
|
98
|
+
list.push(place);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
byRegion.set(region, [place]);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const found = new Set([...byRegion.keys()].filter((region) => region !== undefined));
|
|
105
|
+
const missing = [...asked.keys()].filter((region) => region !== undefined && !found.has(region));
|
|
106
|
+
if (missing.length > 0 && times * 2 <= 24) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const wanted = new Map();
|
|
110
|
+
for (const [region, many] of asked) {
|
|
111
|
+
const part = region !== undefined && found.has(region) ? region : undefined;
|
|
112
|
+
wanted.set(part, (wanted.get(part) ?? 0) + many);
|
|
113
|
+
}
|
|
114
|
+
const claimed = new Set([...wanted.keys()].filter((region) => region !== undefined));
|
|
115
|
+
const places = [];
|
|
116
|
+
const regions = [];
|
|
117
|
+
let short = false;
|
|
118
|
+
for (const [region, many] of wanted) {
|
|
119
|
+
if (region === undefined) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const own = byRegion.get(region) ?? [];
|
|
123
|
+
if (own.length < many) {
|
|
124
|
+
short = true;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
for (const place of own.slice(0, many)) {
|
|
128
|
+
places.push(place);
|
|
129
|
+
regions.push(region);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const rest = wanted.get(undefined) ?? 0;
|
|
133
|
+
const taken = new Set(places);
|
|
134
|
+
const free = pool.filter((_, at) => !claimed.has(inRegion[at]));
|
|
135
|
+
const spare = free.length >= rest ? free : pool.filter((place) => !taken.has(place));
|
|
136
|
+
if (short || spare.length < rest) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const place of spare.slice(0, rest)) {
|
|
140
|
+
places.push(place);
|
|
141
|
+
regions.push(undefined);
|
|
142
|
+
}
|
|
143
|
+
return { found, missing, places, regions };
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
81
147
|
const nested = (items, keys) => {
|
|
82
148
|
const [key, ...rest] = keys;
|
|
83
149
|
if (!key) {
|
|
@@ -101,10 +167,38 @@ export const fitInStencil = (positions, stencil, { clustersOf, } = {}) => {
|
|
|
101
167
|
return { positions: new Map() };
|
|
102
168
|
}
|
|
103
169
|
const field = fieldOf(stencil);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
170
|
+
const options = { place: stencil.place ?? 'fill', shell: stencil.shell ?? SHELL };
|
|
171
|
+
const groupOf = (id) => clustersOf?.(id)[0];
|
|
172
|
+
const noteRegion = new Map(notes.map((note) => {
|
|
173
|
+
const group = groupOf(note.id);
|
|
174
|
+
return [note.id, group === undefined ? undefined : stencil.regions?.[group]];
|
|
175
|
+
}));
|
|
176
|
+
const asked = new Map();
|
|
177
|
+
for (const region of noteRegion.values()) {
|
|
178
|
+
asked.set(region, (asked.get(region) ?? 0) + 1);
|
|
179
|
+
}
|
|
180
|
+
const regional = [...asked.keys()].some((region) => region !== undefined)
|
|
181
|
+
? placesByRegion(field, asked, options)
|
|
182
|
+
: undefined;
|
|
183
|
+
if (regional && regional.missing.length > 0) {
|
|
184
|
+
console.warn(`fitInStencil: the shape has no part named ${regional.missing.map((part) => `"${part}"`).join(', ')}, so the notes sent there are placed with the rest. Its parts: ${[...regional.found].join(', ') || 'none'}.`);
|
|
185
|
+
}
|
|
186
|
+
else if (!regional && [...asked.keys()].some((region) => region !== undefined)) {
|
|
187
|
+
console.warn(field.regionOf
|
|
188
|
+
? 'fitInStencil: the parts named in regions are too small for the notes sent to them, so the notes are placed over the whole shape.'
|
|
189
|
+
: 'fitInStencil: the shape has no named parts, so regions is not used and the notes are placed over the whole shape.');
|
|
190
|
+
}
|
|
191
|
+
for (const [id, region] of noteRegion) {
|
|
192
|
+
if (region !== undefined && !regional?.found.has(region)) {
|
|
193
|
+
noteRegion.set(id, undefined);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const wanted = new Map();
|
|
197
|
+
for (const region of noteRegion.values()) {
|
|
198
|
+
wanted.set(region, (wanted.get(region) ?? 0) + 1);
|
|
199
|
+
}
|
|
200
|
+
const places = regional?.places ?? placesIn(field, notes.length, options);
|
|
201
|
+
const placeRegion = regional?.regions ?? places.map(() => undefined);
|
|
108
202
|
if (places.length < notes.length) {
|
|
109
203
|
return { positions: new Map(positions) };
|
|
110
204
|
}
|
|
@@ -237,13 +331,17 @@ export const fitInStencil = (positions, stencil, { clustersOf, } = {}) => {
|
|
|
237
331
|
halve(ours.slice(0, half), near, level);
|
|
238
332
|
halve(ours.slice(half), rest, level);
|
|
239
333
|
};
|
|
240
|
-
|
|
334
|
+
for (const region of wanted.keys()) {
|
|
335
|
+
halve(notes.filter((note) => noteRegion.get(note.id) === region), targets.flatMap((_, at) => (placeRegion[at] === region ? [at] : [])));
|
|
336
|
+
}
|
|
241
337
|
return { frame: { field, middle, scale, shell: thickness }, positions: fitted };
|
|
242
338
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
339
|
+
for (const region of wanted.keys()) {
|
|
340
|
+
const ours = nested(notes.filter((note) => noteRegion.get(note.id) === region), [(note) => note.at.x, (note) => note.at.y]);
|
|
341
|
+
const theirs = nested(spots.filter((_, at) => placeRegion[at] === region), [(spot) => spot.across, (spot) => spot.down]);
|
|
342
|
+
ours.forEach((note, at) => {
|
|
343
|
+
fitted.set(note.id, theirs[at].at);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
248
346
|
return { frame: { field, middle, scale, shell: thickness }, positions: fitted };
|
|
249
347
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GraphDriver } from '../connect-graph/connect-graph.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type NodeLook } from '../connect-graph/looks.js';
|
|
3
3
|
/** A cosmos.gl graph, as far as the driver calls it. */
|
|
4
4
|
export interface CosmosGraph {
|
|
5
5
|
fitView(duration?: number, padding?: number): void;
|
|
@@ -24,7 +24,7 @@ export interface CosmosGraph {
|
|
|
24
24
|
setPointShapes(pointShapes: Float32Array): void;
|
|
25
25
|
setPointSizes(pointSizes: Float32Array): void;
|
|
26
26
|
setZoomLevel(value: number, duration?: number): void;
|
|
27
|
-
setZoomTransformByPointPositions(positions: Float32Array, duration?: number, scale?: number): void;
|
|
27
|
+
setZoomTransformByPointPositions(positions: Float32Array, duration?: number, scale?: number, padding?: number): void;
|
|
28
28
|
spaceToScreenPosition(spacePosition: [number, number]): [number, number];
|
|
29
29
|
spaceToScreenRadius(spaceRadius: number): number;
|
|
30
30
|
start(alpha?: number): void;
|
|
@@ -40,7 +40,7 @@ export interface CosmosDriverOptions {
|
|
|
40
40
|
* default, past where twenty thousand notes settle on a laptop's GPU.
|
|
41
41
|
*/
|
|
42
42
|
layoutLimit?: number | undefined;
|
|
43
|
-
/** A note's shape where it does not say: `circle` by default, `square` or `diamond`. */
|
|
43
|
+
/** A note's shape where it does not say: `circle` by default, `square` or `diamond`. A `ring` is drawn as a circle. */
|
|
44
44
|
shape?: NodeLook['shape'];
|
|
45
45
|
/** The size of cosmos.gl's space, as its own `spaceSize` says. 4096 by default. */
|
|
46
46
|
spaceSize?: number | undefined;
|
|
@@ -62,6 +62,7 @@ export interface CosmosDriverOptions {
|
|
|
62
62
|
* each group, over the drawing, as the Sigma driver does.
|
|
63
63
|
*
|
|
64
64
|
* **It is `@cosmos.gl/graph`, which is MIT**, and not Cosmograph, which is
|
|
65
|
-
* licensed for noncommercial use only.
|
|
65
|
+
* licensed for noncommercial use only. The graph is the page's: after
|
|
66
|
+
* `disconnect()`, `graph.destroy()` frees it.
|
|
66
67
|
*/
|
|
67
68
|
export declare const cosmosDriver: (graph: CosmosGraph, container: HTMLElement, options?: CosmosDriverOptions) => GraphDriver;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { linkKey } from '../connect-graph/diff.js';
|
|
2
|
+
import { rgbOf } from '../connect-graph/looks.js';
|
|
1
3
|
import { gesturesHeld, groupsOver, namesOver, signalsOver, } from '../overlay/overlay.js';
|
|
2
4
|
const SHAPES = { circle: 0, diamond: 3, ring: 0, square: 1 };
|
|
3
5
|
const STEP = 250;
|
|
@@ -9,8 +11,10 @@ const pixels = (size) => 0.5 * (1.6 + 1.35 * Math.max(size, 0));
|
|
|
9
11
|
const CLUSTER = 0.1;
|
|
10
12
|
const FOLLOW = 1000;
|
|
11
13
|
const PADDING = 0.08;
|
|
14
|
+
const SIGNALED = 2500;
|
|
15
|
+
const still = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
12
16
|
const channels = (color, alpha = 1) => {
|
|
13
|
-
const [red
|
|
17
|
+
const [red, green, blue] = rgbOf(color);
|
|
14
18
|
return [red / 255, green / 255, blue / 255, alpha];
|
|
15
19
|
};
|
|
16
20
|
export const cosmosDriver = (graph, container, options = {}) => {
|
|
@@ -25,6 +29,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
25
29
|
let near = new Set();
|
|
26
30
|
let groupSample = [];
|
|
27
31
|
let following = new Set();
|
|
32
|
+
const signaled = new Map();
|
|
28
33
|
let moved = false;
|
|
29
34
|
let stopped = false;
|
|
30
35
|
let uploaded = 0;
|
|
@@ -51,8 +56,17 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
51
56
|
if (hovered !== undefined) {
|
|
52
57
|
next.add(hovered);
|
|
53
58
|
}
|
|
59
|
+
const now = performance.now();
|
|
54
60
|
for (const at of extra) {
|
|
55
|
-
|
|
61
|
+
signaled.set(at, now + SIGNALED);
|
|
62
|
+
}
|
|
63
|
+
for (const [at, until] of signaled) {
|
|
64
|
+
if (until < now) {
|
|
65
|
+
signaled.delete(at);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
next.add(at);
|
|
69
|
+
}
|
|
56
70
|
}
|
|
57
71
|
if (next.size !== following.size || [...next].some((at) => !following.has(at))) {
|
|
58
72
|
following = next;
|
|
@@ -80,7 +94,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
80
94
|
};
|
|
81
95
|
const held = gesturesHeld(container);
|
|
82
96
|
const signals = signalsOver(container, onScreen);
|
|
83
|
-
const groups = groupsOver(container, onScreen);
|
|
97
|
+
const groups = groupsOver(container, onScreen, () => names.boxes());
|
|
84
98
|
const names = namesOver(container, onScreen);
|
|
85
99
|
const drawNames = () => {
|
|
86
100
|
const lit = hovered === undefined ? undefined : ids[hovered];
|
|
@@ -96,10 +110,11 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
96
110
|
const flatNow = () => live()
|
|
97
111
|
? graph.getPointPositions()
|
|
98
112
|
: ids.flatMap((id) => toSpace(drawnNow.get(id) ?? { x: 0, y: 0 }));
|
|
99
|
-
const fitAll = (
|
|
113
|
+
const fitAll = (asked) => {
|
|
100
114
|
if (stopped) {
|
|
101
115
|
return;
|
|
102
116
|
}
|
|
117
|
+
const duration = still() ? 0 : asked;
|
|
103
118
|
if (!graph.isReady) {
|
|
104
119
|
void graph.ready.then(() => requestAnimationFrame(() => fitAll(duration)));
|
|
105
120
|
return;
|
|
@@ -123,30 +138,59 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
123
138
|
graph.setConfigPartial({ pointSizeScale: by });
|
|
124
139
|
if (Math.abs(clarity() - painted) > 0.04) {
|
|
125
140
|
paintLinks();
|
|
126
|
-
graph.render();
|
|
141
|
+
graph.render(undefined, 0);
|
|
127
142
|
}
|
|
128
143
|
}
|
|
129
144
|
};
|
|
130
145
|
const clarity = () => FAINT + (1 - FAINT) * Math.min(Math.max((grown - 1) / 0.8, 0), 1);
|
|
131
146
|
let painted = 1;
|
|
147
|
+
let linkColors = new Float32Array(0);
|
|
148
|
+
let widths = new Float32Array(0);
|
|
149
|
+
let linksOf = new Map();
|
|
150
|
+
const paintLink = (at, lit) => {
|
|
151
|
+
const link = links[at];
|
|
152
|
+
const look = looks.link(link.source, link.target);
|
|
153
|
+
const touches = lit !== undefined && (link.source === lit || link.target === lit);
|
|
154
|
+
const whole = look.raised || touches;
|
|
155
|
+
linkColors.set(channels(touches ? looks.node(lit).color : look.color, look.hidden ? 0 : whole ? 1 : painted), at * 4);
|
|
156
|
+
widths[at] = whole ? 1.6 : 0.8;
|
|
157
|
+
};
|
|
132
158
|
const paintLinks = () => {
|
|
133
159
|
if (!looks) {
|
|
134
160
|
return;
|
|
135
161
|
}
|
|
136
162
|
painted = clarity();
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
if (linkColors.length !== links.length * 4) {
|
|
164
|
+
linkColors = new Float32Array(links.length * 4);
|
|
165
|
+
widths = new Float32Array(links.length);
|
|
166
|
+
}
|
|
139
167
|
const lit = hovered === undefined ? undefined : ids[hovered];
|
|
140
|
-
links.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
for (let at = 0; at < links.length; at++) {
|
|
169
|
+
paintLink(at, lit);
|
|
170
|
+
}
|
|
171
|
+
graph.setLinkColors(linkColors);
|
|
172
|
+
graph.setLinkWidths(widths);
|
|
173
|
+
};
|
|
174
|
+
const hover = (next) => {
|
|
175
|
+
const was = hovered === undefined ? undefined : ids[hovered];
|
|
176
|
+
hovered = next;
|
|
177
|
+
container.style.cursor = next === undefined ? cursor : 'pointer';
|
|
178
|
+
if (!looks) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const lit = next === undefined ? undefined : ids[next];
|
|
182
|
+
for (const id of [was, lit]) {
|
|
183
|
+
for (const at of id === undefined ? [] : (linksOf.get(id) ?? [])) {
|
|
184
|
+
paintLink(at, lit);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
147
187
|
graph.setLinkColors(linkColors);
|
|
148
188
|
graph.setLinkWidths(widths);
|
|
189
|
+
follow();
|
|
190
|
+
graph.render(undefined, 0);
|
|
191
|
+
redraw();
|
|
149
192
|
};
|
|
193
|
+
const cursor = container.style.cursor;
|
|
150
194
|
const paint = (transition = 0) => {
|
|
151
195
|
if (!looks) {
|
|
152
196
|
return;
|
|
@@ -181,12 +225,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
181
225
|
hoveredPointRingColor: channels(looks.ink, 0.6),
|
|
182
226
|
});
|
|
183
227
|
follow();
|
|
184
|
-
|
|
185
|
-
graph.render(undefined, transition);
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
graph.render();
|
|
189
|
-
}
|
|
228
|
+
graph.render(undefined, still() ? 0 : transition);
|
|
190
229
|
redraw();
|
|
191
230
|
};
|
|
192
231
|
const load = (nodes, next, positions, transition = 0) => {
|
|
@@ -194,7 +233,20 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
194
233
|
index = new Map(ids.map((id, at) => [id, at]));
|
|
195
234
|
drawnNow = new Map(nodes.map((node) => [node.id, node]));
|
|
196
235
|
sizes = nodes.map((node) => pixels(node.size));
|
|
236
|
+
hovered = undefined;
|
|
197
237
|
links = next.filter((link) => index.has(link.source) && index.has(link.target));
|
|
238
|
+
linksOf = new Map();
|
|
239
|
+
links.forEach((link, at) => {
|
|
240
|
+
for (const end of [link.source, link.target]) {
|
|
241
|
+
const list = linksOf.get(end);
|
|
242
|
+
if (list) {
|
|
243
|
+
list.push(at);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
linksOf.set(end, [at]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
198
250
|
const pairs = new Float32Array(links.length * 2);
|
|
199
251
|
links.forEach((link, at) => {
|
|
200
252
|
pairs[at * 2] = index.get(link.source);
|
|
@@ -215,14 +267,8 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
215
267
|
};
|
|
216
268
|
graph.setConfigPartial({
|
|
217
269
|
backgroundColor: [0, 0, 0, 0],
|
|
218
|
-
onPointMouseOut: () =>
|
|
219
|
-
|
|
220
|
-
paint();
|
|
221
|
-
},
|
|
222
|
-
onPointMouseOver: (at) => {
|
|
223
|
-
hovered = at;
|
|
224
|
-
paint();
|
|
225
|
-
},
|
|
270
|
+
onPointMouseOut: () => hover(undefined),
|
|
271
|
+
onPointMouseOver: (at) => hover(at),
|
|
226
272
|
onSimulationTick: redraw,
|
|
227
273
|
onTransition: redraw,
|
|
228
274
|
onZoom: () => {
|
|
@@ -243,11 +289,15 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
243
289
|
center: (id, animate) => {
|
|
244
290
|
const at = index.get(id);
|
|
245
291
|
if (at !== undefined) {
|
|
292
|
+
moved = true;
|
|
246
293
|
graph.zoomToPointByIndex(at, animate ? STEP * 2 : 0, graph.getZoomLevel(), true);
|
|
247
294
|
}
|
|
248
295
|
},
|
|
249
296
|
draw: ({ links: next, nodes }) => {
|
|
250
|
-
|
|
297
|
+
let reach = 1;
|
|
298
|
+
for (const node of nodes) {
|
|
299
|
+
reach = Math.max(reach, Math.abs(node.x), Math.abs(node.y));
|
|
300
|
+
}
|
|
251
301
|
scale = (space * 0.12) / reach;
|
|
252
302
|
moved = false;
|
|
253
303
|
const positions = new Float32Array(nodes.length * 2);
|
|
@@ -259,11 +309,41 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
259
309
|
fitAll(0);
|
|
260
310
|
},
|
|
261
311
|
fit: (fitted, animate) => {
|
|
262
|
-
const
|
|
312
|
+
const flat = flatNow();
|
|
313
|
+
const box = { bottom: -Infinity, left: Infinity, right: -Infinity, top: Infinity };
|
|
314
|
+
for (const id of fitted) {
|
|
263
315
|
const at = index.get(id);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
316
|
+
if (at !== undefined) {
|
|
317
|
+
const x = flat[at * 2] ?? 0;
|
|
318
|
+
const y = flat[at * 2 + 1] ?? 0;
|
|
319
|
+
box.left = Math.min(box.left, x);
|
|
320
|
+
box.right = Math.max(box.right, x);
|
|
321
|
+
box.top = Math.min(box.top, y);
|
|
322
|
+
box.bottom = Math.max(box.bottom, y);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!Number.isFinite(box.left)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
let low = Infinity;
|
|
329
|
+
let high = -Infinity;
|
|
330
|
+
for (let at = 0; at < flat.length; at++) {
|
|
331
|
+
low = Math.min(low, flat[at]);
|
|
332
|
+
high = Math.max(high, flat[at]);
|
|
333
|
+
}
|
|
334
|
+
const least = Math.max((high - low) * 0.2, 1);
|
|
335
|
+
const middle = { x: (box.left + box.right) / 2, y: (box.top + box.bottom) / 2 };
|
|
336
|
+
const half = {
|
|
337
|
+
x: Math.max(box.right - box.left, least) / 2,
|
|
338
|
+
y: Math.max(box.bottom - box.top, least) / 2,
|
|
339
|
+
};
|
|
340
|
+
moved = true;
|
|
341
|
+
graph.setZoomTransformByPointPositions(new Float32Array([
|
|
342
|
+
middle.x - half.x,
|
|
343
|
+
middle.y - half.y,
|
|
344
|
+
middle.x + half.x,
|
|
345
|
+
middle.y + half.y,
|
|
346
|
+
]), animate && !still() ? STEP * 2 : 0, undefined, 0.25);
|
|
267
347
|
},
|
|
268
348
|
gestures: (next) => {
|
|
269
349
|
held.set(next);
|
|
@@ -309,6 +389,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
309
389
|
},
|
|
310
390
|
onPress: (press) => {
|
|
311
391
|
graph.setConfigPartial({
|
|
392
|
+
onBackgroundClick: () => press(undefined),
|
|
312
393
|
onPointClick: (at) => {
|
|
313
394
|
const id = ids[at];
|
|
314
395
|
if (id !== undefined) {
|
|
@@ -316,11 +397,12 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
316
397
|
}
|
|
317
398
|
},
|
|
318
399
|
});
|
|
319
|
-
return () => graph.setConfigPartial({ onPointClick: undefined });
|
|
400
|
+
return () => graph.setConfigPartial({ onBackgroundClick: undefined, onPointClick: undefined });
|
|
320
401
|
},
|
|
321
402
|
panBy: (x, y, animate) => {
|
|
322
403
|
const { height, width } = container.getBoundingClientRect();
|
|
323
404
|
const to = graph.screenToSpacePosition([width / 2 + x, height / 2 + y]);
|
|
405
|
+
moved = true;
|
|
324
406
|
graph.setZoomTransformByPointPositions(new Float32Array(to), animate ? STEP : 0, graph.getZoomLevel());
|
|
325
407
|
},
|
|
326
408
|
settle: (unplaced, placed) => {
|
|
@@ -349,6 +431,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
349
431
|
clearInterval(fitting);
|
|
350
432
|
graph.setConfigPartial({ onSimulationEnd: undefined });
|
|
351
433
|
graph.setPinnedPoints(null);
|
|
434
|
+
graph.pause();
|
|
352
435
|
};
|
|
353
436
|
},
|
|
354
437
|
signal: (from, to, colors, animate) => {
|
|
@@ -359,7 +442,9 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
359
442
|
stop: () => {
|
|
360
443
|
stopped = true;
|
|
361
444
|
clearTimeout(overviewing);
|
|
445
|
+
container.style.cursor = cursor;
|
|
362
446
|
graph.setConfigPartial({
|
|
447
|
+
onBackgroundClick: undefined,
|
|
363
448
|
onPointClick: undefined,
|
|
364
449
|
onPointMouseOut: undefined,
|
|
365
450
|
onPointMouseOver: undefined,
|
|
@@ -396,11 +481,8 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
396
481
|
for (const node of changes.addNodes) {
|
|
397
482
|
kept.set(node.id, node);
|
|
398
483
|
}
|
|
399
|
-
const removed = new Set(changes.removeLinks.map(
|
|
400
|
-
const next = [
|
|
401
|
-
...links.filter((link) => !removed.has(`${link.source}\u0000${link.target}`)),
|
|
402
|
-
...changes.addLinks,
|
|
403
|
-
];
|
|
484
|
+
const removed = new Set(changes.removeLinks.map(linkKey));
|
|
485
|
+
const next = [...links.filter((link) => !removed.has(linkKey(link))), ...changes.addLinks];
|
|
404
486
|
const nodes = [...kept.values()];
|
|
405
487
|
const positions = new Float32Array(nodes.length * 2);
|
|
406
488
|
nodes.forEach((node, at) => {
|
|
@@ -413,6 +495,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
413
495
|
}
|
|
414
496
|
},
|
|
415
497
|
zoomBy: (direction, animate) => {
|
|
498
|
+
moved = true;
|
|
416
499
|
graph.setZoomLevel(graph.getZoomLevel() * (direction > 0 ? ZOOM : 1 / ZOOM), animate ? STEP : 0);
|
|
417
500
|
},
|
|
418
501
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { type ConnectGraphOptions, connectGraph, type DrawnLink, type DrawnNode, type GraphChanges, type GraphConnection, type GraphDriver, type GraphGroup, } from './connect-graph/connect-graph.js';
|
|
2
|
-
export { diffGraph } from './connect-graph/diff.js';
|
|
3
|
-
export { type GraphColors, type GraphLooks, graphColorsFrom, type LinkLook, type NodeLook, } from './connect-graph/looks.js';
|
|
4
|
-
export { type GraphPoint, seedPositions } from './connect-graph/seed.js';
|
|
2
|
+
export { diffGraph, linkKey } from './connect-graph/diff.js';
|
|
3
|
+
export { type GraphColors, type GraphLooks, type GraphTone, graphColorsFrom, type LinkLook, type NodeLook, } from './connect-graph/looks.js';
|
|
4
|
+
export { type GraphPoint, type SeedLink, type SeedNode, seedPositions, } from './connect-graph/seed.js';
|
|
5
5
|
export { fitToStencil, type GraphStencil, type StencilFrame, } from './connect-graph/stencil.js';
|
|
6
|
+
export type { ShapeField } from './shapes/shape-field.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { connectGraph, } from './connect-graph/connect-graph.js';
|
|
2
|
-
export { diffGraph } from './connect-graph/diff.js';
|
|
2
|
+
export { diffGraph, linkKey } from './connect-graph/diff.js';
|
|
3
3
|
export { graphColorsFrom, } from './connect-graph/looks.js';
|
|
4
|
-
export { seedPositions } from './connect-graph/seed.js';
|
|
4
|
+
export { seedPositions, } from './connect-graph/seed.js';
|
|
5
5
|
export { fitToStencil, } from './connect-graph/stencil.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { shapeFromModel } from './model.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { shapeFromModel } from './model.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ShapeField } from '../shapes/shape-field.js';
|
|
2
|
+
import { type TriangleOptions } from '../shapes/triangles.js';
|
|
3
|
+
/**
|
|
4
|
+
* A solid field from a glTF model, the web's standard format for one: a
|
|
5
|
+
* URL to a `.glb` or `.gltf` file, or the file's bytes. Every mesh in it is
|
|
6
|
+
* read, placed as the model places it, and where it has two meshes or more,
|
|
7
|
+
* each is a part of the field, named as the model names it, which a
|
|
8
|
+
* stencil's `regions` sends groups to. A mesh the model leaves unnamed is
|
|
9
|
+
* named as three.js names it, `mesh_0` and on.
|
|
10
|
+
*
|
|
11
|
+
* **three.js reads the file**, imported when this is first called, so a page
|
|
12
|
+
* that draws no model never downloads a loader. It is an optional peer of
|
|
13
|
+
* this package. A model that cannot be fetched or read is an error that
|
|
14
|
+
* names it, the loader's own error as its `cause`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const shapeFromModel: (source: string | ArrayBuffer, options?: TriangleOptions) => Promise<ShapeField>;
|