@fusion.dev/helix-graph 0.120.0 → 0.121.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 +141 -8
- package/dist/connect-graph/arrange.d.ts +18 -0
- package/dist/connect-graph/arrange.js +92 -0
- package/dist/connect-graph/connect-graph.d.ts +40 -5
- package/dist/connect-graph/connect-graph.js +81 -18
- package/dist/connect-graph/diff.d.ts +1 -1
- package/dist/connect-graph/looks.d.ts +29 -3
- package/dist/connect-graph/looks.js +24 -6
- package/dist/connect-graph/seed.js +11 -2
- package/dist/connect-graph/stencil.d.ts +34 -0
- package/dist/connect-graph/stencil.js +121 -0
- package/dist/cosmos/cosmos-driver.d.ts +67 -0
- package/dist/cosmos/cosmos-driver.js +467 -0
- package/dist/cosmos/index.d.ts +1 -0
- package/dist/cosmos/index.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/overlay/overlay.d.ts +59 -0
- package/dist/overlay/overlay.js +296 -0
- package/dist/sigma/node-program.d.ts +71 -0
- package/dist/sigma/node-program.js +170 -0
- package/dist/sigma/sigma-driver.d.ts +31 -3
- package/dist/sigma/sigma-driver.js +125 -140
- package/package.json +25 -6
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { gesturesHeld, groupsOver, 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 = document.createElement('canvas');
|
|
85
|
+
names.setAttribute('aria-hidden', 'true');
|
|
86
|
+
names.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none';
|
|
87
|
+
container.append(names);
|
|
88
|
+
const drawNames = () => {
|
|
89
|
+
const context = names.getContext('2d');
|
|
90
|
+
if (!context || !looks) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const ratio = window.devicePixelRatio || 1;
|
|
94
|
+
const width = Math.round(container.clientWidth * ratio);
|
|
95
|
+
const height = Math.round(container.clientHeight * ratio);
|
|
96
|
+
if (names.width !== width || names.height !== height) {
|
|
97
|
+
names.width = width;
|
|
98
|
+
names.height = height;
|
|
99
|
+
}
|
|
100
|
+
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
101
|
+
context.clearRect(0, 0, width, height);
|
|
102
|
+
const taken = [];
|
|
103
|
+
const shown = [selected, hovered === undefined ? undefined : ids[hovered], ...near];
|
|
104
|
+
for (const id of shown) {
|
|
105
|
+
if (id === undefined) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const at = onScreen(id);
|
|
109
|
+
const label = looks.node(id);
|
|
110
|
+
if (!at || label.hidden) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const name = drawnNow.get(id)?.label ?? id;
|
|
114
|
+
const strong = id === selected || id === ids[hovered ?? -1];
|
|
115
|
+
context.font = `${strong ? 600 : 500} ${strong ? 13 : 12}px ${looks.font}`;
|
|
116
|
+
const x = at.x + at.radius + 4;
|
|
117
|
+
const y = at.y + 4;
|
|
118
|
+
const box = {
|
|
119
|
+
bottom: y + 4,
|
|
120
|
+
left: x - 2,
|
|
121
|
+
right: x + context.measureText(name).width + 2,
|
|
122
|
+
top: y - 13,
|
|
123
|
+
};
|
|
124
|
+
const free = taken.every((other) => box.right < other.left ||
|
|
125
|
+
box.left > other.right ||
|
|
126
|
+
box.bottom < other.top ||
|
|
127
|
+
box.top > other.bottom);
|
|
128
|
+
if (!free && !strong) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
taken.push(box);
|
|
132
|
+
context.lineJoin = 'round';
|
|
133
|
+
context.lineWidth = 3;
|
|
134
|
+
context.strokeStyle = looks.ground;
|
|
135
|
+
context.strokeText(name, x, y);
|
|
136
|
+
context.fillStyle = strong ? looks.ink : looks.quiet;
|
|
137
|
+
context.fillText(name, x, y);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const redraw = () => {
|
|
141
|
+
drawNames();
|
|
142
|
+
groups.draw();
|
|
143
|
+
};
|
|
144
|
+
const flatNow = () => live()
|
|
145
|
+
? graph.getPointPositions()
|
|
146
|
+
: ids.flatMap((id) => toSpace(drawnNow.get(id) ?? { x: 0, y: 0 }));
|
|
147
|
+
const fitAll = (duration) => {
|
|
148
|
+
if (stopped) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!graph.isReady) {
|
|
152
|
+
void graph.ready.then(() => requestAnimationFrame(() => fitAll(duration)));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!moved && live()) {
|
|
156
|
+
graph.fitView(duration, PADDING);
|
|
157
|
+
clearTimeout(overviewing);
|
|
158
|
+
overviewing = setTimeout(() => {
|
|
159
|
+
overview = graph.getZoomLevel();
|
|
160
|
+
grow();
|
|
161
|
+
}, duration + 20);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
let overview = 0;
|
|
165
|
+
let overviewing;
|
|
166
|
+
let grown = 1;
|
|
167
|
+
const grow = () => {
|
|
168
|
+
const by = overview > 0 ? Math.min(Math.max(Math.sqrt(graph.getZoomLevel() / overview), 0.7), 2.4) : 1;
|
|
169
|
+
if (Math.abs(by - grown) > 0.01) {
|
|
170
|
+
grown = by;
|
|
171
|
+
graph.setConfigPartial({ pointSizeScale: by });
|
|
172
|
+
if (Math.abs(clarity() - painted) > 0.04) {
|
|
173
|
+
paintLinks();
|
|
174
|
+
graph.render();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const clarity = () => FAINT + (1 - FAINT) * Math.min(Math.max((grown - 1) / 0.8, 0), 1);
|
|
179
|
+
let painted = 1;
|
|
180
|
+
const paintLinks = () => {
|
|
181
|
+
if (!looks) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
painted = clarity();
|
|
185
|
+
const linkColors = new Float32Array(links.length * 4);
|
|
186
|
+
const widths = new Float32Array(links.length);
|
|
187
|
+
const lit = hovered === undefined ? undefined : ids[hovered];
|
|
188
|
+
links.forEach((link, at) => {
|
|
189
|
+
const look = looks.link(link.source, link.target);
|
|
190
|
+
const touches = lit !== undefined && (link.source === lit || link.target === lit);
|
|
191
|
+
const whole = look.raised || touches;
|
|
192
|
+
linkColors.set(channels(touches ? looks.node(lit).color : look.color, look.hidden ? 0 : whole ? 1 : painted), at * 4);
|
|
193
|
+
widths[at] = whole ? 1.6 : 0.8;
|
|
194
|
+
});
|
|
195
|
+
graph.setLinkColors(linkColors);
|
|
196
|
+
graph.setLinkWidths(widths);
|
|
197
|
+
};
|
|
198
|
+
const paint = (transition = 0) => {
|
|
199
|
+
if (!looks) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const colors = new Float32Array(ids.length * 4);
|
|
203
|
+
const drawn = new Float32Array(ids.length);
|
|
204
|
+
const shapes = new Float32Array(ids.length);
|
|
205
|
+
selected = undefined;
|
|
206
|
+
near = new Set();
|
|
207
|
+
ids.forEach((id, at) => {
|
|
208
|
+
const look = looks.node(id);
|
|
209
|
+
const weight = Math.min(Math.max(((drawnNow.get(id)?.size ?? 1) - 1) / 6, 0), 1);
|
|
210
|
+
const alpha = look.selected || look.raised ? 1 : 0.5 + 0.5 * weight;
|
|
211
|
+
colors.set(channels(look.color, look.hidden ? 0 : alpha), at * 4);
|
|
212
|
+
drawn[at] = (sizes[at] ?? 8) * (look.selected ? 1.5 : look.raised ? 1.25 : 1);
|
|
213
|
+
shapes[at] = SHAPES[look.shape ?? options.shape ?? 'circle'];
|
|
214
|
+
if (look.selected) {
|
|
215
|
+
selected = id;
|
|
216
|
+
}
|
|
217
|
+
else if (look.raised) {
|
|
218
|
+
near.add(id);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
graph.setPointColors(colors);
|
|
222
|
+
graph.setPointSizes(drawn);
|
|
223
|
+
graph.setPointShapes(shapes);
|
|
224
|
+
paintLinks();
|
|
225
|
+
const focused = selected === undefined ? undefined : index.get(selected);
|
|
226
|
+
graph.setConfigPartial({
|
|
227
|
+
focusedPointIndex: focused,
|
|
228
|
+
focusedPointRingColor: selected === undefined ? undefined : channels(looks.node(selected).color),
|
|
229
|
+
hoveredPointRingColor: channels(looks.ink, 0.6),
|
|
230
|
+
});
|
|
231
|
+
follow();
|
|
232
|
+
if (transition > 0) {
|
|
233
|
+
graph.render(undefined, transition);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
graph.render();
|
|
237
|
+
}
|
|
238
|
+
redraw();
|
|
239
|
+
};
|
|
240
|
+
const load = (nodes, next, positions, transition = 0) => {
|
|
241
|
+
ids = nodes.map((node) => node.id);
|
|
242
|
+
index = new Map(ids.map((id, at) => [id, at]));
|
|
243
|
+
drawnNow = new Map(nodes.map((node) => [node.id, node]));
|
|
244
|
+
sizes = nodes.map((node) => pixels(node.size));
|
|
245
|
+
links = next.filter((link) => index.has(link.source) && index.has(link.target));
|
|
246
|
+
const pairs = new Float32Array(links.length * 2);
|
|
247
|
+
links.forEach((link, at) => {
|
|
248
|
+
pairs[at * 2] = index.get(link.source);
|
|
249
|
+
pairs[at * 2 + 1] = index.get(link.target);
|
|
250
|
+
});
|
|
251
|
+
graph.setPointPositions(positions, true);
|
|
252
|
+
graph.setLinks(pairs);
|
|
253
|
+
paint(transition);
|
|
254
|
+
uploaded = 0;
|
|
255
|
+
if (graph.isReady) {
|
|
256
|
+
uploaded = ids.length;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
void graph.ready.then(() => {
|
|
260
|
+
uploaded = ids.length;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
graph.setConfigPartial({
|
|
265
|
+
backgroundColor: [0, 0, 0, 0],
|
|
266
|
+
onPointMouseOut: () => {
|
|
267
|
+
hovered = undefined;
|
|
268
|
+
paint();
|
|
269
|
+
},
|
|
270
|
+
onPointMouseOver: (at) => {
|
|
271
|
+
hovered = at;
|
|
272
|
+
paint();
|
|
273
|
+
},
|
|
274
|
+
onSimulationTick: redraw,
|
|
275
|
+
onTransition: redraw,
|
|
276
|
+
onZoom: () => {
|
|
277
|
+
grow();
|
|
278
|
+
redraw();
|
|
279
|
+
},
|
|
280
|
+
onZoomStart: (_, userDriven) => {
|
|
281
|
+
moved ||= userDriven;
|
|
282
|
+
},
|
|
283
|
+
simulationDecay: 600,
|
|
284
|
+
simulationGravity: 1.5,
|
|
285
|
+
simulationLinkDistance: 30,
|
|
286
|
+
simulationLinkSpring: 0.15,
|
|
287
|
+
simulationRepulsion: 30,
|
|
288
|
+
spaceSize: space,
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
center: (id, animate) => {
|
|
292
|
+
const at = index.get(id);
|
|
293
|
+
if (at !== undefined) {
|
|
294
|
+
graph.zoomToPointByIndex(at, animate ? STEP * 2 : 0, graph.getZoomLevel(), true);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
draw: ({ links: next, nodes }) => {
|
|
298
|
+
const reach = Math.max(1, ...nodes.map((node) => Math.max(Math.abs(node.x), Math.abs(node.y))));
|
|
299
|
+
scale = (space * 0.12) / reach;
|
|
300
|
+
moved = false;
|
|
301
|
+
const positions = new Float32Array(nodes.length * 2);
|
|
302
|
+
nodes.forEach((node, at) => {
|
|
303
|
+
positions.set(toSpace(node), at * 2);
|
|
304
|
+
});
|
|
305
|
+
load(nodes, next, positions);
|
|
306
|
+
graph.pause();
|
|
307
|
+
fitAll(0);
|
|
308
|
+
},
|
|
309
|
+
fit: (fitted, animate) => {
|
|
310
|
+
const indices = fitted.flatMap((id) => {
|
|
311
|
+
const at = index.get(id);
|
|
312
|
+
return at === undefined ? [] : [at];
|
|
313
|
+
});
|
|
314
|
+
graph.fitViewByPointIndices(indices, animate ? STEP * 2 : 0, 0.25);
|
|
315
|
+
},
|
|
316
|
+
gestures: (next) => {
|
|
317
|
+
held.set(next);
|
|
318
|
+
},
|
|
319
|
+
groups: (next) => {
|
|
320
|
+
if (options.groupLabels === false) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
groupSample = next.flatMap((group) => group.ids.slice(0, SAMPLE).flatMap((id) => {
|
|
324
|
+
const at = index.get(id);
|
|
325
|
+
return at === undefined ? [] : [at];
|
|
326
|
+
}));
|
|
327
|
+
follow();
|
|
328
|
+
groups.set(next.map((group) => ({ ...group, ids: group.ids.slice(0, SAMPLE) })));
|
|
329
|
+
const cluster = new Map();
|
|
330
|
+
next.forEach((group, at) => {
|
|
331
|
+
for (const id of group.ids) {
|
|
332
|
+
cluster.set(id, at);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const clusters = ids.map((id) => cluster.get(id));
|
|
336
|
+
if (clusters.length === clustered.length &&
|
|
337
|
+
clusters.every((at, position) => at === clustered[position])) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
clustered = clusters;
|
|
341
|
+
graph.setPointClusters(clusters);
|
|
342
|
+
graph.setClusterPositions(next.flatMap((_, at) => {
|
|
343
|
+
const turn = (at / Math.max(next.length, 1)) * Math.PI * 2;
|
|
344
|
+
return [
|
|
345
|
+
space / 2 + Math.cos(turn) * space * 0.2,
|
|
346
|
+
space / 2 + Math.sin(turn) * space * 0.2,
|
|
347
|
+
];
|
|
348
|
+
}));
|
|
349
|
+
graph.setConfigPartial({ simulationCluster: next.length > 1 ? CLUSTER : 0 });
|
|
350
|
+
graph.render();
|
|
351
|
+
},
|
|
352
|
+
look: (next) => {
|
|
353
|
+
looks = next;
|
|
354
|
+
signals.look(next);
|
|
355
|
+
groups.look(next);
|
|
356
|
+
paint();
|
|
357
|
+
},
|
|
358
|
+
onPress: (press) => {
|
|
359
|
+
graph.setConfigPartial({
|
|
360
|
+
onPointClick: (at) => {
|
|
361
|
+
const id = ids[at];
|
|
362
|
+
if (id !== undefined) {
|
|
363
|
+
press(id);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
return () => graph.setConfigPartial({ onPointClick: undefined });
|
|
368
|
+
},
|
|
369
|
+
panBy: (x, y, animate) => {
|
|
370
|
+
const { height, width } = container.getBoundingClientRect();
|
|
371
|
+
const to = graph.screenToSpacePosition([width / 2 + x, height / 2 + y]);
|
|
372
|
+
graph.setZoomTransformByPointPositions(new Float32Array(to), animate ? STEP : 0, graph.getZoomLevel());
|
|
373
|
+
},
|
|
374
|
+
settle: (unplaced, placed) => {
|
|
375
|
+
graph.setPinnedPoints(ids.flatMap((id, at) => (unplaced.has(id) ? [] : [at])));
|
|
376
|
+
let done = false;
|
|
377
|
+
const finish = () => {
|
|
378
|
+
if (done) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
done = true;
|
|
382
|
+
clearTimeout(limit);
|
|
383
|
+
clearInterval(fitting);
|
|
384
|
+
graph.setPinnedPoints(null);
|
|
385
|
+
graph.pause();
|
|
386
|
+
fitAll(STEP * 2);
|
|
387
|
+
const flat = flatNow();
|
|
388
|
+
placed(new Map(ids.map((id, at) => [id, fromSpace(flat[at * 2] ?? 0, flat[at * 2 + 1] ?? 0)])));
|
|
389
|
+
};
|
|
390
|
+
graph.setConfigPartial({ onSimulationEnd: finish });
|
|
391
|
+
graph.start(1);
|
|
392
|
+
const limit = setTimeout(finish, options.layoutLimit ?? 15000);
|
|
393
|
+
const fitting = setInterval(() => fitAll(FOLLOW), FOLLOW);
|
|
394
|
+
return () => {
|
|
395
|
+
done = true;
|
|
396
|
+
clearTimeout(limit);
|
|
397
|
+
clearInterval(fitting);
|
|
398
|
+
graph.setConfigPartial({ onSimulationEnd: undefined });
|
|
399
|
+
graph.setPinnedPoints(null);
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
signal: (from, to, colors, animate) => {
|
|
403
|
+
const ends = [index.get(from), index.get(to)].filter((at) => at !== undefined);
|
|
404
|
+
follow(ends);
|
|
405
|
+
signals.signal(from, to, colors, animate);
|
|
406
|
+
},
|
|
407
|
+
stop: () => {
|
|
408
|
+
stopped = true;
|
|
409
|
+
clearTimeout(overviewing);
|
|
410
|
+
graph.setConfigPartial({
|
|
411
|
+
onPointClick: undefined,
|
|
412
|
+
onPointMouseOut: undefined,
|
|
413
|
+
onPointMouseOver: undefined,
|
|
414
|
+
onSimulationEnd: undefined,
|
|
415
|
+
onSimulationTick: undefined,
|
|
416
|
+
onTransition: undefined,
|
|
417
|
+
onZoom: undefined,
|
|
418
|
+
});
|
|
419
|
+
signals.stop();
|
|
420
|
+
groups.stop();
|
|
421
|
+
names.remove();
|
|
422
|
+
held.stop();
|
|
423
|
+
},
|
|
424
|
+
update: (changes, animate) => {
|
|
425
|
+
const flat = flatNow();
|
|
426
|
+
const kept = new Map();
|
|
427
|
+
ids.forEach((id, at) => {
|
|
428
|
+
const node = drawnNow.get(id);
|
|
429
|
+
if (node) {
|
|
430
|
+
kept.set(id, { ...node, ...fromSpace(flat[at * 2] ?? 0, flat[at * 2 + 1] ?? 0) });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
for (const id of changes.removeNodes) {
|
|
434
|
+
kept.delete(id);
|
|
435
|
+
}
|
|
436
|
+
let shifted = false;
|
|
437
|
+
for (const node of changes.changeNodes) {
|
|
438
|
+
const was = drawnNow.get(node.id);
|
|
439
|
+
const moved = was !== undefined && (was.x !== node.x || was.y !== node.y);
|
|
440
|
+
shifted ||= moved;
|
|
441
|
+
const now = kept.get(node.id);
|
|
442
|
+
kept.set(node.id, moved || !now ? node : { ...node, x: now.x, y: now.y });
|
|
443
|
+
}
|
|
444
|
+
for (const node of changes.addNodes) {
|
|
445
|
+
kept.set(node.id, node);
|
|
446
|
+
}
|
|
447
|
+
const removed = new Set(changes.removeLinks.map((link) => `${link.source}\u0000${link.target}`));
|
|
448
|
+
const next = [
|
|
449
|
+
...links.filter((link) => !removed.has(`${link.source}\u0000${link.target}`)),
|
|
450
|
+
...changes.addLinks,
|
|
451
|
+
];
|
|
452
|
+
const nodes = [...kept.values()];
|
|
453
|
+
const positions = new Float32Array(nodes.length * 2);
|
|
454
|
+
nodes.forEach((node, at) => {
|
|
455
|
+
positions.set(toSpace(node), at * 2);
|
|
456
|
+
});
|
|
457
|
+
const glide = shifted && animate ? MORPH : 0;
|
|
458
|
+
load(nodes, next, positions, glide);
|
|
459
|
+
if (shifted) {
|
|
460
|
+
fitAll(glide);
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
zoomBy: (direction, animate) => {
|
|
464
|
+
graph.setZoomLevel(graph.getZoomLevel() * (direction > 0 ? ZOOM : 1 / ZOOM), animate ? STEP : 0);
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type CosmosDriverOptions, type CosmosGraph, cosmosDriver } from './cosmos-driver.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cosmosDriver } from './cosmos-driver.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { type ConnectGraphOptions, connectGraph, type DrawnLink, type DrawnNode, type GraphChanges, type GraphConnection, type GraphDriver, } from './connect-graph/connect-graph.js';
|
|
1
|
+
export { type ConnectGraphOptions, connectGraph, type DrawnLink, type DrawnNode, type GraphChanges, type GraphConnection, type GraphDriver, type GraphGroup, } from './connect-graph/connect-graph.js';
|
|
2
2
|
export { diffGraph } from './connect-graph/diff.js';
|
|
3
3
|
export { type GraphColors, type GraphLooks, graphColorsFrom, type LinkLook, type NodeLook, } from './connect-graph/looks.js';
|
|
4
4
|
export { type GraphPoint, seedPositions } from './connect-graph/seed.js';
|
|
5
|
+
export { fitToStencil, type GraphStencil } from './connect-graph/stencil.js';
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { connectGraph, } from './connect-graph/connect-graph.js';
|
|
|
2
2
|
export { diffGraph } from './connect-graph/diff.js';
|
|
3
3
|
export { graphColorsFrom, } from './connect-graph/looks.js';
|
|
4
4
|
export { seedPositions } from './connect-graph/seed.js';
|
|
5
|
+
export { fitToStencil } from './connect-graph/stencil.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ViewportGestureMode } from '@fusion.dev/helix-core';
|
|
2
|
+
import type { GraphGroup } from '../connect-graph/connect-graph.js';
|
|
3
|
+
import { type GraphLooks } from '../connect-graph/looks.js';
|
|
4
|
+
/** Where a note is on screen and how large, or nothing where it is not drawn. */
|
|
5
|
+
export type OnScreen = (id: string) => {
|
|
6
|
+
radius: number;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
} | undefined;
|
|
10
|
+
/** Eases in and out, so a signal leaves and arrives gently. */
|
|
11
|
+
export declare const ease: (at: number) => number;
|
|
12
|
+
/** A name's box on screen, in pixels. */
|
|
13
|
+
export interface Box {
|
|
14
|
+
bottom: number;
|
|
15
|
+
left: number;
|
|
16
|
+
right: number;
|
|
17
|
+
top: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* **Signals are drawn on a canvas of the driver's own**, over the renderer's,
|
|
21
|
+
* so a signal never makes the renderer redraw the graph: ten signals a second
|
|
22
|
+
* over ten thousand notes draw ten dots. It draws only while something is in
|
|
23
|
+
* flight, and takes no pointer, so a press reaches the note under it.
|
|
24
|
+
*/
|
|
25
|
+
export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
26
|
+
look: (looks: GraphLooks) => void;
|
|
27
|
+
signal: (from: string, to: string, colors: {
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
} | undefined, animate: boolean) => void;
|
|
31
|
+
stop: () => void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* **Group names are drawn on a canvas of their own**, under the signals', and
|
|
35
|
+
* drawn again whenever the driver says the view moved, so they follow it.
|
|
36
|
+
*/
|
|
37
|
+
export declare const groupsOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
38
|
+
draw: () => void;
|
|
39
|
+
look: (next: GraphLooks) => void;
|
|
40
|
+
set: (next: readonly GraphGroup[]) => void;
|
|
41
|
+
stop: () => void;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* **Held back from the renderer rather than switched off in it.** A
|
|
45
|
+
* renderer's own switches turn its zoom and pan off for code as well, so the
|
|
46
|
+
* frame's buttons stopped working with them. A listener on the container in
|
|
47
|
+
* the capture phase runs before the renderer's, and keeps from it what the
|
|
48
|
+
* page has: the plain wheel and one finger while cooperative, and every
|
|
49
|
+
* gesture while a waiting surface is off.
|
|
50
|
+
*/
|
|
51
|
+
export declare const gesturesHeld: (container: HTMLElement) => {
|
|
52
|
+
/**
|
|
53
|
+
* Which gestures the renderer takes. One finger scrolls the page past a
|
|
54
|
+
* graph that shares its gestures, and every finger does past one that is
|
|
55
|
+
* off.
|
|
56
|
+
*/
|
|
57
|
+
set: (next: ViewportGestureMode) => void;
|
|
58
|
+
stop: () => void;
|
|
59
|
+
};
|