@fusion.dev/helix-graph 0.129.1 → 0.131.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
@@ -242,7 +242,7 @@ it loads three.js, so a page that draws a flat shape never bundles it:
242
242
 
243
243
  | Function | Makes |
244
244
  |---|---|
245
- | `shapeFromModel(url)` | From `/model`: a solid from a glTF model, `.glb` or `.gltf`, every mesh where the model places it. Async; three.js reads the file, and an error names the model |
245
+ | `shapeFromModel(url)` | From `/model`: a solid from a glTF model, `.glb` or `.gltf`, every mesh where the model places it, compressed with meshopt or not. Async; three.js reads the file, and an error names the model |
246
246
  | `shapeFromTriangles(positions, index)` | A solid from a closed surface of triangles a page already holds |
247
247
  | `shapeFromSvg(svg)` | A flat field from an `<svg>` or its markup: every path, rectangle, circle, ellipse and polygon in its `viewBox` |
248
248
  | `shapeFromImage(image, { threshold })` | A flat field from an image's opaque pixels |
@@ -362,6 +362,7 @@ Sigma 3 is a peer dependency.
362
362
  | `connectGraph` option | Default | What else it takes |
363
363
  |---|---|---|
364
364
  | `center` | `true` | `false` leaves the view where it is when a note is chosen in the list |
365
+ | `reach` | `links` | How far a selection reaches, which the drawing lights: `upstream`, every note a path of links leads from to the selected one, or `downstream`, every note a path leads to from it |
365
366
  | `stencil` | none | A shape to draw the notes in: a field from `shapes`, `{ path, width, height }` or `{ inside, width, height }`, with `place`, `fill` or `surface`; `shell`, how thick a surface's shell is as a share of the shape's size, `0.04` by default; and `regions`, the part each group goes to |
366
367
 
367
368
  | `sigmaDriver` option | Default | What else it takes |
@@ -1,9 +1,11 @@
1
1
  import { type ViewportGestureMode } from '@fusion.dev/helix-core';
2
2
  import { type DrawnLink, type DrawnNode, type GraphChanges } from './diff.js';
3
3
  import { type GraphLooks } from './looks.js';
4
+ import { type GraphReach } from './reach.js';
4
5
  import { type GraphPoint } from './seed.js';
5
6
  import { type GraphStencil, type StencilFrame } from './stencil.js';
6
7
  export type { DrawnLink, DrawnNode, GraphChanges } from './diff.js';
8
+ export type { GraphReach } from './reach.js';
7
9
  /**
8
10
  * What a renderer has to answer for the frame to drive it, and nothing that is
9
11
  * the same for every renderer: that is the connector's.
@@ -117,6 +119,15 @@ export interface ConnectGraphOptions {
117
119
  * reader is already looking at it.
118
120
  */
119
121
  center?: boolean | undefined;
