@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.
@@ -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 placed.
19
- * Keep the view where it is: a note added to a graph is no reason to move.
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, 0.72),
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;
@@ -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 x = at.x + at.radius + 4;
320
+ const width = context.measureText(name).width;
287
321
  const y = at.y + 4;
288
- const box = {
322
+ const boxAt = (start) => ({
289
323
  bottom: y + 4,
290
- left: x - 2,
291
- right: x + context.measureText(name).width + 2,
324
+ left: start - 2,
325
+ right: start + width + 2,
292
326
  top: y - 13,
293
- };
294
- const free = taken.every((other) => box.right < other.left ||
295
- box.left > other.right ||
296
- box.bottom < other.top ||
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 x = data.x + data.size + 4;
53
+ const width = context.measureText(data.label).width;
54
54
  const y = data.y + size / 3;
55
- const box = {
55
+ const boxAt = (from) => ({
56
56
  bottom: y + 4,
57
- left: x - 2,
58
- right: x + context.measureText(data.label).width + 2,
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 = taken.every((other) => box.right < other.left ||
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
- const started = performance.now();
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.128.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.128.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.128.0",
59
- "@fusion.dev/helix-testing": "0.128.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",