@fusion.dev/helix-graph 0.128.0 → 0.129.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/dist/connect-graph/connect-graph.d.ts +3 -2
- package/dist/connect-graph/connect-graph.js +7 -0
- package/dist/connect-graph/looks.d.ts +2 -0
- package/dist/connect-graph/looks.js +4 -1
- package/dist/overlay/overlay.d.ts +18 -0
- package/dist/overlay/overlay.js +43 -11
- package/dist/sigma/sigma-driver.js +28 -15
- package/package.json +4 -4
|
@@ -15,8 +15,9 @@ export type { DrawnLink, DrawnNode, GraphChanges } from './diff.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export interface GraphDriver {
|
|
17
17
|
/**
|
|
18
|
-
* Draws the graph, replacing whatever was drawn, where each note is
|
|
19
|
-
*
|
|
18
|
+
* Draws the graph whole, replacing whatever was drawn, where each note is
|
|
19
|
+
* placed, and frames it: the first drawing, a graph the page replaced with
|
|
20
|
+
* one that shares no note, or every change for a driver without `update`.
|
|
20
21
|
*/
|
|
21
22
|
draw(graph: {
|
|
22
23
|
links: DrawnLink[];
|
|
@@ -66,6 +66,7 @@ export const connectGraph = (frame, driver, options = {}) => {
|
|
|
66
66
|
?.trim()
|
|
67
67
|
.replace(/^["']|["']$/g, ''),
|
|
68
68
|
groups: nodes.flatMap((node) => (node.group === undefined ? [] : [node.group])),
|
|
69
|
+
links: links.length,
|
|
69
70
|
near,
|
|
70
71
|
noteOf: (id) => notes.get(id),
|
|
71
72
|
selected,
|
|
@@ -103,6 +104,12 @@ export const connectGraph = (frame, driver, options = {}) => {
|
|
|
103
104
|
cancelAnimationFrame(pending);
|
|
104
105
|
const nodes = graph.nodes ?? [];
|
|
105
106
|
const ids = new Set(nodes.map((node) => node.id));
|
|
107
|
+
if (!first &&
|
|
108
|
+
drawnNodes.size > 0 &&
|
|
109
|
+
ids.size > 0 &&
|
|
110
|
+
![...ids].some((id) => drawnNodes.has(id))) {
|
|
111
|
+
whole = true;
|
|
112
|
+
}
|
|
106
113
|
const nextLinks = new Map();
|
|
107
114
|
for (const { label, source, target } of graph.links ?? []) {
|
|
108
115
|
if (source !== target && ids.has(source) && ids.has(target)) {
|
|
@@ -131,6 +131,8 @@ export declare const looksOf: (options: {
|
|
|
131
131
|
colors: GraphColors;
|
|
132
132
|
font?: string | undefined;
|
|
133
133
|
groups: readonly string[];
|
|
134
|
+
/** How many links the graph draws, which sets how far a link at rest steps back. */
|
|
135
|
+
links?: number | undefined;
|
|
134
136
|
near: ReadonlySet<string>;
|
|
135
137
|
noteOf: (id: string) => NoteFacts | undefined;
|
|
136
138
|
selected: string | undefined;
|
|
@@ -79,6 +79,9 @@ export const looksOf = (options) => {
|
|
|
79
79
|
};
|
|
80
80
|
const focused = selected !== undefined;
|
|
81
81
|
const hidden = (id) => shown !== undefined && !shown.has(id);
|
|
82
|
+
const count = options.links ?? 2000;
|
|
83
|
+
const along = Math.min(Math.max(Math.log(count / 50) / Math.log(2000 / 50), 0), 1);
|
|
84
|
+
const rest = 0.35 + (0.72 - 0.35) * along;
|
|
82
85
|
return {
|
|
83
86
|
colorOf: own,
|
|
84
87
|
font: options.font || 'sans-serif',
|
|
@@ -91,7 +94,7 @@ export const looksOf = (options) => {
|
|
|
91
94
|
? own(selected)
|
|
92
95
|
: focused
|
|
93
96
|
? mixRgb(colors.line, colors.ground, 0.6)
|
|
94
|
-
: mixRgb(own(source), colors.ground,
|
|
97
|
+
: mixRgb(own(source), colors.ground, rest),
|
|
95
98
|
hidden: hidden(source) || hidden(target),
|
|
96
99
|
raised: touches,
|
|
97
100
|
};
|
|
@@ -38,6 +38,24 @@ export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen) =
|
|
|
38
38
|
} | undefined, animate: boolean) => void;
|
|
39
39
|
stop: () => void;
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Where the frame's own controls lie over the drawing, in the drawing's
|
|
43
|
+
* pixels: its zoom buttons, and its list where it opens over the drawing.
|
|
44
|
+
* Names keep clear of them, since a name under a button can be read by
|
|
45
|
+
* nobody. Read from the frame the drawing is in, where there is one.
|
|
46
|
+
*/
|
|
47
|
+
export declare const chromeOver: (container: HTMLElement) => Box[];
|
|
48
|
+
/** Whether a box overlaps none of `others`. */
|
|
49
|
+
export declare const clearOf: (box: Box, others: readonly Box[]) => boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Where a note's name starts, along the line: after the note, or before it
|
|
52
|
+
* where after it would run under the frame's controls or off the drawing.
|
|
53
|
+
* `box` gives the name's box for a start.
|
|
54
|
+
*/
|
|
55
|
+
export declare const nameStart: (at: {
|
|
56
|
+
radius: number;
|
|
57
|
+
x: number;
|
|
58
|
+
}, width: number, box: (start: number) => Box, avoid: readonly Box[], room: number) => number;
|
|
41
59
|
/** A note to name over the drawing: its name, and whether it is the one the reader is on. */
|
|
42
60
|
export interface Named {
|
|
43
61
|
id: string;
|
package/dist/overlay/overlay.js
CHANGED
|
@@ -266,6 +266,40 @@ export const signalsOver = (container, onScreen) => {
|
|
|
266
266
|
},
|
|
267
267
|
};
|
|
268
268
|
};
|
|
269
|
+
export const chromeOver = (container) => {
|
|
270
|
+
const frame = container.closest('helix-graph, helix-headless-graph');
|
|
271
|
+
const parts = frame?.shadowRoot?.querySelectorAll('[part="controls"], [part="panel"]') ?? [];
|
|
272
|
+
const origin = container.getBoundingClientRect();
|
|
273
|
+
const boxes = [];
|
|
274
|
+
for (const part of parts) {
|
|
275
|
+
const at = part.getBoundingClientRect();
|
|
276
|
+
if (at.width > 0 && at.height > 0) {
|
|
277
|
+
boxes.push({
|
|
278
|
+
bottom: at.bottom - origin.top,
|
|
279
|
+
left: at.left - origin.left,
|
|
280
|
+
right: at.right - origin.left,
|
|
281
|
+
top: at.top - origin.top,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return boxes;
|
|
286
|
+
};
|
|
287
|
+
export const clearOf = (box, others) => others.every((other) => box.right < other.left ||
|
|
288
|
+
box.left > other.right ||
|
|
289
|
+
box.bottom < other.top ||
|
|
290
|
+
box.top > other.bottom);
|
|
291
|
+
export const nameStart = (at, width, box, avoid, room) => {
|
|
292
|
+
const after = at.x + at.radius + 4;
|
|
293
|
+
const fits = (start) => {
|
|
294
|
+
const placed = box(start);
|
|
295
|
+
return placed.left >= 0 && placed.right <= room && clearOf(placed, avoid);
|
|
296
|
+
};
|
|
297
|
+
if (fits(after)) {
|
|
298
|
+
return after;
|
|
299
|
+
}
|
|
300
|
+
const before = at.x - at.radius - 4 - width;
|
|
301
|
+
return fits(before) ? before : after;
|
|
302
|
+
};
|
|
269
303
|
export const namesOver = (container, onScreen) => {
|
|
270
304
|
const canvas = layer(container, 'names');
|
|
271
305
|
let taken = [];
|
|
@@ -276,26 +310,24 @@ export const namesOver = (container, onScreen) => {
|
|
|
276
310
|
if (!context || !looks) {
|
|
277
311
|
return;
|
|
278
312
|
}
|
|
279
|
-
taken =
|
|
313
|
+
taken = chromeOver(container);
|
|
280
314
|
for (const { id, name, strong } of named) {
|
|
281
315
|
const at = onScreen(id);
|
|
282
316
|
if (!at || looks.node(id).hidden) {
|
|
283
317
|
continue;
|
|
284
318
|
}
|
|
285
319
|
context.font = `${strong ? 600 : 500} ${strong ? 13 : 12}px ${looks.font}`;
|
|
286
|
-
const
|
|
320
|
+
const width = context.measureText(name).width;
|
|
287
321
|
const y = at.y + 4;
|
|
288
|
-
const
|
|
322
|
+
const boxAt = (start) => ({
|
|
289
323
|
bottom: y + 4,
|
|
290
|
-
left:
|
|
291
|
-
right:
|
|
324
|
+
left: start - 2,
|
|
325
|
+
right: start + width + 2,
|
|
292
326
|
top: y - 13,
|
|
293
|
-
};
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
box.top > other.bottom);
|
|
298
|
-
if (!free && !strong) {
|
|
327
|
+
});
|
|
328
|
+
const x = nameStart(at, width, boxAt, taken, container.clientWidth);
|
|
329
|
+
const box = boxAt(x);
|
|
330
|
+
if (!clearOf(box, taken) && !strong) {
|
|
299
331
|
continue;
|
|
300
332
|
}
|
|
301
333
|
taken.push(box);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ease, gesturesHeld, groupsOver, signalsOver } from '../overlay/overlay.js';
|
|
1
|
+
import { chromeOver, clearOf, ease, gesturesHeld, groupsOver, nameStart, signalsOver, } from '../overlay/overlay.js';
|
|
2
2
|
import { EMPHASES, HelixNodeProgram, MATERIALS, SHAPES } from './node-program.js';
|
|
3
3
|
const SETTINGS = [
|
|
4
4
|
'defaultDrawNodeHover',
|
|
@@ -43,21 +43,23 @@ const hoverOf = (looks) => (context, data, settings) => {
|
|
|
43
43
|
context.fillText(data.label, x, data.y + size / 3);
|
|
44
44
|
};
|
|
45
45
|
const GROW = 300;
|
|
46
|
-
const labelOf = (looks, claim) => (context, data, settings) => {
|
|
46
|
+
const labelOf = (looks, claim, start) => (context, data, settings) => {
|
|
47
47
|
if (!data.label) {
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
const strong = data.strong === true;
|
|
51
51
|
const size = strong ? settings.labelSize : settings.labelSize - 1;
|
|
52
52
|
context.font = `${strong ? 600 : 500} ${size}px ${settings.labelFont}`;
|
|
53
|
-
const
|
|
53
|
+
const width = context.measureText(data.label).width;
|
|
54
54
|
const y = data.y + size / 3;
|
|
55
|
-
const
|
|
55
|
+
const boxAt = (from) => ({
|
|
56
56
|
bottom: y + 4,
|
|
57
|
-
left:
|
|
58
|
-
right:
|
|
57
|
+
left: from - 2,
|
|
58
|
+
right: from + width + 2,
|
|
59
59
|
top: y - size,
|
|
60
|
-
};
|
|
60
|
+
});
|
|
61
|
+
const x = start({ radius: data.size, x: data.x }, width, boxAt);
|
|
62
|
+
const box = boxAt(x);
|
|
61
63
|
if (!claim(box, data.forceLabel === true)) {
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
@@ -113,17 +115,14 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
113
115
|
const naming = options.labels ?? 'hover';
|
|
114
116
|
let taken = [];
|
|
115
117
|
const claim = (box, must) => {
|
|
116
|
-
const free =
|
|
117
|
-
box.left > other.right ||
|
|
118
|
-
box.bottom < other.top ||
|
|
119
|
-
box.top > other.bottom);
|
|
118
|
+
const free = clearOf(box, taken);
|
|
120
119
|
if (free || must) {
|
|
121
120
|
taken.push(box);
|
|
122
121
|
}
|
|
123
122
|
return free || must;
|
|
124
123
|
};
|
|
125
124
|
const clear = () => {
|
|
126
|
-
taken =
|
|
125
|
+
taken = chromeOver(container);
|
|
127
126
|
};
|
|
128
127
|
sigma.on('beforeRender', clear);
|
|
129
128
|
const held = gesturesHeld(container);
|
|
@@ -177,6 +176,7 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
177
176
|
},
|
|
178
177
|
draw: ({ links, nodes }) => {
|
|
179
178
|
graph.clear();
|
|
179
|
+
camera.setState({ ratio: 1, x: 0.5, y: 0.5 });
|
|
180
180
|
for (const node of nodes) {
|
|
181
181
|
graph.addNode(node.id, {
|
|
182
182
|
label: node.label,
|
|
@@ -239,8 +239,8 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
239
239
|
sigma.setSetting('minEdgeThickness', 0.8);
|
|
240
240
|
sigma.setSetting('labelFont', looks.font);
|
|
241
241
|
sigma.setSetting('labelRenderedSizeThreshold', 0);
|
|
242
|
-
sigma.setSetting('labelGridCellSize', 130);
|
|
243
|
-
sigma.setSetting('defaultDrawNodeLabel', labelOf(looks, claim));
|
|
242
|
+
sigma.setSetting('labelGridCellSize', naming === 'all' ? 24 : 130);
|
|
243
|
+
sigma.setSetting('defaultDrawNodeLabel', labelOf(looks, claim, (at, width, box) => nameStart(at, width, box, taken, container.clientWidth)));
|
|
244
244
|
sigma.setSetting('defaultDrawNodeHover', hoverOf(looks));
|
|
245
245
|
sigma.setSetting('nodeReducer', (node, data) => {
|
|
246
246
|
const look = looks.node(node);
|
|
@@ -390,7 +390,7 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
390
390
|
const layout = make(graph);
|
|
391
391
|
layout.start();
|
|
392
392
|
const limit = options.layoutLimit ?? 10000;
|
|
393
|
-
|
|
393
|
+
let started = performance.now();
|
|
394
394
|
let last = positionsOf(graph);
|
|
395
395
|
let done = false;
|
|
396
396
|
let moving = false;
|
|
@@ -408,7 +408,20 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
408
408
|
});
|
|
409
409
|
placed(positionsOf(graph));
|
|
410
410
|
};
|
|
411
|
+
let paused = false;
|
|
411
412
|
const timer = setInterval(() => {
|
|
413
|
+
if (!container.isConnected) {
|
|
414
|
+
if (!paused) {
|
|
415
|
+
paused = true;
|
|
416
|
+
layout.stop();
|
|
417
|
+
}
|
|
418
|
+
started += SAMPLE;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (paused) {
|
|
422
|
+
paused = false;
|
|
423
|
+
layout.start();
|
|
424
|
+
}
|
|
412
425
|
const now = positionsOf(graph);
|
|
413
426
|
let moved = 0;
|
|
414
427
|
let low = Number.POSITIVE_INFINITY;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-graph",
|
|
3
3
|
"description": "Connects the Helix Graph frame to a graph renderer: Sigma.js, cosmos.gl, three.js, or a driver of your own.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.129.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
},
|
|
52
52
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@fusion.dev/helix-core": "0.
|
|
54
|
+
"@fusion.dev/helix-core": "0.129.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@cosmos.gl/graph": "^3.4.2",
|
|
58
|
-
"@fusion.dev/helix-headless": "0.
|
|
59
|
-
"@fusion.dev/helix-testing": "0.
|
|
58
|
+
"@fusion.dev/helix-headless": "0.129.0",
|
|
59
|
+
"@fusion.dev/helix-testing": "0.129.0",
|
|
60
60
|
"@types/bun": "^1.3.0",
|
|
61
61
|
"@types/three": "^0.186.0",
|
|
62
62
|
"d3-force-3d": "^3.0.6",
|