122
+ /**
123
+ * How far a selection reaches, which the drawing lights and a selection
124
+ * made in the list frames: `links`, the notes it links to and from, which
125
+ * is the default; `upstream`, everything a path of links leads from to it;
126
+ * or `downstream`, everything a path leads to from it. Where each link runs
127
+ * from what a note was derived from to the note, `upstream` lights all a
128
+ * note came from.
129
+ */
130
+ reach?: GraphReach | undefined;
120
131
  /**
121
132
  * A shape the notes are drawn in, such as a brain or a country's outline:
122
133
  * an SVG path and the box it is drawn in. **No layout runs**: each group
@@ -2,6 +2,7 @@ import { HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphSignalEvent, H
2
2
  import { arrangeByGroup, hubsOf } from './arrange.js';
3
3
  import { diffGraph, isEmpty, linkKey, } from './diff.js';
4
4
  import { graphColorsFrom, looksOf, toneColor, } from './looks.js';
5
+ import { reachOf } from './reach.js';
5
6
  import { seedPositions } from './seed.js';
6
7
  import { fieldOf, fitInStencil } from './stencil.js';
7
8
  const clustersOf = (nodes, hubOf) => {
@@ -47,19 +48,10 @@ export const connectGraph = (frame, driver, options = {}) => {
47
48
  const nodes = graph.nodes ?? [];
48
49
  const links = graph.links ?? [];
49
50
  const selected = selectedNote();
50
- const near = new Set();
51
- if (selected !== undefined) {
52
- for (const { source, target } of links) {
53
- if (source === selected) {
54
- near.add(target);
55
- }
56
- else if (target === selected) {
57
- near.add(source);
58
- }
59
- }
60
- }
51
+ const reached = selected === undefined ? undefined : reachOf(selected, links, options.reach ?? 'links');
61
52
  const notes = new Map(nodes.map((node) => [node.id, node]));
62
53
  current = looksOf({
54
+ along: reached?.along,
63
55
  colors,
64
56
  font: getComputedStyle(frame)
65
57
  .fontFamily.split(',')[0]
@@ -67,7 +59,7 @@ export const connectGraph = (frame, driver, options = {}) => {
67
59
  .replace(/^["']|["']$/g, ''),
68
60
  groups: nodes.flatMap((node) => (node.group === undefined ? [] : [node.group])),
69
61
  links: links.length,
70
- near,
62
+ near: reached?.near ?? new Set(),
71
63
  noteOf: (id) => notes.get(id),
72
64
  selected,
73
65
  shown: graph.shown,
@@ -75,8 +67,9 @@ export const connectGraph = (frame, driver, options = {}) => {
75
67
  driver.look(current);
76
68
  if (driver.groups) {
77
69
  const members = new Map();
70
+ const shown = graph.shown;
78
71
  for (const node of nodes) {
79
- if (node.group !== undefined) {
72
+ if (node.group !== undefined && (!shown || shown.has(node.id))) {
80
73
  const list = members.get(node.group);
81
74
  if (list) {
82
75
  list.push(node.id);
@@ -222,7 +215,11 @@ export const connectGraph = (frame, driver, options = {}) => {
222
215
  };
223
216
  const handleSignal = (event) => {
224
217
  const { from, to, tone } = event.detail;
225
- if (!driver.signal || !drawnNodes.has(from) || !drawnNodes.has(to)) {
218
+ const shown = graph.shown;
219
+ if (!driver.signal ||
220
+ !drawnNodes.has(from) ||
221
+ !drawnNodes.has(to) ||
222
+ (shown && !(shown.has(from) && shown.has(to)))) {
226
223
  return;
227
224
  }
228
225
  colors ??= graphColorsFrom(frame);
@@ -244,15 +241,10 @@ export const connectGraph = (frame, driver, options = {}) => {
244
241
  selected !== pressed &&
245
242
  options.center !== false &&
246
243
  drawnNodes.has(selected)) {
247
- const near = [selected];
248
- for (const { source, target } of drawnLinks.values()) {
249
- if (source === selected) {
250
- near.push(target);
251
- }
252
- else if (target === selected) {
253
- near.push(source);
254
- }
255
- }
244
+ const near = [
245
+ selected,
246
+ ...reachOf(selected, drawnLinks.values(), options.reach ?? 'links').near,
247
+ ];
256
248
  if (driver.fit && near.length > 1) {
257
249
  driver.fit(near, !still());
258
250
  }
@@ -128,6 +128,11 @@ export interface GraphLooks {
128
128
  * color whichever notes a page loads first.
129
129
  */
