@fusion.dev/helix-graph 0.126.0 → 0.128.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 +70 -38
- package/dist/connect-graph/arrange.d.ts +4 -2
- package/dist/connect-graph/arrange.js +5 -22
- package/dist/connect-graph/connect-graph.d.ts +18 -6
- package/dist/connect-graph/connect-graph.js +76 -32
- package/dist/connect-graph/diff.d.ts +7 -0
- package/dist/connect-graph/looks.d.ts +8 -0
- package/dist/connect-graph/looks.js +6 -3
- package/dist/connect-graph/seed.d.ts +9 -0
- package/dist/connect-graph/seed.js +21 -15
- package/dist/connect-graph/stencil.d.ts +3 -1
- package/dist/connect-graph/stencil.js +32 -10
- package/dist/cosmos/cosmos-driver.d.ts +5 -4
- package/dist/cosmos/cosmos-driver.js +122 -39
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.js +1 -0
- package/dist/{shapes → model}/model.d.ts +4 -3
- package/dist/{shapes → model}/model.js +10 -5
- package/dist/overlay/overlay.d.ts +14 -8
- package/dist/overlay/overlay.js +64 -62
- package/dist/shapes/flat.d.ts +1 -1
- package/dist/shapes/flat.js +9 -5
- package/dist/shapes/index.d.ts +3 -4
- package/dist/shapes/index.js +3 -4
- package/dist/{connect-graph → shapes}/shape-field.d.ts +7 -3
- package/dist/{connect-graph → shapes}/shape-field.js +4 -2
- package/dist/shapes/solids.d.ts +3 -3
- package/dist/shapes/solids.js +25 -9
- package/dist/{three → shapes}/surface.d.ts +1 -1
- package/dist/shapes/triangles.d.ts +2 -2
- package/dist/shapes/triangles.js +49 -30
- package/dist/sigma/sigma-driver.d.ts +33 -17
- package/dist/sigma/sigma-driver.js +69 -17
- package/dist/three/three-driver.d.ts +12 -3
- package/dist/three/three-driver.js +376 -201
- package/package.json +9 -5
- /package/dist/{three → shapes}/surface.js +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** A point in the renderer's own units. */
|
|
2
2
|
export interface GraphPoint {
|
|
3
|
+
/** Across. */
|
|
3
4
|
x: number;
|
|
5
|
+
/** Up. */
|
|
4
6
|
y: number;
|
|
5
7
|
/** How near the viewer, for a renderer that draws in three dimensions. */
|
|
6
8
|
z?: number | undefined;
|
|
@@ -17,6 +19,13 @@ export interface SeedLink {
|
|
|
17
19
|
source: string;
|
|
18
20
|
target: string;
|
|
19
21
|
}
|
|
22
|
+
/** The angle between one place and the next on a spiral: the golden angle, which never lines two up. */
|
|
23
|
+
export declare const TURN: number;
|
|
24
|
+
/**
|
|
25
|
+
* Each note's neighbors across the links, both ways, kept to the links
|
|
26
|
+
* whose two ends `keep` allows where it is given.
|
|
27
|
+
*/
|
|
28
|
+
export declare const neighborsOf: (links: readonly SeedLink[], keep?: (id: string) => boolean) => Map<string, string[]>;
|
|
20
29
|
/**
|
|
21
30
|
* Where each note starts, and which of them a layout still has to place.
|
|
22
31
|
*
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
const TURN = Math.PI * (3 - Math.sqrt(5));
|
|
1
|
+
export const TURN = Math.PI * (3 - Math.sqrt(5));
|
|
2
|
+
export const neighborsOf = (links, keep) => {
|
|
3
|
+
const neighbors = new Map();
|
|
4
|
+
const add = (from, to) => {
|
|
5
|
+
const list = neighbors.get(from);
|
|
6
|
+
if (list) {
|
|
7
|
+
list.push(to);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
neighbors.set(from, [to]);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
for (const { source, target } of links) {
|
|
14
|
+
if (!keep || (keep(source) && keep(target))) {
|
|
15
|
+
add(source, target);
|
|
16
|
+
add(target, source);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return neighbors;
|
|
20
|
+
};
|
|
2
21
|
const NEAR = 0.5;
|
|
3
22
|
const nudge = (id) => {
|
|
4
23
|
let hash = 2166136261;
|
|
@@ -26,20 +45,7 @@ export const seedPositions = (nodes, links, known = new Map()) => {
|
|
|
26
45
|
}
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
|
-
const neighbors =
|
|
30
|
-
const add = (from, to) => {
|
|
31
|
-
const list = neighbors.get(from);
|
|
32
|
-
if (list) {
|
|
33
|
-
list.push(to);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
neighbors.set(from, [to]);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
for (const { source, target } of links) {
|
|
40
|
-
add(source, target);
|
|
41
|
-
add(target, source);
|
|
42
|
-
}
|
|
48
|
+
const neighbors = neighborsOf(links);
|
|
43
49
|
let spiral = positions.size;
|
|
44
50
|
for (const node of nodes) {
|
|
45
51
|
if (positions.has(node.id)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { type ShapeField } from '../shapes/shape-field.js';
|
|
1
2
|
import type { GraphPoint } from './seed.js';
|
|
2
|
-
import { type ShapeField } from './shape-field.js';
|
|
3
3
|
/**
|
|
4
4
|
* A shape the notes are drawn in. **Any shape field**, flat or solid, such as
|
|
5
5
|
* `inflate` makes; or, flat, an SVG path in a box `width` by `height`, as an
|
|
@@ -63,7 +63,9 @@ export declare const fitToStencil: (positions: ReadonlyMap<string, GraphPoint>,
|
|
|
63
63
|
* `middle.y - (y - height / 2) * scale` and `middle.z + (z - depth / 2) * scale`.
|
|
64
64
|
*/
|
|
65
65
|
export interface StencilFrame {
|
|
66
|
+
/** The stencil's shape, as a field, whatever form it was given in. */
|
|
66
67
|
field: ShapeField;
|
|
68
|
+
/** Where the middle of the stencil's box was put, in the places' units. */
|
|
67
69
|
middle: {
|
|
68
70
|
x: number;
|
|
69
71
|
y: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fieldFromInside, fieldFromPath } from '
|
|
1
|
+
import { fieldFromInside, fieldFromPath } from '../shapes/shape-field.js';
|
|
2
2
|
import { surfaceWeb } from './surface-web.js';
|
|
3
3
|
const PROBE = { flat: 64, solid: 32 };
|
|
4
4
|
const SHELL = 0.04;
|
|
@@ -78,9 +78,9 @@ export const placesIn = (field, count, { place = 'fill', shell = SHELL } = {}) =
|
|
|
78
78
|
}
|
|
79
79
|
return [];
|
|
80
80
|
};
|
|
81
|
-
const placesByRegion = (field,
|
|
81
|
+
const placesByRegion = (field, asked, options) => {
|
|
82
82
|
const regionOf = field.regionOf;
|
|
83
|
-
const count = [...
|
|
83
|
+
const count = [...asked.values()].reduce((sum, many) => sum + many, 0);
|
|
84
84
|
if (!regionOf) {
|
|
85
85
|
return undefined;
|
|
86
86
|
}
|
|
@@ -101,6 +101,16 @@ const placesByRegion = (field, wanted, options) => {
|
|
|
101
101
|
byRegion.set(region, [place]);
|
|
102
102
|
}
|
|
103
103
|
});
|
|
104
|
+
const found = new Set([...byRegion.keys()].filter((region) => region !== undefined));
|
|
105
|
+
const missing = [...asked.keys()].filter((region) => region !== undefined && !found.has(region));
|
|
106
|
+
if (missing.length > 0 && times * 2 <= 24) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const wanted = new Map();
|
|
110
|
+
for (const [region, many] of asked) {
|
|
111
|
+
const part = region !== undefined && found.has(region) ? region : undefined;
|
|
112
|
+
wanted.set(part, (wanted.get(part) ?? 0) + many);
|
|
113
|
+
}
|
|
104
114
|
const claimed = new Set([...wanted.keys()].filter((region) => region !== undefined));
|
|
105
115
|
const places = [];
|
|
106
116
|
const regions = [];
|
|
@@ -130,7 +140,7 @@ const placesByRegion = (field, wanted, options) => {
|
|
|
130
140
|
places.push(place);
|
|
131
141
|
regions.push(undefined);
|
|
132
142
|
}
|
|
133
|
-
return { places, regions };
|
|
143
|
+
return { found, missing, places, regions };
|
|
134
144
|
}
|
|
135
145
|
return undefined;
|
|
136
146
|
};
|
|
@@ -163,18 +173,30 @@ export const fitInStencil = (positions, stencil, { clustersOf, } = {}) => {
|
|
|
163
173
|
const group = groupOf(note.id);
|
|
164
174
|
return [note.id, group === undefined ? undefined : stencil.regions?.[group]];
|
|
165
175
|
}));
|
|
166
|
-
const
|
|
176
|
+
const asked = new Map();
|
|
167
177
|
for (const region of noteRegion.values()) {
|
|
168
|
-
|
|
178
|
+
asked.set(region, (asked.get(region) ?? 0) + 1);
|
|
169
179
|
}
|
|
170
|
-
const regional =
|
|
171
|
-
? placesByRegion(field,
|
|
180
|
+
const regional = [...asked.keys()].some((region) => region !== undefined)
|
|
181
|
+
? placesByRegion(field, asked, options)
|
|
172
182
|
: undefined;
|
|
173
|
-
if (
|
|
174
|
-
|
|
183
|
+
if (regional && regional.missing.length > 0) {
|
|
184
|
+
console.warn(`fitInStencil: the shape has no part named ${regional.missing.map((part) => `"${part}"`).join(', ')}, so the notes sent there are placed with the rest. Its parts: ${[...regional.found].join(', ') || 'none'}.`);
|
|
185
|
+
}
|
|
186
|
+
else if (!regional && [...asked.keys()].some((region) => region !== undefined)) {
|
|
187
|
+
console.warn(field.regionOf
|
|
188
|
+
? 'fitInStencil: the parts named in regions are too small for the notes sent to them, so the notes are placed over the whole shape.'
|
|
189
|
+
: 'fitInStencil: the shape has no named parts, so regions is not used and the notes are placed over the whole shape.');
|
|
190
|
+
}
|
|
191
|
+
for (const [id, region] of noteRegion) {
|
|
192
|
+
if (region !== undefined && !regional?.found.has(region)) {
|
|
175
193
|
noteRegion.set(id, undefined);
|
|
176
194
|
}
|
|
177
195
|
}
|
|
196
|
+
const wanted = new Map();
|
|
197
|
+
for (const region of noteRegion.values()) {
|
|
198
|
+
wanted.set(region, (wanted.get(region) ?? 0) + 1);
|
|
199
|
+
}
|
|
178
200
|
const places = regional?.places ?? placesIn(field, notes.length, options);
|
|
179
201
|
const placeRegion = regional?.regions ?? places.map(() => undefined);
|
|
180
202
|
if (places.length < notes.length) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GraphDriver } from '../connect-graph/connect-graph.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type NodeLook } from '../connect-graph/looks.js';
|
|
3
3
|
/** A cosmos.gl graph, as far as the driver calls it. */
|
|
4
4
|
export interface CosmosGraph {
|
|
5
5
|
fitView(duration?: number, padding?: number): void;
|
|
@@ -24,7 +24,7 @@ export interface CosmosGraph {
|
|
|
24
24
|
setPointShapes(pointShapes: Float32Array): void;
|
|
25
25
|
setPointSizes(pointSizes: Float32Array): void;
|
|
26
26
|
setZoomLevel(value: number, duration?: number): void;
|
|
27
|
-
setZoomTransformByPointPositions(positions: Float32Array, duration?: number, scale?: number): void;
|
|
27
|
+
setZoomTransformByPointPositions(positions: Float32Array, duration?: number, scale?: number, padding?: number): void;
|
|
28
28
|
spaceToScreenPosition(spacePosition: [number, number]): [number, number];
|
|
29
29
|
spaceToScreenRadius(spaceRadius: number): number;
|
|
30
30
|
start(alpha?: number): void;
|
|
@@ -40,7 +40,7 @@ export interface CosmosDriverOptions {
|
|
|
40
40
|
* default, past where twenty thousand notes settle on a laptop's GPU.
|
|
41
41
|
*/
|
|
42
42
|
layoutLimit?: number | undefined;
|
|
43
|
-
/** A note's shape where it does not say: `circle` by default, `square` or `diamond`. */
|
|
43
|
+
/** A note's shape where it does not say: `circle` by default, `square` or `diamond`. A `ring` is drawn as a circle. */
|
|
44
44
|
shape?: NodeLook['shape'];
|
|
45
45
|
/** The size of cosmos.gl's space, as its own `spaceSize` says. 4096 by default. */
|
|
46
46
|
spaceSize?: number | undefined;
|
|
@@ -62,6 +62,7 @@ export interface CosmosDriverOptions {
|
|
|
62
62
|
* each group, over the drawing, as the Sigma driver does.
|
|
63
63
|
*
|
|
64
64
|
* **It is `@cosmos.gl/graph`, which is MIT**, and not Cosmograph, which is
|
|
65
|
-
* licensed for noncommercial use only.
|
|
65
|
+
* licensed for noncommercial use only. The graph is the page's: after
|
|
66
|
+
* `disconnect()`, `graph.destroy()` frees it.
|
|
66
67
|
*/
|
|
67
68
|
export declare const cosmosDriver: (graph: CosmosGraph, container: HTMLElement, options?: CosmosDriverOptions) => GraphDriver;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { linkKey } from '../connect-graph/diff.js';
|
|
2
|
+
import { rgbOf } from '../connect-graph/looks.js';
|
|
1
3
|
import { gesturesHeld, groupsOver, namesOver, signalsOver, } from '../overlay/overlay.js';
|
|
2
4
|
const SHAPES = { circle: 0, diamond: 3, ring: 0, square: 1 };
|
|
3
5
|
const STEP = 250;
|
|
@@ -9,8 +11,10 @@ const pixels = (size) => 0.5 * (1.6 + 1.35 * Math.max(size, 0));
|
|
|
9
11
|
const CLUSTER = 0.1;
|
|
10
12
|
const FOLLOW = 1000;
|
|
11
13
|
const PADDING = 0.08;
|
|
14
|
+
const SIGNALED = 2500;
|
|
15
|
+
const still = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
12
16
|
const channels = (color, alpha = 1) => {
|
|
13
|
-
const [red
|
|
17
|
+
const [red, green, blue] = rgbOf(color);
|
|
14
18
|
return [red / 255, green / 255, blue / 255, alpha];
|
|
15
19
|
};
|
|
16
20
|
export const cosmosDriver = (graph, container, options = {}) => {
|
|
@@ -25,6 +29,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
25
29
|
let near = new Set();
|
|
26
30
|
let groupSample = [];
|
|
27
31
|
let following = new Set();
|
|
32
|
+
const signaled = new Map();
|
|
28
33
|
let moved = false;
|
|
29
34
|
let stopped = false;
|
|
30
35
|
let uploaded = 0;
|
|
@@ -51,8 +56,17 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
51
56
|
if (hovered !== undefined) {
|
|
52
57
|
next.add(hovered);
|
|
53
58
|
}
|
|
59
|
+
const now = performance.now();
|
|
54
60
|
for (const at of extra) {
|
|
55
|
-
|
|
61
|
+
signaled.set(at, now + SIGNALED);
|
|
62
|
+
}
|
|
63
|
+
for (const [at, until] of signaled) {
|
|
64
|
+
if (until < now) {
|
|
65
|
+
signaled.delete(at);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
next.add(at);
|
|
69
|
+
}
|
|
56
70
|
}
|
|
57
71
|
if (next.size !== following.size || [...next].some((at) => !following.has(at))) {
|
|
58
72
|
following = next;
|
|
@@ -80,7 +94,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
80
94
|
};
|
|
81
95
|
const held = gesturesHeld(container);
|
|
82
96
|
const signals = signalsOver(container, onScreen);
|
|
83
|
-
const groups = groupsOver(container, onScreen);
|
|
97
|
+
const groups = groupsOver(container, onScreen, () => names.boxes());
|
|
84
98
|
const names = namesOver(container, onScreen);
|
|
85
99
|
const drawNames = () => {
|
|
86
100
|
const lit = hovered === undefined ? undefined : ids[hovered];
|
|
@@ -96,10 +110,11 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
96
110
|
const flatNow = () => live()
|
|
97
111
|
? graph.getPointPositions()
|
|
98
112
|
: ids.flatMap((id) => toSpace(drawnNow.get(id) ?? { x: 0, y: 0 }));
|
|
99
|
-
const fitAll = (
|
|
113
|
+
const fitAll = (asked) => {
|
|
100
114
|
if (stopped) {
|
|
101
115
|
return;
|
|
102
116
|
}
|
|
117
|
+
const duration = still() ? 0 : asked;
|
|
103
118
|
if (!graph.isReady) {
|
|
104
119
|
void graph.ready.then(() => requestAnimationFrame(() => fitAll(duration)));
|
|
105
120
|
return;
|
|
@@ -123,30 +138,59 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
123
138
|
graph.setConfigPartial({ pointSizeScale: by });
|
|
124
139
|
if (Math.abs(clarity() - painted) > 0.04) {
|
|
125
140
|
paintLinks();
|
|
126
|
-
graph.render();
|
|
141
|
+
graph.render(undefined, 0);
|
|
127
142
|
}
|
|
128
143
|
}
|
|
129
144
|
};
|
|
130
145
|
const clarity = () => FAINT + (1 - FAINT) * Math.min(Math.max((grown - 1) / 0.8, 0), 1);
|
|
131
146
|
let painted = 1;
|
|
147
|
+
let linkColors = new Float32Array(0);
|
|
148
|
+
let widths = new Float32Array(0);
|
|
149
|
+
let linksOf = new Map();
|
|
150
|
+
const paintLink = (at, lit) => {
|
|
151
|
+
const link = links[at];
|
|
152
|
+
const look = looks.link(link.source, link.target);
|
|
153
|
+
const touches = lit !== undefined && (link.source === lit || link.target === lit);
|
|
154
|
+
const whole = look.raised || touches;
|
|
155
|
+
linkColors.set(channels(touches ? looks.node(lit).color : look.color, look.hidden ? 0 : whole ? 1 : painted), at * 4);
|
|
156
|
+
widths[at] = whole ? 1.6 : 0.8;
|
|
157
|
+
};
|
|
132
158
|
const paintLinks = () => {
|
|
133
159
|
if (!looks) {
|
|
134
160
|
return;
|
|
135
161
|
}
|
|
136
162
|
painted = clarity();
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
if (linkColors.length !== links.length * 4) {
|
|
164
|
+
linkColors = new Float32Array(links.length * 4);
|
|
165
|
+
widths = new Float32Array(links.length);
|
|
166
|
+
}
|
|
139
167
|
const lit = hovered === undefined ? undefined : ids[hovered];
|
|
140
|
-
links.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
for (let at = 0; at < links.length; at++) {
|
|
169
|
+
paintLink(at, lit);
|
|
170
|
+
}
|
|
171
|
+
graph.setLinkColors(linkColors);
|
|
172
|
+
graph.setLinkWidths(widths);
|
|
173
|
+
};
|
|
174
|
+
const hover = (next) => {
|
|
175
|
+
const was = hovered === undefined ? undefined : ids[hovered];
|
|
176
|
+
hovered = next;
|
|
177
|
+
container.style.cursor = next === undefined ? cursor : 'pointer';
|
|
178
|
+
if (!looks) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const lit = next === undefined ? undefined : ids[next];
|
|
182
|
+
for (const id of [was, lit]) {
|
|
183
|
+
for (const at of id === undefined ? [] : (linksOf.get(id) ?? [])) {
|
|
184
|
+
paintLink(at, lit);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
147
187
|
graph.setLinkColors(linkColors);
|
|
148
188
|
graph.setLinkWidths(widths);
|
|
189
|
+
follow();
|
|
190
|
+
graph.render(undefined, 0);
|
|
191
|
+
redraw();
|
|
149
192
|
};
|
|
193
|
+
const cursor = container.style.cursor;
|
|
150
194
|
const paint = (transition = 0) => {
|
|
151
195
|
if (!looks) {
|
|
152
196
|
return;
|
|
@@ -181,12 +225,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
181
225
|
hoveredPointRingColor: channels(looks.ink, 0.6),
|
|
182
226
|
});
|
|
183
227
|
follow();
|
|
184
|
-
|
|
185
|
-
graph.render(undefined, transition);
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
graph.render();
|
|
189
|
-
}
|
|
228
|
+
graph.render(undefined, still() ? 0 : transition);
|
|
190
229
|
redraw();
|
|
191
230
|
};
|
|
192
231
|
const load = (nodes, next, positions, transition = 0) => {
|
|
@@ -194,7 +233,20 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
194
233
|
index = new Map(ids.map((id, at) => [id, at]));
|
|
195
234
|
drawnNow = new Map(nodes.map((node) => [node.id, node]));
|
|
196
235
|
sizes = nodes.map((node) => pixels(node.size));
|
|
236
|
+
hovered = undefined;
|
|
197
237
|
links = next.filter((link) => index.has(link.source) && index.has(link.target));
|
|
238
|
+
linksOf = new Map();
|
|
239
|
+
links.forEach((link, at) => {
|
|
240
|
+
for (const end of [link.source, link.target]) {
|
|
241
|
+
const list = linksOf.get(end);
|
|
242
|
+
if (list) {
|
|
243
|
+
list.push(at);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
linksOf.set(end, [at]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
198
250
|
const pairs = new Float32Array(links.length * 2);
|
|
199
251
|
links.forEach((link, at) => {
|
|
200
252
|
pairs[at * 2] = index.get(link.source);
|
|
@@ -215,14 +267,8 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
215
267
|
};
|
|
216
268
|
graph.setConfigPartial({
|
|
217
269
|
backgroundColor: [0, 0, 0, 0],
|
|
218
|
-
onPointMouseOut: () =>
|
|
219
|
-
|
|
220
|
-
paint();
|
|
221
|
-
},
|
|
222
|
-
onPointMouseOver: (at) => {
|
|
223
|
-
hovered = at;
|
|
224
|
-
paint();
|
|
225
|
-
},
|
|
270
|
+
onPointMouseOut: () => hover(undefined),
|
|
271
|
+
onPointMouseOver: (at) => hover(at),
|
|
226
272
|
onSimulationTick: redraw,
|
|
227
273
|
onTransition: redraw,
|
|
228
274
|
onZoom: () => {
|
|
@@ -243,11 +289,15 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
243
289
|
center: (id, animate) => {
|
|
244
290
|
const at = index.get(id);
|
|
245
291
|
if (at !== undefined) {
|
|
292
|
+
moved = true;
|
|
246
293
|
graph.zoomToPointByIndex(at, animate ? STEP * 2 : 0, graph.getZoomLevel(), true);
|
|
247
294
|
}
|
|
248
295
|
},
|
|
249
296
|
draw: ({ links: next, nodes }) => {
|
|
250
|
-
|
|
297
|
+
let reach = 1;
|
|
298
|
+
for (const node of nodes) {
|
|
299
|
+
reach = Math.max(reach, Math.abs(node.x), Math.abs(node.y));
|
|
300
|
+
}
|
|
251
301
|
scale = (space * 0.12) / reach;
|
|
252
302
|
moved = false;
|
|
253
303
|
const positions = new Float32Array(nodes.length * 2);
|
|
@@ -259,11 +309,41 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
259
309
|
fitAll(0);
|
|
260
310
|
},
|
|
261
311
|
fit: (fitted, animate) => {
|
|
262
|
-
const
|
|
312
|
+
const flat = flatNow();
|
|
313
|
+
const box = { bottom: -Infinity, left: Infinity, right: -Infinity, top: Infinity };
|
|
314
|
+
for (const id of fitted) {
|
|
263
315
|
const at = index.get(id);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
316
|
+
if (at !== undefined) {
|
|
317
|
+
const x = flat[at * 2] ?? 0;
|
|
318
|
+
const y = flat[at * 2 + 1] ?? 0;
|
|
319
|
+
box.left = Math.min(box.left, x);
|
|
320
|
+
box.right = Math.max(box.right, x);
|
|
321
|
+
box.top = Math.min(box.top, y);
|
|
322
|
+
box.bottom = Math.max(box.bottom, y);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!Number.isFinite(box.left)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
let low = Infinity;
|
|
329
|
+
let high = -Infinity;
|
|
330
|
+
for (let at = 0; at < flat.length; at++) {
|
|
331
|
+
low = Math.min(low, flat[at]);
|
|
332
|
+
high = Math.max(high, flat[at]);
|
|
333
|
+
}
|
|
334
|
+
const least = Math.max((high - low) * 0.2, 1);
|
|
335
|
+
const middle = { x: (box.left + box.right) / 2, y: (box.top + box.bottom) / 2 };
|
|
336
|
+
const half = {
|
|
337
|
+
x: Math.max(box.right - box.left, least) / 2,
|
|
338
|
+
y: Math.max(box.bottom - box.top, least) / 2,
|
|
339
|
+
};
|
|
340
|
+
moved = true;
|
|
341
|
+
graph.setZoomTransformByPointPositions(new Float32Array([
|
|
342
|
+
middle.x - half.x,
|
|
343
|
+
middle.y - half.y,
|
|
344
|
+
middle.x + half.x,
|
|
345
|
+
middle.y + half.y,
|
|
346
|
+
]), animate && !still() ? STEP * 2 : 0, undefined, 0.25);
|
|
267
347
|
},
|
|
268
348
|
gestures: (next) => {
|
|
269
349
|
held.set(next);
|
|
@@ -309,6 +389,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
309
389
|
},
|
|
310
390
|
onPress: (press) => {
|
|
311
391
|
graph.setConfigPartial({
|
|
392
|
+
onBackgroundClick: () => press(undefined),
|
|
312
393
|
onPointClick: (at) => {
|
|
313
394
|
const id = ids[at];
|
|
314
395
|
if (id !== undefined) {
|
|
@@ -316,11 +397,12 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
316
397
|
}
|
|
317
398
|
},
|
|
318
399
|
});
|
|
319
|
-
return () => graph.setConfigPartial({ onPointClick: undefined });
|
|
400
|
+
return () => graph.setConfigPartial({ onBackgroundClick: undefined, onPointClick: undefined });
|
|
320
401
|
},
|
|
321
402
|
panBy: (x, y, animate) => {
|
|
322
403
|
const { height, width } = container.getBoundingClientRect();
|
|
323
404
|
const to = graph.screenToSpacePosition([width / 2 + x, height / 2 + y]);
|
|
405
|
+
moved = true;
|
|
324
406
|
graph.setZoomTransformByPointPositions(new Float32Array(to), animate ? STEP : 0, graph.getZoomLevel());
|
|
325
407
|
},
|
|
326
408
|
settle: (unplaced, placed) => {
|
|
@@ -349,6 +431,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
349
431
|
clearInterval(fitting);
|
|
350
432
|
graph.setConfigPartial({ onSimulationEnd: undefined });
|
|
351
433
|
graph.setPinnedPoints(null);
|
|
434
|
+
graph.pause();
|
|
352
435
|
};
|
|
353
436
|
},
|
|
354
437
|
signal: (from, to, colors, animate) => {
|
|
@@ -359,7 +442,9 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
359
442
|
stop: () => {
|
|
360
443
|
stopped = true;
|
|
361
444
|
clearTimeout(overviewing);
|
|
445
|
+
container.style.cursor = cursor;
|
|
362
446
|
graph.setConfigPartial({
|
|
447
|
+
onBackgroundClick: undefined,
|
|
363
448
|
onPointClick: undefined,
|
|
364
449
|
onPointMouseOut: undefined,
|
|
365
450
|
onPointMouseOver: undefined,
|
|
@@ -396,11 +481,8 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
396
481
|
for (const node of changes.addNodes) {
|
|
397
482
|
kept.set(node.id, node);
|
|
398
483
|
}
|
|
399
|
-
const removed = new Set(changes.removeLinks.map(
|
|
400
|
-
const next = [
|
|
401
|
-
...links.filter((link) => !removed.has(`${link.source}\u0000${link.target}`)),
|
|
402
|
-
...changes.addLinks,
|
|
403
|
-
];
|
|
484
|
+
const removed = new Set(changes.removeLinks.map(linkKey));
|
|
485
|
+
const next = [...links.filter((link) => !removed.has(linkKey(link))), ...changes.addLinks];
|
|
404
486
|
const nodes = [...kept.values()];
|
|
405
487
|
const positions = new Float32Array(nodes.length * 2);
|
|
406
488
|
nodes.forEach((node, at) => {
|
|
@@ -413,6 +495,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
413
495
|
}
|
|
414
496
|
},
|
|
415
497
|
zoomBy: (direction, animate) => {
|
|
498
|
+
moved = true;
|
|
416
499
|
graph.setZoomLevel(graph.getZoomLevel() * (direction > 0 ? ZOOM : 1 / ZOOM), animate ? STEP : 0);
|
|
417
500
|
},
|
|
418
501
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { type ConnectGraphOptions, connectGraph, type DrawnLink, type DrawnNode, type GraphChanges, type GraphConnection, type GraphDriver, type GraphGroup, } from './connect-graph/connect-graph.js';
|
|
2
|
-
export { diffGraph } from './connect-graph/diff.js';
|
|
3
|
-
export { type GraphColors, type GraphLooks, graphColorsFrom, type LinkLook, type NodeLook, } from './connect-graph/looks.js';
|
|
4
|
-
export { type GraphPoint, seedPositions } from './connect-graph/seed.js';
|
|
2
|
+
export { diffGraph, linkKey } from './connect-graph/diff.js';
|
|
3
|
+
export { type GraphColors, type GraphLooks, type GraphTone, graphColorsFrom, type LinkLook, type NodeLook, } from './connect-graph/looks.js';
|
|
4
|
+
export { type GraphPoint, type SeedLink, type SeedNode, seedPositions, } from './connect-graph/seed.js';
|
|
5
5
|
export { fitToStencil, type GraphStencil, type StencilFrame, } from './connect-graph/stencil.js';
|
|
6
|
+
export type { ShapeField } from './shapes/shape-field.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { connectGraph, } from './connect-graph/connect-graph.js';
|
|
2
|
-
export { diffGraph } from './connect-graph/diff.js';
|
|
2
|
+
export { diffGraph, linkKey } from './connect-graph/diff.js';
|
|
3
3
|
export { graphColorsFrom, } from './connect-graph/looks.js';
|
|
4
|
-
export { seedPositions } from './connect-graph/seed.js';
|
|
4
|
+
export { seedPositions, } from './connect-graph/seed.js';
|
|
5
5
|
export { fitToStencil, } from './connect-graph/stencil.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { shapeFromModel } from './model.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { shapeFromModel } from './model.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ShapeField } from '../
|
|
2
|
-
import { type TriangleOptions } from '
|
|
1
|
+
import type { ShapeField } from '../shapes/shape-field.js';
|
|
2
|
+
import { type TriangleOptions } from '../shapes/triangles.js';
|
|
3
3
|
/**
|
|
4
4
|
* A solid field from a glTF model, the web's standard format for one: a
|
|
5
5
|
* URL to a `.glb` or `.gltf` file, or the file's bytes. Every mesh in it is
|
|
@@ -10,6 +10,7 @@ import { type TriangleOptions } from './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.
|
|
13
|
+
* this package. A model that cannot be fetched or read is an error that
|
|
14
|
+
* names it, the loader's own error as its `cause`.
|
|
14
15
|
*/
|
|
15
16
|
export declare const shapeFromModel: (source: string | ArrayBuffer, options?: TriangleOptions) => Promise<ShapeField>;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { shapeFromTriangles } from '
|
|
1
|
+
import { shapeFromTriangles } from '../shapes/triangles.js';
|
|
2
2
|
export const shapeFromModel = async (source, options = {}) => {
|
|
3
|
+
const which = typeof source === 'string' ? `The model ${source}` : 'The model given as bytes';
|
|
3
4
|
const [{ GLTFLoader }, { Mesh, Vector3 }] = await Promise.all([
|
|
4
5
|
import('three/addons/loaders/GLTFLoader.js'),
|
|
5
6
|
import('three'),
|
|
6
|
-
])
|
|
7
|
+
]).catch((cause) => {
|
|
8
|
+
throw new Error(`${which} could not be read, because three.js could not be loaded. Install three alongside @fusion.dev/helix-graph.`, { cause });
|
|
9
|
+
});
|
|
7
10
|
const loader = new GLTFLoader();
|
|
8
|
-
const model = typeof source === 'string'
|
|
9
|
-
?
|
|
10
|
-
:
|
|
11
|
+
const model = await (typeof source === 'string'
|
|
12
|
+
? loader.loadAsync(source)
|
|
13
|
+
: loader.parseAsync(source, '')).catch((cause) => {
|
|
14
|
+
throw new Error(`${which} could not be read as a glTF model: ${String(cause)}`, { cause });
|
|
15
|
+
});
|
|
11
16
|
model.scene.updateMatrixWorld(true);
|
|
12
17
|
const corners = [];
|
|
13
18
|
const names = [];
|
|
@@ -7,11 +7,6 @@ export type OnScreen = (id: string) => {
|
|
|
7
7
|
x: number;
|
|
8
8
|
y: number;
|
|
9
9
|
} | undefined;
|
|
10
|
-
/** A point and how to bend toward it: the control point of a curve on screen. */
|
|
11
|
-
export type Bend = (from: string, to: string) => {
|
|
12
|
-
x: number;
|
|
13
|
-
y: number;
|
|
14
|
-
} | undefined;
|
|
15
10
|
/** Whether an `rgb()` color is light, which a light ink says the ground is dark. */
|
|
16
11
|
export declare const isLight: (color: string) => boolean;
|
|
17
12
|
/** Eases in and out, so a signal leaves and arrives gently. */
|
|
@@ -23,13 +18,19 @@ export interface Box {
|
|
|
23
18
|
right: number;
|
|
24
19
|
top: number;
|
|
25
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* How many pixels a layer has for each of the page's: the screen's own, and
|
|
23
|
+
* never fewer than two, so on a screen of one a name's edges and a signal's
|
|
24
|
+
* tail are drawn twice as finely and smoothed down. At most three.
|
|
25
|
+
*/
|
|
26
|
+
export declare const overlayDensity: () => number;
|
|
26
27
|
/**
|
|
27
28
|
* **Signals are drawn on a canvas of the driver's own**, over the renderer's,
|
|
28
29
|
* so a signal never makes the renderer redraw the graph: ten signals a second
|
|
29
30
|
* over ten thousand notes draw ten dots. It draws only while something is in
|
|
30
31
|
* flight, and takes no pointer, so a press reaches the note under it.
|
|
31
32
|
*/
|
|
32
|
-
export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen
|
|
33
|
+
export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
33
34
|
look: (looks: GraphLooks) => void;
|
|
34
35
|
signal: (from: string, to: string, colors: {
|
|
35
36
|
from: string;
|
|
@@ -50,14 +51,18 @@ export interface Named {
|
|
|
50
51
|
* order given.
|
|
51
52
|
*/
|
|
52
53
|
export declare const namesOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
54
|
+
/** The boxes the names last drawn take. */
|
|
55
|
+
boxes: () => readonly Box[];
|
|
53
56
|
draw: (looks: GraphLooks | undefined, named: readonly Named[]) => void;
|
|
54
57
|
stop: () => void;
|
|
55
58
|
};
|
|
56
59
|
/**
|
|
57
60
|
* **Group names are drawn on a canvas of their own**, under the signals', and
|
|
58
61
|
* drawn again whenever the driver says the view moved, so they follow it.
|
|
62
|
+
* `avoid` says where notes' names are drawn, which a group's name keeps
|
|
63
|
+
* clear of: moved a line, or left out where no line is clear.
|
|
59
64
|
*/
|
|
60
|
-
export declare const groupsOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
65
|
+
export declare const groupsOver: (container: HTMLElement, onScreen: OnScreen, avoid?: () => readonly Box[]) => {
|
|
61
66
|
draw: () => void;
|
|
62
67
|
look: (next: GraphLooks) => void;
|
|
63
68
|
set: (next: readonly GraphGroup[]) => void;
|
|
@@ -69,7 +74,8 @@ export declare const groupsOver: (container: HTMLElement, onScreen: OnScreen) =>
|
|
|
69
74
|
* frame's buttons stopped working with them. A listener on the container in
|
|
70
75
|
* the capture phase runs before the renderer's, and keeps from it what the
|
|
71
76
|
* page has: the plain wheel and one finger while cooperative, and every
|
|
72
|
-
* gesture while a waiting surface is off
|
|
77
|
+
* gesture while a waiting surface is off, a double click among them, which
|
|
78
|
+
* zooms where a renderer zooms on one. A single click still selects a note.
|
|
73
79
|
*/
|
|
74
80
|
export declare const gesturesHeld: (container: HTMLElement) => {
|
|
75
81
|
/**
|