@fusion.dev/helix-graph 0.120.0 → 0.122.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.
@@ -0,0 +1,199 @@
1
+ import { fieldFromInside, fieldFromPath } from './shape-field.js';
2
+ const PROBE = { flat: 64, solid: 32 };
3
+ const SHELL = 0.04;
4
+ const WAVE = 0.07;
5
+ const seeded = (seed) => {
6
+ let state = seed >>> 0 || 1;
7
+ return () => {
8
+ state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
9
+ return state / 4294967296;
10
+ };
11
+ };
12
+ const fields = new WeakMap();
13
+ export const fieldOf = (stencil) => {
14
+ if ('distance' in stencil) {
15
+ return stencil;
16
+ }
17
+ const made = fields.get(stencil);
18
+ if (made) {
19
+ return made;
20
+ }
21
+ const field = 'path' in stencil
22
+ ? fieldFromPath(stencil.path, stencil.width, stencil.height)
23
+ : fieldFromInside((x, y) => stencil.inside(x, y), stencil);
24
+ fields.set(stencil, field);
25
+ return field;
26
+ };
27
+ export const placesIn = (field, count, { place = 'fill', shell = SHELL } = {}) => {
28
+ const solid = field.depth > 0;
29
+ const { depth, height, width } = field;
30
+ const thickness = shell * Math.max(width, height, depth);
31
+ const takes = (x, y, z) => {
32
+ const distance = field.distance(x, y, z);
33
+ return distance < 0 && (place === 'fill' || distance > -thickness);
34
+ };
35
+ const probe = solid ? PROBE.solid : PROBE.flat;
36
+ let taken = 0;
37
+ for (let layer = 0; layer < (solid ? probe : 1); layer++) {
38
+ for (let row = 0; row < probe; row++) {
39
+ for (let column = 0; column < probe; column++) {
40
+ if (takes(((column + 0.5) / probe) * width, ((row + 0.5) / probe) * height, solid ? ((layer + 0.5) / probe) * depth : 0)) {
41
+ taken += 1;
42
+ }
43
+ }
44
+ }
45
+ }
46
+ if (count === 0 || taken === 0) {
47
+ return [];
48
+ }
49
+ const random = seeded(count);
50
+ const room = (taken / probe ** (solid ? 3 : 2)) * width * height * (solid ? depth : 1);
51
+ let step = (room / count) ** (1 / (solid ? 3 : 2));
52
+ for (let tries = 0; tries < 8; tries++) {
53
+ const places = [];
54
+ const layers = solid ? Math.ceil(depth / step) : 1;
55
+ for (let layer = 0; layer < layers; layer++) {
56
+ for (let y = step / 2; y < height; y += step) {
57
+ for (let x = step / 2; x < width; x += step) {
58
+ const place = {
59
+ x: x + (random() - 0.5) * step * 0.8,
60
+ y: y + (random() - 0.5) * step * 0.8,
61
+ ...(solid ? { z: (layer + 0.5 + (random() - 0.5) * 0.8) * step } : {}),
62
+ };
63
+ if (takes(place.x, place.y, place.z ?? 0)) {
64
+ places.push(place);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ if (places.length >= count) {
70
+ for (let at = places.length - 1; at > 0; at--) {
71
+ const other = Math.floor(random() * (at + 1));
72
+ [places[at], places[other]] = [places[other], places[at]];
73
+ }
74
+ return places.slice(0, count);
75
+ }
76
+ step *= (places.length / count) ** (1 / (solid ? 3 : 2)) * 0.97 || 0.5;
77
+ }
78
+ return [];
79
+ };
80
+ const nested = (items, keys) => {
81
+ const [key, ...rest] = keys;
82
+ if (!key) {
83
+ return items;
84
+ }
85
+ const sorted = [...items].sort((one, other) => key(one) - key(other));
86
+ if (rest.length === 0) {
87
+ return sorted;
88
+ }
89
+ const slice = Math.ceil(items.length ** (rest.length / keys.length));
90
+ const out = [];
91
+ for (let from = 0; from < sorted.length; from += slice) {
92
+ out.push(...nested(sorted.slice(from, from + slice), rest));
93
+ }
94
+ return out;
95
+ };
96
+ export const fitToStencil = (positions, stencil) => {
97
+ const notes = [...positions].map(([id, at]) => ({ at, id }));
98
+ if (notes.length === 0) {
99
+ return new Map();
100
+ }
101
+ const field = fieldOf(stencil);
102
+ const places = placesIn(field, notes.length, {
103
+ place: stencil.place ?? 'fill',
104
+ shell: stencil.shell ?? SHELL,
105
+ });
106
+ if (places.length < notes.length) {
107
+ return new Map(positions);
108
+ }
109
+ const solid = field.depth > 0;
110
+ const low = {
111
+ x: Number.POSITIVE_INFINITY,
112
+ y: Number.POSITIVE_INFINITY,
113
+ z: Number.POSITIVE_INFINITY,
114
+ };
115
+ const high = {
116
+ x: Number.NEGATIVE_INFINITY,
117
+ y: Number.NEGATIVE_INFINITY,
118
+ z: Number.NEGATIVE_INFINITY,
119
+ };
120
+ for (const { at } of notes) {
121
+ for (const axis of ['x', 'y', 'z']) {
122
+ const value = at[axis] ?? 0;
123
+ low[axis] = Math.min(low[axis], value);
124
+ high[axis] = Math.max(high[axis], value);
125
+ }
126
+ }
127
+ const { depth, height, width } = field;
128
+ const scale = Math.max(high.x - low.x, high.y - low.y, solid ? high.z - low.z : 0, 1) /
129
+ Math.max(width, height, depth);
130
+ const middle = { x: (low.x + high.x) / 2, y: (low.y + high.y) / 2, z: (low.z + high.z) / 2 };
131
+ const spots = places.map((place) => {
132
+ const z = place.z ?? depth / 2;
133
+ return {
134
+ across: place.x + WAVE * width * Math.sin((place.y / height) * Math.PI * 3 + 1.3),
135
+ at: {
136
+ x: middle.x + (place.x - width / 2) * scale,
137
+ y: middle.y - (place.y - height / 2) * scale,
138
+ ...(solid ? { z: middle.z + (z - depth / 2) * scale } : {}),
139
+ },
140
+ down: -place.y + WAVE * height * Math.sin((place.x / width) * Math.PI * 3 + 0.4),
141
+ };
142
+ });
143
+ const fitted = new Map();
144
+ if (solid) {
145
+ const surface = (stencil.place ?? 'fill') === 'surface';
146
+ const way = (point) => {
147
+ if (!surface) {
148
+ return point;
149
+ }
150
+ const x = point.x - middle.x;
151
+ const y = point.y - middle.y;
152
+ const z = (point.z ?? 0) - middle.z;
153
+ const length = Math.hypot(x, y, z) || 1;
154
+ return { x: x / length, y: y / length, z: z / length };
155
+ };
156
+ const widest = (points) => {
157
+ let best = 'x';
158
+ let spread = Number.NEGATIVE_INFINITY;
159
+ for (const axis of ['x', 'y', 'z']) {
160
+ let low = Number.POSITIVE_INFINITY;
161
+ let high = Number.NEGATIVE_INFINITY;
162
+ for (const point of points) {
163
+ const value = point[axis] ?? 0;
164
+ low = Math.min(low, value);
165
+ high = Math.max(high, value);
166
+ }
167
+ if (high - low > spread) {
168
+ spread = high - low;
169
+ best = axis;
170
+ }
171
+ }
172
+ return best;
173
+ };
174
+ const halve = (ours, theirs) => {
175
+ if (ours.length === 1) {
176
+ fitted.set(ours[0].id, theirs[0].at);
177
+ return;
178
+ }
179
+ if (ours.length === 0) {
180
+ return;
181
+ }
182
+ const mine = widest(ours.map((note) => note.key));
183
+ const yours = widest(theirs.map((spot) => spot.key));
184
+ ours.sort((one, other) => (one.key[mine] ?? 0) - (other.key[mine] ?? 0));
185
+ theirs.sort((one, other) => (one.key[yours] ?? 0) - (other.key[yours] ?? 0));
186
+ const half = Math.floor(ours.length / 2);
187
+ halve(ours.slice(0, half), theirs.slice(0, half));
188
+ halve(ours.slice(half), theirs.slice(half));
189
+ };
190
+ halve(notes.map((note) => ({ ...note, key: way(note.at) })), spots.map((spot) => ({ at: spot.at, key: way(spot.at) })));
191
+ return fitted;
192
+ }
193
+ const ours = nested(notes, [(note) => note.at.x, (note) => note.at.y]);
194
+ const theirs = nested(spots, [(spot) => spot.across, (spot) => spot.down]);
195
+ ours.forEach((note, at) => {
196
+ fitted.set(note.id, theirs[at].at);
197
+ });
198
+ return fitted;
199
+ };
@@ -0,0 +1,67 @@
1
+ import type { GraphDriver } from '../connect-graph/connect-graph.js';
2
+ import type { NodeLook } from '../connect-graph/looks.js';
3
+ /** A cosmos.gl graph, as far as the driver calls it. */
4
+ export interface CosmosGraph {
5
+ fitView(duration?: number, padding?: number): void;
6
+ fitViewByPointIndices(indices: number[], duration?: number, padding?: number): void;
7
+ getPointPositions(): number[];
8
+ getTrackedPointPositionsMap(): ReadonlyMap<number, [number, number]>;
9
+ getZoomLevel(): number;
10
+ readonly isReady: boolean;
11
+ readonly ready: Promise<void>;
12
+ pause(): void;
13
+ render(simulationAlpha?: number, transitionDuration?: number): void;
14
+ screenToSpacePosition(screenPosition: [number, number]): [number, number];
15
+ setConfigPartial(config: Record<string, unknown>): void;
16
+ setLinkColors(linkColors: Float32Array): void;
17
+ setLinks(links: Float32Array): void;
18
+ setLinkWidths(linkWidths: Float32Array): void;
19
+ setClusterPositions(clusterPositions: (number | undefined)[]): void;
20
+ setPinnedPoints(pinnedIndices: number[] | null): void;
21
+ setPointClusters(pointClusters: (number | undefined)[]): void;
22
+ setPointColors(pointColors: Float32Array): void;
23
+ setPointPositions(pointPositions: Float32Array, dontRescale?: boolean): void;
24
+ setPointShapes(pointShapes: Float32Array): void;
25
+ setPointSizes(pointSizes: Float32Array): void;
26
+ setZoomLevel(value: number, duration?: number): void;
27
+ setZoomTransformByPointPositions(positions: Float32Array, duration?: number, scale?: number): void;
28
+ spaceToScreenPosition(spacePosition: [number, number]): [number, number];
29
+ spaceToScreenRadius(spaceRadius: number): number;
30
+ start(alpha?: number): void;
31
+ trackPointPositionsByIndices(indices: number[]): void;
32
+ zoomToPointByIndex(index: number, duration?: number, scale?: number, canZoomOut?: boolean): void;
33
+ }
34
+ /** How the cosmos.gl driver behaves where the page does not say. */
35
+ export interface CosmosDriverOptions {
36
+ /** Whether each group's name is drawn over its notes. On by default. */
37
+ groupLabels?: boolean | undefined;
38
+ /**
39
+ * The longest a layout runs, in milliseconds, settled or not. 15 seconds by
40
+ * default, past where twenty thousand notes settle on a laptop's GPU.
41
+ */
42
+ layoutLimit?: number | undefined;
43
+ /** A note's shape where it does not say: `circle` by default, `square` or `diamond`. */
44
+ shape?: NodeLook['shape'];
45
+ /** The size of cosmos.gl's space, as its own `spaceSize` says. 4096 by default. */
46
+ spaceSize?: number | undefined;
47
+ }
48
+ /**
49
+ * Drives cosmos.gl for a Graph frame: the layout and the drawing both run on
50
+ * the GPU, which is the renderer to reach for past fifty thousand notes. Make
51
+ * the graph in a box inside the frame and pass both here, and
52
+ * `connectGraph` fills it:
53
+ *
54
+ * ```js
55
+ * const graph = new Graph(box, { enableSimulation: true })
56
+ * connectGraph(frame, cosmosDriver(graph, box))
57
+ * ```
58
+ *
59
+ * **cosmos.gl draws notes with its own shaders**, so a note's shape is
60
+ * honored and its `material` and `emphasis` are not, and it names nothing:
61
+ * the driver names the hovered note, the selected one and its neighbors, and
62
+ * each group, over the drawing, as the Sigma driver does.
63
+ *
64
+ * **It is `@cosmos.gl/graph`, which is MIT**, and not Cosmograph, which is
65
+ * licensed for noncommercial use only.
66
+ */
67
+ export declare const cosmosDriver: (graph: CosmosGraph, container: HTMLElement, options?: CosmosDriverOptions) => GraphDriver;
@@ -0,0 +1,419 @@
1
+ import { gesturesHeld, groupsOver, namesOver, signalsOver, } from '../overlay/overlay.js';
2
+ const SHAPES = { circle: 0, diamond: 3, ring: 0, square: 1 };
3
+ const STEP = 250;
4
+ const FAINT = 0.16;
5
+ const MORPH = 1400;
6
+ const ZOOM = 1.5;
7
+ const SAMPLE = 24;
8
+ const pixels = (size) => 0.5 * (1.6 + 1.35 * Math.max(size, 0));
9
+ const CLUSTER = 0.1;
10
+ const FOLLOW = 1000;
11
+ const PADDING = 0.08;
12
+ const channels = (color, alpha = 1) => {
13
+ const [red = 128, green = 128, blue = 128] = color.match(/[\d.]+/g)?.map(Number) ?? [];
14
+ return [red / 255, green / 255, blue / 255, alpha];
15
+ };
16
+ export const cosmosDriver = (graph, container, options = {}) => {
17
+ let ids = [];
18
+ let index = new Map();
19
+ let drawnNow = new Map();
20
+ let links = [];
21
+ let sizes = [];
22
+ let looks;
23
+ let hovered;
24
+ let selected;
25
+ let near = new Set();
26
+ let groupSample = [];
27
+ let following = new Set();
28
+ let moved = false;
29
+ let stopped = false;
30
+ let uploaded = 0;
31
+ const live = () => graph.isReady && uploaded > 0 && !stopped;
32
+ let clustered = [];
33
+ const space = options.spaceSize ?? 4096;
34
+ let scale = 1;
35
+ const toSpace = (point) => [
36
+ space / 2 + point.x * scale,
37
+ space / 2 + point.y * scale,
38
+ ];
39
+ const fromSpace = (x, y) => ({
40
+ x: (x - space / 2) / scale,
41
+ y: (y - space / 2) / scale,
42
+ });
43
+ const follow = (extra = []) => {
44
+ const next = new Set(groupSample);
45
+ for (const id of [selected, ...near]) {
46
+ const at = id === undefined ? undefined : index.get(id);
47
+ if (at !== undefined) {
48
+ next.add(at);
49
+ }
50
+ }
51
+ if (hovered !== undefined) {
52
+ next.add(hovered);
53
+ }
54
+ for (const at of extra) {
55
+ next.add(at);
56
+ }
57
+ if (next.size !== following.size || [...next].some((at) => !following.has(at))) {
58
+ following = next;
59
+ graph.trackPointPositionsByIndices([...next]);
60
+ }
61
+ };
62
+ let tracked = new Map();
63
+ let trackedAt = Number.NEGATIVE_INFINITY;
64
+ const positionsNow = () => {
65
+ const now = performance.now();
66
+ if (now - trackedAt > 8 && live()) {
67
+ tracked = graph.getTrackedPointPositionsMap();
68
+ trackedAt = now;
69
+ }
70
+ return tracked;
71
+ };
72
+ const onScreen = (id) => {
73
+ const at = index.get(id);
74
+ const point = at === undefined ? undefined : positionsNow().get(at);
75
+ if (!point || at === undefined) {
76
+ return undefined;
77
+ }
78
+ const [x, y] = graph.spaceToScreenPosition(point);
79
+ return { radius: (sizes[at] ?? 8) / 2, x, y };
80
+ };
81
+ const held = gesturesHeld(container);
82
+ const signals = signalsOver(container, onScreen);
83
+ const groups = groupsOver(container, onScreen);
84
+ const names = namesOver(container, onScreen);
85
+ const drawNames = () => {
86
+ const lit = hovered === undefined ? undefined : ids[hovered];
87
+ const shown = [...new Set([selected, lit, ...near])].flatMap((id) => id === undefined
88
+ ? []
89
+ : [{ id, name: drawnNow.get(id)?.label ?? id, strong: id === selected || id === lit }]);
90
+ names.draw(looks, shown);
91
+ };
92
+ const redraw = () => {
93
+ drawNames();
94
+ groups.draw();
95
+ };
96
+ const flatNow = () => live()
97
+ ? graph.getPointPositions()
98
+ : ids.flatMap((id) => toSpace(drawnNow.get(id) ?? { x: 0, y: 0 }));
99
+ const fitAll = (duration) => {
100
+ if (stopped) {
101
+ return;
102
+ }
103
+ if (!graph.isReady) {
104
+ void graph.ready.then(() => requestAnimationFrame(() => fitAll(duration)));
105
+ return;
106
+ }
107
+ if (!moved && live()) {
108
+ graph.fitView(duration, PADDING);
109
+ clearTimeout(overviewing);
110
+ overviewing = setTimeout(() => {
111
+ overview = graph.getZoomLevel();
112
+ grow();
113
+ }, duration + 20);
114
+ }
115
+ };
116
+ let overview = 0;
117
+ let overviewing;
118
+ let grown = 1;
119
+ const grow = () => {
120
+ const by = overview > 0 ? Math.min(Math.max(Math.sqrt(graph.getZoomLevel() / overview), 0.7), 2.4) : 1;
121
+ if (Math.abs(by - grown) > 0.01) {
122
+ grown = by;
123
+ graph.setConfigPartial({ pointSizeScale: by });
124
+ if (Math.abs(clarity() - painted) > 0.04) {
125
+ paintLinks();
126
+ graph.render();
127
+ }
128
+ }
129
+ };
130
+ const clarity = () => FAINT + (1 - FAINT) * Math.min(Math.max((grown - 1) / 0.8, 0), 1);
131
+ let painted = 1;
132
+ const paintLinks = () => {
133
+ if (!looks) {
134
+ return;
135
+ }
136
+ painted = clarity();
137
+ const linkColors = new Float32Array(links.length * 4);
138
+ const widths = new Float32Array(links.length);
139
+ const lit = hovered === undefined ? undefined : ids[hovered];
140
+ links.forEach((link, at) => {
141
+ const look = looks.link(link.source, link.target);
142
+ const touches = lit !== undefined && (link.source === lit || link.target === lit);
143
+ const whole = look.raised || touches;
144
+ linkColors.set(channels(touches ? looks.node(lit).color : look.color, look.hidden ? 0 : whole ? 1 : painted), at * 4);
145
+ widths[at] = whole ? 1.6 : 0.8;
146
+ });
147
+ graph.setLinkColors(linkColors);
148
+ graph.setLinkWidths(widths);
149
+ };
150
+ const paint = (transition = 0) => {
151
+ if (!looks) {
152
+ return;
153
+ }
154
+ const colors = new Float32Array(ids.length * 4);
155
+ const drawn = new Float32Array(ids.length);
156
+ const shapes = new Float32Array(ids.length);
157
+ selected = undefined;
158
+ near = new Set();
159
+ ids.forEach((id, at) => {
160
+ const look = looks.node(id);
161
+ const weight = Math.min(Math.max(((drawnNow.get(id)?.size ?? 1) - 1) / 6, 0), 1);
162
+ const alpha = look.selected || look.raised ? 1 : 0.5 + 0.5 * weight;
163
+ colors.set(channels(look.color, look.hidden ? 0 : alpha), at * 4);
164
+ drawn[at] = (sizes[at] ?? 8) * (look.selected ? 1.5 : look.raised ? 1.25 : 1);
165
+ shapes[at] = SHAPES[look.shape ?? options.shape ?? 'circle'];
166
+ if (look.selected) {
167
+ selected = id;
168
+ }
169
+ else if (look.raised) {
170
+ near.add(id);
171
+ }
172
+ });
173
+ graph.setPointColors(colors);
174
+ graph.setPointSizes(drawn);
175
+ graph.setPointShapes(shapes);
176
+ paintLinks();
177
+ const focused = selected === undefined ? undefined : index.get(selected);
178
+ graph.setConfigPartial({
179
+ focusedPointIndex: focused,
180
+ focusedPointRingColor: selected === undefined ? undefined : channels(looks.node(selected).color),
181
+ hoveredPointRingColor: channels(looks.ink, 0.6),
182
+ });
183
+ follow();
184
+ if (transition > 0) {
185
+ graph.render(undefined, transition);
186
+ }
187
+ else {
188
+ graph.render();
189
+ }
190
+ redraw();
191
+ };
192
+ const load = (nodes, next, positions, transition = 0) => {
193
+ ids = nodes.map((node) => node.id);
194
+ index = new Map(ids.map((id, at) => [id, at]));
195
+ drawnNow = new Map(nodes.map((node) => [node.id, node]));
196
+ sizes = nodes.map((node) => pixels(node.size));
197
+ links = next.filter((link) => index.has(link.source) && index.has(link.target));
198
+ const pairs = new Float32Array(links.length * 2);
199
+ links.forEach((link, at) => {
200
+ pairs[at * 2] = index.get(link.source);
201
+ pairs[at * 2 + 1] = index.get(link.target);
202
+ });
203
+ graph.setPointPositions(positions, true);
204
+ graph.setLinks(pairs);
205
+ paint(transition);
206
+ uploaded = 0;
207
+ if (graph.isReady) {
208
+ uploaded = ids.length;
209
+ }
210
+ else {
211
+ void graph.ready.then(() => {
212
+ uploaded = ids.length;
213
+ });
214
+ }
215
+ };
216
+ graph.setConfigPartial({
217
+ backgroundColor: [0, 0, 0, 0],
218
+ onPointMouseOut: () => {
219
+ hovered = undefined;
220
+ paint();
221
+ },
222
+ onPointMouseOver: (at) => {
223
+ hovered = at;
224
+ paint();
225
+ },
226
+ onSimulationTick: redraw,
227
+ onTransition: redraw,
228
+ onZoom: () => {
229
+ grow();
230
+ redraw();
231
+ },
232
+ onZoomStart: (_, userDriven) => {
233
+ moved ||= userDriven;
234
+ },
235
+ simulationDecay: 600,
236
+ simulationGravity: 1.5,
237
+ simulationLinkDistance: 30,
238
+ simulationLinkSpring: 0.15,
239
+ simulationRepulsion: 30,
240
+ spaceSize: space,
241
+ });
242
+ return {
243
+ center: (id, animate) => {
244
+ const at = index.get(id);
245
+ if (at !== undefined) {
246
+ graph.zoomToPointByIndex(at, animate ? STEP * 2 : 0, graph.getZoomLevel(), true);
247
+ }
248
+ },
249
+ draw: ({ links: next, nodes }) => {
250
+ const reach = Math.max(1, ...nodes.map((node) => Math.max(Math.abs(node.x), Math.abs(node.y))));
251
+ scale = (space * 0.12) / reach;
252
+ moved = false;
253
+ const positions = new Float32Array(nodes.length * 2);
254
+ nodes.forEach((node, at) => {
255
+ positions.set(toSpace(node), at * 2);
256
+ });
257
+ load(nodes, next, positions);
258
+ graph.pause();
259
+ fitAll(0);
260
+ },
261
+ fit: (fitted, animate) => {
262
+ const indices = fitted.flatMap((id) => {
263
+ const at = index.get(id);
264
+ return at === undefined ? [] : [at];
265
+ });
266
+ graph.fitViewByPointIndices(indices, animate ? STEP * 2 : 0, 0.25);
267
+ },
268
+ gestures: (next) => {
269
+ held.set(next);
270
+ },
271
+ groups: (next) => {
272
+ if (options.groupLabels === false) {
273
+ return;
274
+ }
275
+ groupSample = next.flatMap((group) => group.ids.slice(0, SAMPLE).flatMap((id) => {
276
+ const at = index.get(id);
277
+ return at === undefined ? [] : [at];
278
+ }));
279
+ follow();
280
+ groups.set(next.map((group) => ({ ...group, ids: group.ids.slice(0, SAMPLE) })));
281
+ const cluster = new Map();
282
+ next.forEach((group, at) => {
283
+ for (const id of group.ids) {
284
+ cluster.set(id, at);
285
+ }
286
+ });
287
+ const clusters = ids.map((id) => cluster.get(id));
288
+ if (clusters.length === clustered.length &&
289
+ clusters.every((at, position) => at === clustered[position])) {
290
+ return;
291
+ }
292
+ clustered = clusters;
293
+ graph.setPointClusters(clusters);
294
+ graph.setClusterPositions(next.flatMap((_, at) => {
295
+ const turn = (at / Math.max(next.length, 1)) * Math.PI * 2;
296
+ return [
297
+ space / 2 + Math.cos(turn) * space * 0.2,
298
+ space / 2 + Math.sin(turn) * space * 0.2,
299
+ ];
300
+ }));
301
+ graph.setConfigPartial({ simulationCluster: next.length > 1 ? CLUSTER : 0 });
302
+ graph.render();
303
+ },
304
+ look: (next) => {
305
+ looks = next;
306
+ signals.look(next);
307
+ groups.look(next);
308
+ paint();
309
+ },
310
+ onPress: (press) => {
311
+ graph.setConfigPartial({
312
+ onPointClick: (at) => {
313
+ const id = ids[at];
314
+ if (id !== undefined) {
315
+ press(id);
316
+ }
317
+ },
318
+ });
319
+ return () => graph.setConfigPartial({ onPointClick: undefined });
320
+ },
321
+ panBy: (x, y, animate) => {
322
+ const { height, width } = container.getBoundingClientRect();
323
+ const to = graph.screenToSpacePosition([width / 2 + x, height / 2 + y]);
324
+ graph.setZoomTransformByPointPositions(new Float32Array(to), animate ? STEP : 0, graph.getZoomLevel());
325
+ },
326
+ settle: (unplaced, placed) => {
327
+ graph.setPinnedPoints(ids.flatMap((id, at) => (unplaced.has(id) ? [] : [at])));
328
+ let done = false;
329
+ const finish = () => {
330
+ if (done) {
331
+ return;
332
+ }
333
+ done = true;
334
+ clearTimeout(limit);
335
+ clearInterval(fitting);
336
+ graph.setPinnedPoints(null);
337
+ graph.pause();
338
+ fitAll(STEP * 2);
339
+ const flat = flatNow();
340
+ placed(new Map(ids.map((id, at) => [id, fromSpace(flat[at * 2] ?? 0, flat[at * 2 + 1] ?? 0)])));
341
+ };
342
+ graph.setConfigPartial({ onSimulationEnd: finish });
343
+ graph.start(1);
344
+ const limit = setTimeout(finish, options.layoutLimit ?? 15000);
345
+ const fitting = setInterval(() => fitAll(FOLLOW), FOLLOW);
346
+ return () => {
347
+ done = true;
348
+ clearTimeout(limit);
349
+ clearInterval(fitting);
350
+ graph.setConfigPartial({ onSimulationEnd: undefined });
351
+ graph.setPinnedPoints(null);
352
+ };
353
+ },
354
+ signal: (from, to, colors, animate) => {
355
+ const ends = [index.get(from), index.get(to)].filter((at) => at !== undefined);
356
+ follow(ends);
357
+ signals.signal(from, to, colors, animate);
358
+ },
359
+ stop: () => {
360
+ stopped = true;
361
+ clearTimeout(overviewing);
362
+ graph.setConfigPartial({
363
+ onPointClick: undefined,
364
+ onPointMouseOut: undefined,
365
+ onPointMouseOver: undefined,
366
+ onSimulationEnd: undefined,
367
+ onSimulationTick: undefined,
368
+ onTransition: undefined,
369
+ onZoom: undefined,
370
+ });
371
+ signals.stop();
372
+ groups.stop();
373
+ names.stop();
374
+ held.stop();
375
+ },
376
+ update: (changes, animate) => {
377
+ const flat = flatNow();
378
+ const kept = new Map();
379
+ ids.forEach((id, at) => {
380
+ const node = drawnNow.get(id);
381
+ if (node) {
382
+ kept.set(id, { ...node, ...fromSpace(flat[at * 2] ?? 0, flat[at * 2 + 1] ?? 0) });
383
+ }
384
+ });
385
+ for (const id of changes.removeNodes) {
386
+ kept.delete(id);
387
+ }
388
+ let shifted = false;
389
+ for (const node of changes.changeNodes) {
390
+ const was = drawnNow.get(node.id);
391
+ const moved = was !== undefined && (was.x !== node.x || was.y !== node.y);
392
+ shifted ||= moved;
393
+ const now = kept.get(node.id);
394
+ kept.set(node.id, moved || !now ? node : { ...node, x: now.x, y: now.y });
395
+ }
396
+ for (const node of changes.addNodes) {
397
+ kept.set(node.id, node);
398
+ }
399
+ const removed = new Set(changes.removeLinks.map((link) => `${link.source}\u0000${link.target}`));
400
+ const next = [
401
+ ...links.filter((link) => !removed.has(`${link.source}\u0000${link.target}`)),
402
+ ...changes.addLinks,
403
+ ];
404
+ const nodes = [...kept.values()];
405
+ const positions = new Float32Array(nodes.length * 2);
406
+ nodes.forEach((node, at) => {
407
+ positions.set(toSpace(node), at * 2);
408
+ });
409
+ const glide = shifted && animate ? MORPH : 0;
410
+ load(nodes, next, positions, glide);
411
+ if (shifted) {
412
+ fitAll(glide);
413
+ }
414
+ },
415
+ zoomBy: (direction, animate) => {
416
+ graph.setZoomLevel(graph.getZoomLevel() * (direction > 0 ? ZOOM : 1 / ZOOM), animate ? STEP : 0);
417
+ },
418
+ };
419
+ };
@@ -0,0 +1 @@
1
+ export { type CosmosDriverOptions, type CosmosGraph, cosmosDriver } from './cosmos-driver.js';
@@ -0,0 +1 @@
1
+ export { cosmosDriver } from './cosmos-driver.js';