130
130
  export declare const looksOf: (options: {
131
+ /**
132
+ * Whether a link is one the selection reaches along, which lights it. The
133
+ * links that touch the selected note, where not given.
134
+ */
135
+ along?: ((source: string, target: string) => boolean) | undefined;
131
136
  colors: GraphColors;
132
137
  font?: string | undefined;
133
138
  groups: readonly string[];
@@ -88,7 +88,8 @@ export const looksOf = (options) => {
88
88
  ground: colors.ground,
89
89
  ink: colors.ink,
90
90
  link: (source, target) => {
91
- const touches = focused && (source === selected || target === selected);
91
+ const touches = focused &&
92
+ (options.along ? options.along(source, target) : source === selected || target === selected);
92
93
  return {
93
94
  color: touches
94
95
  ? own(selected)
@@ -0,0 +1,24 @@
1
+ /**
2
+ * How far a selection reaches, which is what the drawing lights:
3
+ *
4
+ * - `links`, the notes the selected one links to and from, and the links
5
+ * that touch it.
6
+ * - `upstream`, every note a path of links leads from to the selected one,
7
+ * and the links on those paths. Where a link runs from what a note was
8
+ * derived from to the note, this is everything the note came from.
9
+ * - `downstream`, every note a path of links leads to from the selected one,
10
+ * and the links on those paths: everything that depends on it.
11
+ */
12
+ export type GraphReach = 'downstream' | 'links' | 'upstream';
13
+ /** What a selection reaches: the notes, without the selected one, and whether a link is on the way. */
14
+ export interface Reached {
15
+ /** Whether the link from `source` to `target` is one the selection reaches along. */
16
+ along(source: string, target: string): boolean;
17
+ /** The notes reached, not counting the selected one. */
18
+ near: ReadonlySet<string>;
19
+ }
20
+ /** What `selected` reaches over `links`, as `reach` says. */
21
+ export declare const reachOf: (selected: string, links: Iterable<{
22
+ source: string;
23
+ target: string;
24
+ }>, reach: GraphReach) => Reached;
@@ -0,0 +1,45 @@
1
+ export const reachOf = (selected, links, reach) => {
2
+ const list = [...links];
3
+ if (reach === 'links') {
4
+ const near = new Set();
5
+ for (const { source, target } of list) {
6
+ if (source === selected) {
7
+ near.add(target);
8
+ }
9
+ else if (target === selected) {
10
+ near.add(source);
11
+ }
12
+ }
13
+ return {
14
+ along: (source, target) => source === selected || target === selected,
15
+ near,
16
+ };
17
+ }
18
+ const up = reach === 'upstream';
19
+ const next = new Map();
20
+ for (const { source, target } of list) {
21
+ const [from, to] = up ? [target, source] : [source, target];
22
+ const out = next.get(from);
23
+ if (out) {
24
+ out.push(to);
25
+ }
26
+ else {
27
+ next.set(from, [to]);
28
+ }
29
+ }
30
+ const reached = new Set([selected]);
31
+ const queue = [selected];
32
+ for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
33
+ for (const to of next.get(at) ?? []) {
34
+ if (!reached.has(to)) {
35
+ reached.add(to);
36
+ queue.push(to);
37
+ }
38
+ }
39
+ }
40
+ const along = up
41
+ ? (_, target) => reached.has(target)
42
+ : (source) => reached.has(source);
43
+ reached.delete(selected);
44
+ return { along, near: reached };
45
+ };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type ConnectGraphOptions, connectGraph, type DrawnLink, type DrawnNode, type GraphChanges, type GraphConnection, type GraphDriver, type GraphGroup, } from './connect-graph/connect-graph.js';
1
+ export { type ConnectGraphOptions, connectGraph, type DrawnLink, type DrawnNode, type GraphChanges, type GraphConnection, type GraphDriver, type GraphGroup, type GraphReach, } from './connect-graph/connect-graph.js';
2
2
  export { diffGraph, linkKey } from './connect-graph/diff.js';
3
3
  export { type GraphColors, type GraphLooks, type GraphTone, graphColorsFrom, type LinkLook, type NodeLook, } from './connect-graph/looks.js';
4
4
  export { type GraphPoint, type SeedLink, type SeedNode, seedPositions, } from './connect-graph/seed.js';
@@ -10,7 +10,10 @@ import { type TriangleOptions } from '../shapes/triangles.js';
10
10
  *
11
11
  * **three.js reads the file**, imported when this is first called, so a page
12
12
  * that draws no model never downloads a loader. It is an optional peer of
13
- * this package. A model that cannot be fetched or read is an error that
13
+ * this package. A file compressed with meshopt, as `gltfpack` and
14
+ * glTF-Transform write one, is read with the decoder three.js ships, and
15
+ * corners stored as integers, as `KHR_mesh_quantization` allows, are read as
16
+ * the places they stand for. A model that cannot be fetched or read is an error that
14
17
  * names it, the loader's own error as its `cause`.
15
18
  */
16
19
  export declare const shapeFromModel: (source: string | ArrayBuffer, options?: TriangleOptions) => Promise<ShapeField>;
@@ -1,13 +1,15 @@
1
1
  import { shapeFromTriangles } from '../shapes/triangles.js';
2
2
  export const shapeFromModel = async (source, options = {}) => {
3
3
  const which = typeof source === 'string' ? `The model ${source}` : 'The model given as bytes';
4
- const [{ GLTFLoader }, { Mesh, Vector3 }] = await Promise.all([
4
+ const [{ GLTFLoader }, { MeshoptDecoder }, { Mesh, Vector3 }] = await Promise.all([
5
5
  import('three/addons/loaders/GLTFLoader.js'),
6
+ import('three/addons/libs/meshopt_decoder.module.js'),
6
7
  import('three'),
7
8
  ]).catch((cause) => {
8
9
  throw new Error(`${which} could not be read, because three.js could not be loaded. Install three alongside @fusion.dev/helix-graph.`, { cause });
9
10
  });
10
11
  const loader = new GLTFLoader();
12
+ loader.setMeshoptDecoder(MeshoptDecoder);
11
13
  const model = await (typeof source === 'string'
12
14
  ? loader.loadAsync(source)
13
15
  : loader.parseAsync(source, '')).catch((cause) => {
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.129.1",
4
+ "version": "0.131.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -51,18 +51,19 @@
51
51
  },
52
52
  "bugs": "https://github.com/project-fusion/fusion/issues",
53
53
  "dependencies": {
54
- "@fusion.dev/helix-core": "0.129.1"
54
+ "@fusion.dev/helix-core": "0.131.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@cosmos.gl/graph": "^3.4.2",
58
- "@fusion.dev/helix-headless": "0.129.1",
59
- "@fusion.dev/helix-testing": "0.129.1",
58
+ "@fusion.dev/helix-headless": "0.131.0",
59
+ "@fusion.dev/helix-testing": "0.131.0",
60
60
  "@types/bun": "^1.3.0",
61
61
  "@types/three": "^0.186.0",
62
62
  "d3-force-3d": "^3.0.6",
63
63
  "graphology": "^0.26.0",
64
64
  "graphology-layout-forceatlas2": "^0.10.1",
65
65
  "graphology-types": "^0.24.8",
66
+ "meshoptimizer": "^1.1.1",
66
67
  "sigma": "^3.0.3",
67
68
  "three": "^0.186.1"
68
69
  },