@fusion.dev/helix-graph 0.134.0 → 0.135.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 CHANGED
@@ -69,6 +69,8 @@ first.
69
69
  gestures the frame's `gestures` lets it take.
70
70
  - Draws only what changed when the page hands the frame new notes or links, and
71
71
  draws the frame's `signal()`s.
72
+ - Fades out a snapshot drawn on the server by `graphSnapshot` once the renderer
73
+ has drawn the graph whole.
72
74
 
73
75
  ## Live graphs
74
76
 
@@ -357,6 +359,7 @@ connectGraph(frame, threeDriver(box), {
357
359
  | `@fusion.dev/helix-graph/three` | `threeDriver(box, options)`, and the type `ThreeDriverOptions` |
358
360
  | `@fusion.dev/helix-graph/shapes` | `shapeFromTriangles`, `shapeFromSvg`, `shapeFromImage`, `shapeFromPath`, `shapeFromInside`, `inflate`, `sphere`, `torus`, `cuboid`, `onSphere`, `surfaceOf`, and the types `ImageOptions`, `ShapeField` and `TriangleOptions` |
359
361
  | `@fusion.dev/helix-graph/model` | `shapeFromModel(source, options)`, which needs three.js |
362
+ | `@fusion.dev/helix-graph/snapshot` | `graphSnapshot(nodes, links, options)`, an SVG of the graph to paint before the renderer loads, and the types `GraphSnapshotOptions`, `SnapshotLink` and `SnapshotNode` |
360
363
 
361
364
  **Sigma, cosmos.gl, three.js and d3-force-3d are optional peers, and graphology is the page's.** Each
362
365
  driver types its renderer by the calls it makes on it, so a page brings
@@ -4,6 +4,7 @@ import { diffGraph, isEmpty, linkKey, } from './diff.js';
4
4
  import { graphColorsFrom, looksOf, toneColor, } from './looks.js';
5
5
  import { reachOf } from './reach.js';
6
6
  import { seedPositions } from './seed.js';
7
+ import { sizeOf } from './sizes.js';
7
8
  import { fieldOf, fitInStencil } from './stencil.js';
8
9
  const clustersOf = (nodes, hubOf) => {
9
10
  const groupOf = new Map(nodes.map((node) => [node.id, node.group ?? '']));
@@ -33,6 +34,35 @@ export const connectGraph = (frame, driver, options = {}) => {
33
34
  let pressed;
34
35
  let pending = 0;
35
36
  let connected = true;
37
+ const snapshots = [...frame.querySelectorAll(':scope > [data-helix-graph-snapshot]')];
38
+ const away = (snapshot) => {
39
+ snapshot.hidden = true;
40
+ snapshot.style.display = 'none';
41
+ };
42
+ const ready = () => {
43
+ graph.drawn?.(true);
44
+ if (snapshots.length === 0) {
45
+ return;
46
+ }
47
+ const style = getComputedStyle(frame);
48
+ const time = /^\s*(\d*\.?\d+)(ms|s)\s*$/i.exec(style.getPropertyValue('--helix-duration-normal'));
49
+ const duration = time ? Number(time[1]) * (time[2]?.toLowerCase() === 's' ? 1000 : 1) : 200;
50
+ const easing = style.getPropertyValue('--helix-easing-standard').trim() || 'ease';
51
+ for (const snapshot of snapshots.splice(0)) {
52
+ if (still() || duration <= 0 || typeof snapshot.animate !== 'function') {
53
+ away(snapshot);
54
+ continue;
55
+ }
56
+ let fade;
57
+ try {
58
+ fade = snapshot.animate([{ opacity: 1 }, { opacity: 0 }], { duration, easing });
59
+ }
60
+ catch {
61
+ fade = snapshot.animate([{ opacity: 1 }, { opacity: 0 }], { duration });
62
+ }
63
+ fade.finished.then(() => away(snapshot), () => away(snapshot));
64
+ }
65
+ };
36
66
  const selectedNote = () => {
37
67
  const active = graph.active;
38
68
  return active && (graph.nodes ?? []).some((node) => node.id === active) ? active : undefined;
@@ -145,9 +175,7 @@ export const connectGraph = (frame, driver, options = {}) => {
145
175
  nextNodes.set(node.id, {
146
176
  id: node.id,
147
177
  label: node.label || node.id,
148
- size: Number.isFinite(node.size)
149
- ? node.size
150
- : 1 + Math.min(Math.sqrt(degree.get(node.id) ?? 0), 6),
178
+ size: sizeOf(node.size, degree.get(node.id) ?? 0),
151
179
  x: at.x,
152
180
  y: at.y,
153
181
  ...(at.z === undefined ? {} : { z: at.z }),
@@ -195,7 +223,7 @@ export const connectGraph = (frame, driver, options = {}) => {
195
223
  graph.laidOut?.(Object.fromEntries([...settled].map(([id, at]) => [id, { ...at }])));
196
224
  if (owed) {
197
225
  owed = false;
198
- graph.drawn?.(true);
226
+ ready();
199
227
  }
200
228
  });
201
229
  return;
@@ -208,7 +236,7 @@ export const connectGraph = (frame, driver, options = {}) => {
208
236
  pending = requestAnimationFrame(() => {
209
237
  if (connected && owed) {
210
238
  owed = false;
211
- graph.drawn?.(true);
239
+ ready();
212
240
  }
213
241
  });
214
242
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * How large a note is drawn, in one place, so the drawing and a snapshot
3
+ * drawn on the server agree.
4
+ */
5
+ /**
6
+ * **A note without a size is as large as it is connected**, so a hub reads as
7
+ * one at a glance and the graph's shape shows before a word of it is read.
8
+ * The square root, so a note with a hundred links is not ten times the size
9
+ * of one with ten, and no larger than a note with thirty-six, so a hub with
10
+ * thousands does not cover its own region.
11
+ */
12
+ export declare const sizeOf: (size: number | undefined, links: number) => number;
13
+ /** A note's drawn radius in pixels, from its relative size: a leaf about four, a busy hub about nine. */
14
+ export declare const pixelsOf: (size: number) => number;
@@ -0,0 +1,2 @@
1
+ export const sizeOf = (size, links) => Number.isFinite(size) ? size : 1 + Math.min(Math.sqrt(links), 6);
2
+ export const pixelsOf = (size) => 1.6 + 1.35 * Math.max(size, 0);
@@ -1,3 +1,4 @@
1
+ import { pixelsOf } from '../connect-graph/sizes.js';
1
2
  import { chromeOver, clearOf, ease, gesturesHeld, groupsOver, nameStart, signalsOver, } from '../overlay/overlay.js';
2
3
  import { EMPHASES, HelixNodeProgram, MATERIALS, SHAPES } from './node-program.js';
3
4
  const SETTINGS = [
@@ -21,7 +22,6 @@ const SAMPLE = 250;
21
22
  const STILL = 0.004;
22
23
  const HELD = 2;
23
24
  const IDLE = 2000;
24
- const pixels = (size) => 1.6 + 1.35 * Math.max(size, 0);
25
25
  const hoverOf = (looks) => (context, data, settings) => {
26
26
  if (!data.label) {
27
27
  return;
@@ -180,7 +180,7 @@ export const sigmaDriver = (sigma, options = {}) => {
180
180
  for (const node of nodes) {
181
181
  graph.addNode(node.id, {
182
182
  label: node.label,
183
- size: pixels(node.size),
183
+ size: pixelsOf(node.size),
184
184
  x: node.x,
185
185
  y: node.y,
186
186
  });
@@ -330,7 +330,7 @@ export const sigmaDriver = (sigma, options = {}) => {
330
330
  }
331
331
  const growing = new Map();
332
332
  for (const node of changes.addNodes) {
333
- const size = pixels(node.size);
333
+ const size = pixelsOf(node.size);
334
334
  graph.addNode(node.id, {
335
335
  label: node.label,
336
336
  size: animate ? 0 : size,
@@ -351,7 +351,7 @@ export const sigmaDriver = (sigma, options = {}) => {
351
351
  }
352
352
  graph.mergeNodeAttributes(node.id, {
353
353
  label: node.label,
354
- size: pixels(node.size),
354
+ size: pixelsOf(node.size),
355
355
  ...(moves.has(node.id) ? {} : { x: node.x, y: node.y }),
356
356
  });
357
357
  }
@@ -0,0 +1 @@
1
+ export { type GraphSnapshotOptions, graphSnapshot, type SnapshotLink, type SnapshotNode, } from './snapshot.js';
@@ -0,0 +1 @@
1
+ export { graphSnapshot, } from './snapshot.js';
@@ -0,0 +1,60 @@
1
+ import type { NodeLook, NoteFacts } from '../connect-graph/looks.js';
2
+ import { type GraphReach } from '../connect-graph/reach.js';
3
+ /** A note, as a snapshot draws it: the frame's own, with its place given. */
4
+ export interface SnapshotNode extends NoteFacts {
5
+ id: string;
6
+ label?: string | undefined;
7
+ size?: number | undefined;
8
+ x?: number | undefined;
9
+ y?: number | undefined;
10
+ }
11
+ /** A link, as a snapshot draws it. */
12
+ export interface SnapshotLink {
13
+ source: string;
14
+ target: string;
15
+ }
16
+ /** What a snapshot draws, and the box it is drawn for. */
17
+ export interface GraphSnapshotOptions {
18
+ /** The selected note, lit as the frame lights it. None by default. */
19
+ active?: string | undefined;
20
+ /**
21
+ * The part of the graph the drawing fits, as Sigma's `setCustomBBox` takes
22
+ * it. The box around the notes by default. Give both the same box.
23
+ */
24
+ bounds?: {
25
+ x: readonly [number, number];
26
+ y: readonly [number, number];
27
+ } | null | undefined;
28
+ /** How heavily a note is drawn where it does not say, as the Sigma driver's `emphasis`. */
29
+ emphasis?: NodeLook['emphasis'];
30
+ /** Whether each group's name is drawn over its notes, as the Sigma driver's `groupLabels`. On by default. */
31
+ groupLabels?: boolean | undefined;
32
+ /** The box's height, in pixels. 480 by default. */
33
+ height?: number | undefined;
34
+ /** Which notes are named, as the Sigma driver's `labels`. `hover` by default. */
35
+ labels?: 'all' | 'hover' | 'hubs' | undefined;
36
+ /** The room kept at each edge, in pixels, as Sigma's `stagePadding`. 30 by default. */
37
+ padding?: number | undefined;
38
+ /** How far `active` reaches, as `connectGraph`'s `reach`. `links` by default. */
39
+ reach?: GraphReach | undefined;
40
+ /** A note's shape where it does not say, as the Sigma driver's `shape`. */
41
+ shape?: NodeLook['shape'];
42
+ /** The box's width, in pixels. 1200 by default. */
43
+ width?: number | undefined;
44
+ }
45
+ /**
46
+ * **The graph drawn on the server, as an SVG in the theme's colors**, for a
47
+ * page to paint before the renderer has loaded. Put it in the frame after
48
+ * the element the renderer draws in, and `connectGraph` fades it out once
49
+ * the renderer has drawn the graph whole.
50
+ *
51
+ * **Where Sigma.js draws it**: fitted to the box as Sigma fits a graph, each
52
+ * note its size and its group's or tone's color, each link tinted by the
53
+ * note it leaves, and the names placed after their notes and left out where
54
+ * one would cover another. The colors are the theme's tokens, so it is
55
+ * light or dark with the page.
56
+ *
57
+ * **Whole or absent.** An empty string where any note has no `x` and `y`,
58
+ * since only a layout run in the browser could place it.
59
+ */
60
+ export declare const graphSnapshot: (nodes: readonly SnapshotNode[], links: readonly SnapshotLink[], options?: GraphSnapshotOptions) => string;
@@ -0,0 +1,204 @@
1
+ import { reachOf } from '../connect-graph/reach.js';
2
+ import { pixelsOf, sizeOf } from '../connect-graph/sizes.js';
3
+ import { clearOf } from '../overlay/overlay.js';
4
+ const SERIES = 24;
5
+ const FADED = 75;
6
+ const HUB = 6.5;
7
+ const LABEL = 13;
8
+ const widthOf = (text, size) => text.length * size * 0.56;
9
+ const escaped = (text) => text.replace(/[&<>"']/g, (character) => ({ "'": '&#39;', '"': '&quot;', '&': '&amp;', '<': '&lt;', '>': '&gt;' })[character]);
10
+ const short = (value) => String(Math.round(value * 10) / 10);
11
+ const token = (name) => `var(--helix-color-${name})`;
12
+ const mix = (from, to, amount) => amount <= 0 ? from : `color-mix(in srgb, ${to} ${short(amount)}%, ${from})`;
13
+ const toneOf = (tone) => tone === 'primary'
14
+ ? token('primary')
15
+ : tone === 'neutral'
16
+ ? token('foreground-muted')
17
+ : token(tone);
18
+ const correctionOf = (width, height, across, down) => {
19
+ const box = height / width;
20
+ const graph = down / across;
21
+ if ((box < 1 && graph > 1) || (box > 1 && graph < 1)) {
22
+ return 1;
23
+ }
24
+ const ratio = Math.min(Math.max(graph, 1 / graph), Math.max(1 / box, box));
25
+ return Number.isFinite(ratio) ? ratio : 1;
26
+ };
27
+ const shapeOf = (shape, x, y, radius, paint) => {
28
+ if (shape === 'square') {
29
+ const half = radius * 0.88;
30
+ return `<rect height="${short(half * 2)}" rx="${short(radius * 0.18)}" width="${short(half * 2)}" x="${short(x - half)}" y="${short(y - half)}" ${paint}/>`;
31
+ }
32
+ if (shape === 'diamond') {
33
+ const reach = radius * 1.216;
34
+ return `<path d="M${short(x)} ${short(y - reach)}L${short(x + reach)} ${short(y)}L${short(x)} ${short(y + reach)}L${short(x - reach)} ${short(y)}Z" ${paint}/>`;
35
+ }
36
+ return `<circle cx="${short(x)}" cy="${short(y)}" r="${short(radius)}" ${paint}/>`;
37
+ };
38
+ export const graphSnapshot = (nodes, links, options = {}) => {
39
+ const width = options.width ?? 1200;
40
+ const height = options.height ?? 480;
41
+ const padding = options.padding ?? 30;
42
+ if (nodes.length === 0 ||
43
+ nodes.some((node) => !(Number.isFinite(node.x) && Number.isFinite(node.y)))) {
44
+ return '';
45
+ }
46
+ const notes = new Map(nodes.map((node) => [node.id, node]));
47
+ const drawn = links.filter((link) => link.source !== link.target && notes.has(link.source) && notes.has(link.target));
48
+ const xs = nodes.map((node) => node.x);
49
+ const ys = nodes.map((node) => node.y);
50
+ const [left, right] = options.bounds?.x ?? [Math.min(...xs), Math.max(...xs)];
51
+ const [bottom, top] = options.bounds?.y ?? [Math.min(...ys), Math.max(...ys)];
52
+ const extent = Math.max(right - left, top - bottom) || 1;
53
+ const scale = ((Math.min(width, height) - 2 * padding) *
54
+ correctionOf(width, height, right - left, top - bottom)) /
55
+ extent;
56
+ const at = (node) => ({
57
+ x: width / 2 + (node.x - (left + right) / 2) * scale,
58
+ y: height / 2 - (node.y - (bottom + top) / 2) * scale,
59
+ });
60
+ const selected = options.active && notes.has(options.active) ? options.active : undefined;
61
+ const reached = selected === undefined ? undefined : reachOf(selected, drawn, options.reach ?? 'links');
62
+ const near = reached?.near ?? new Set();
63
+ const groups = [...new Set(nodes.flatMap((node) => (node.group ? [node.group] : [])))].sort();
64
+ const series = new Map(groups.map((group, index) => [group, token(`series-${(index % SERIES) + 1}`)]));
65
+ const own = (node) => node.tone
66
+ ? toneOf(node.tone)
67
+ : ((node.group && series.get(node.group)) ?? token('foreground-muted'));
68
+ const ground = token('surface-sunken');
69
+ const faded = (node) => selected !== undefined && node.id !== selected && !near.has(node.id);
70
+ const degree = new Map();
71
+ for (const { source, target } of drawn) {
72
+ degree.set(source, (degree.get(source) ?? 0) + 1);
73
+ degree.set(target, (degree.get(target) ?? 0) + 1);
74
+ }
75
+ const radiusOf = (node) => pixelsOf(sizeOf(node.size, degree.get(node.id) ?? 0)) *
76
+ (node.id === selected ? 1.5 : near.has(node.id) ? 1.25 : 1);
77
+ const along = Math.min(Math.max(Math.log(drawn.length / 50) / Math.log(2000 / 50), 0), 1);
78
+ const rest = (0.35 + (0.72 - 0.35) * along) * 100;
79
+ const lines = [];
80
+ const raisedLines = [];
81
+ for (const link of drawn) {
82
+ const from = at(notes.get(link.source));
83
+ const to = at(notes.get(link.target));
84
+ const lit = reached?.along(link.source, link.target) ?? false;
85
+ const color = lit
86
+ ? own(notes.get(selected))
87
+ : selected !== undefined
88
+ ? mix(token('border'), ground, 60)
89
+ : mix(own(notes.get(link.source)), ground, rest);
90
+ (lit ? raisedLines : lines).push(`<line style="stroke:${color}" stroke-width="${lit ? 1.2 : 0.6}" x1="${short(from.x)}" x2="${short(to.x)}" y1="${short(from.y)}" y2="${short(to.y)}"/>`);
91
+ }
92
+ const order = (node) => node.id === selected ? 2 : near.has(node.id) ? 1 : 0;
93
+ const shapes = [...nodes]
94
+ .sort((one, other) => order(one) - order(other))
95
+ .map((node) => {
96
+ const { x, y } = at(node);
97
+ const radius = radiusOf(node);
98
+ const color = faded(node) ? mix(own(node), ground, FADED) : own(node);
99
+ const shape = node.shape ?? options.shape ?? 'circle';
100
+ const emphasis = shape === 'ring' ? 'outlined' : (node.emphasis ?? options.emphasis ?? 'solid');
101
+ const body = emphasis === 'solid'
102
+ ? shapeOf(shape === 'ring' ? 'circle' : shape, x, y, radius, `style="fill:${color}"`)
103
+ :
104
+ (emphasis === 'subtle'
105
+ ? shapeOf(shape, x, y, radius, `fill-opacity="0.3" style="fill:${color}"`)
106
+ : '') +
107
+ shapeOf(shape === 'ring' ? 'circle' : shape, x, y, radius * 0.84, `fill="none" stroke-width="${short(radius * 0.32)}" style="stroke:${color}"`);
108
+ const glow = node.id === selected
109
+ ? `<circle cx="${short(x)}" cy="${short(y)}" fill-opacity="0.18" r="${short(radius * 2.2)}" style="fill:${color}"/>` +
110
+ `<circle cx="${short(x)}" cy="${short(y)}" fill="none" r="${short(radius * 1.32)}" stroke-width="${short(Math.max(radius * 0.08, 1))}" style="stroke:${color}"/>`
111
+ : '';
112
+ return glow + body;
113
+ });
114
+ const naming = options.labels ?? 'hover';
115
+ const taken = [];
116
+ const names = [];
117
+ const candidates = nodes
118
+ .filter((node) => {
119
+ if (!node.label && !node.id) {
120
+ return false;
121
+ }
122
+ if (node.id === selected) {
123
+ return true;
124
+ }
125
+ if (faded(node)) {
126
+ return false;
127
+ }
128
+ return (near.has(node.id) ||
129
+ naming === 'all' ||
130
+ (naming === 'hubs' && pixelsOf(sizeOf(node.size, degree.get(node.id) ?? 0)) >= HUB));
131
+ })
132
+ .sort((one, other) => (other.id === selected ? 1 : 0) - (one.id === selected ? 1 : 0) ||
133
+ radiusOf(other) - radiusOf(one));
134
+ for (const node of candidates) {
135
+ const text = node.label || node.id;
136
+ const strong = node.id === selected || near.has(node.id);
137
+ const size = strong ? LABEL : LABEL - 1;
138
+ const { x, y } = at(node);
139
+ const radius = radiusOf(node);
140
+ const wide = widthOf(text, size);
141
+ const line = y + size / 3;
142
+ const boxAt = (from) => ({
143
+ bottom: line + 4,
144
+ left: from - 2,
145
+ right: from + wide + 2,
146
+ top: line - size,
147
+ });
148
+ const after = x + radius + 4;
149
+ const fits = (from) => {
150
+ const box = boxAt(from);
151
+ return box.left >= 0 && box.right <= width && clearOf(box, taken);
152
+ };
153
+ const before = x - radius - 4 - wide;
154
+ const start = fits(after) || !fits(before) ? after : before;
155
+ const box = boxAt(start);
156
+ if (!clearOf(box, taken) && node.id !== selected) {
157
+ continue;
158
+ }
159
+ taken.push(box);
160
+ names.push(`<text font-size="${size}" font-weight="${strong ? 600 : 500}" style="fill:${token(strong ? 'foreground' : 'foreground-muted')};stroke:${ground}" x="${short(start)}" y="${short(line)}">${escaped(text)}</text>`);
161
+ }
162
+ const regions = [];
163
+ if (options.groupLabels !== false) {
164
+ const ownGroup = selected === undefined ? undefined : notes.get(selected)?.group;
165
+ for (const group of groups) {
166
+ const members = nodes.filter((node) => node.group === group);
167
+ const points = members.map(at);
168
+ const middle = {
169
+ x: points.reduce((sum, point) => sum + point.x, 0) / points.length,
170
+ y: points.reduce((sum, point) => sum + point.y, 0) / points.length,
171
+ };
172
+ const spread = points.reduce((sum, point) => sum + Math.hypot(point.x - middle.x, point.y - middle.y), 0) /
173
+ points.length;
174
+ if (points.length < 2 || spread < 12) {
175
+ continue;
176
+ }
177
+ const name = group.toLocaleUpperCase();
178
+ const half = (widthOf(name, 11) * 1.14) / 2 + 4;
179
+ const boxAt = (y) => ({
180
+ bottom: y + 9,
181
+ left: middle.x - half,
182
+ right: middle.x + half,
183
+ top: y - 9,
184
+ });
185
+ const y = [0, 18, -18]
186
+ .map((step) => middle.y + step)
187
+ .find((one) => clearOf(boxAt(one), taken));
188
+ if (y === undefined) {
189
+ continue;
190
+ }
191
+ taken.push(boxAt(y));
192
+ const tinted = members.find((node) => node.tone === undefined) ?? members[0];
193
+ const color = mix(tinted ? own(tinted) : token('foreground-muted'), token('foreground'), 60);
194
+ const fade = selected !== undefined && ownGroup !== group ? 0.3 : 0.92;
195
+ regions.push(`<text dominant-baseline="middle" fill-opacity="${fade}" font-size="11" font-weight="650" letter-spacing="0.14em" style="fill:${color};stroke:${ground}" text-anchor="middle" x="${short(middle.x)}" y="${short(y)}">${escaped(name)}</text>`);
196
+ }
197
+ }
198
+ return (`<div aria-hidden="true" data-helix-graph-snapshot style="inset:0;overflow:hidden;pointer-events:none;position:absolute">` +
199
+ `<svg height="${height}" viewBox="0 0 ${width} ${height}" width="${width}" xmlns="http://www.w3.org/2000/svg" style="left:50%;position:absolute;top:50%;translate:-50% -50%">` +
200
+ `<g stroke-linecap="round">${lines.join('')}${raisedLines.join('')}</g>` +
201
+ `<g>${shapes.join('')}</g>` +
202
+ `<g paint-order="stroke" stroke-linejoin="round" stroke-width="3">${regions.join('')}${names.join('')}</g>` +
203
+ `</svg></div>`);
204
+ };
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.134.0",
4
+ "version": "0.135.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -33,6 +33,10 @@
33
33
  "types": "./dist/sigma/index.d.ts",
34
34
  "default": "./dist/sigma/index.js"
35
35
  },
36
+ "./snapshot": {
37
+ "types": "./dist/snapshot/index.d.ts",
38
+ "default": "./dist/snapshot/index.js"
39
+ },
36
40
  "./three": {
37
41
  "types": "./dist/three/index.d.ts",
38
42
  "default": "./dist/three/index.js"
@@ -51,12 +55,12 @@
51
55
  },
52
56
  "bugs": "https://github.com/project-fusion/fusion/issues",
53
57
  "dependencies": {
54
- "@fusion.dev/helix-core": "0.134.0"
58
+ "@fusion.dev/helix-core": "0.135.0"
55
59
  },
56
60
  "devDependencies": {
57
61
  "@cosmos.gl/graph": "^3.4.2",
58
- "@fusion.dev/helix-headless": "0.134.0",
59
- "@fusion.dev/helix-testing": "0.134.0",
62
+ "@fusion.dev/helix-headless": "0.135.0",
63
+ "@fusion.dev/helix-testing": "0.135.0",
60
64
  "@types/bun": "^1.3.0",
61
65
  "@types/three": "^0.186.0",
62
66
  "d3-force-3d": "^3.0.6",