@fusion.dev/helix-graph 0.121.0 → 0.122.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 +55 -2
- package/dist/connect-graph/arrange.d.ts +4 -2
- package/dist/connect-graph/arrange.js +45 -12
- package/dist/connect-graph/connect-graph.d.ts +7 -0
- package/dist/connect-graph/connect-graph.js +7 -3
- package/dist/connect-graph/diff.d.ts +2 -0
- package/dist/connect-graph/diff.js +2 -1
- package/dist/connect-graph/seed.d.ts +3 -0
- package/dist/connect-graph/seed.js +10 -2
- package/dist/connect-graph/shape-field.d.ts +47 -0
- package/dist/connect-graph/shape-field.js +172 -0
- package/dist/connect-graph/stencil.d.ts +33 -17
- package/dist/connect-graph/stencil.js +153 -75
- package/dist/cosmos/cosmos-driver.js +8 -56
- package/dist/overlay/overlay.d.ts +18 -0
- package/dist/overlay/overlay.js +63 -1
- package/dist/shapes/index.d.ts +1 -0
- package/dist/shapes/index.js +1 -0
- package/dist/three/index.d.ts +1 -0
- package/dist/three/index.js +1 -0
- package/dist/three/three-driver.d.ts +32 -0
- package/dist/three/three-driver.js +714 -0
- package/package.json +28 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
const PROBE = 64;
|
|
1
|
+
import { fieldFromInside, fieldFromPath } from './shape-field.js';
|
|
2
|
+
const PROBE = { flat: 64, solid: 32 };
|
|
3
|
+
const SHELL = 0.04;
|
|
3
4
|
const WAVE = 0.07;
|
|
4
5
|
const seeded = (seed) => {
|
|
5
6
|
let state = seed >>> 0 || 1;
|
|
@@ -8,57 +9,60 @@ const seeded = (seed) => {
|
|
|
8
9
|
return state / 4294967296;
|
|
9
10
|
};
|
|
10
11
|
};
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const fields = new WeakMap();
|
|
13
|
+
export const fieldOf = (stencil) => {
|
|
14
|
+
if ('distance' in stencil) {
|
|
15
|
+
return stencil;
|
|
14
16
|
}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const canvas = document.createElement('canvas');
|
|
19
|
-
canvas.width = width;
|
|
20
|
-
canvas.height = height;
|
|
21
|
-
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
22
|
-
if (!context) {
|
|
23
|
-
return () => true;
|
|
17
|
+
const made = fields.get(stencil);
|
|
18
|
+
if (made) {
|
|
19
|
+
return made;
|
|
24
20
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const row = Math.floor(y * scale);
|
|
31
|
-
return (column >= 0 &&
|
|
32
|
-
row >= 0 &&
|
|
33
|
-
column < width &&
|
|
34
|
-
row < height &&
|
|
35
|
-
(alpha[(row * width + column) * 4 + 3] ?? 0) > 127);
|
|
36
|
-
};
|
|
21
|
+
const field = 'path' in stencil
|
|
22
|
+
? fieldFromPath(stencil.path, stencil.width, stencil.height)
|
|
23
|
+
: fieldFromInside((x, y) => stencil.inside(x, y), stencil);
|
|
24
|
+
fields.set(stencil, field);
|
|
25
|
+
return field;
|
|
37
26
|
};
|
|
38
|
-
export const placesIn = (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
export const placesIn = (field, count, { place = 'fill', shell = SHELL } = {}) => {
|
|
28
|
+
const solid = field.depth > 0;
|
|
29
|
+
const { depth, height, width } = field;
|
|
30
|
+
const thickness = shell * Math.max(width, height, depth);
|
|
31
|
+
const takes = (x, y, z) => {
|
|
32
|
+
const distance = field.distance(x, y, z);
|
|
33
|
+
return distance < 0 && (place === 'fill' || distance > -thickness);
|
|
34
|
+
};
|
|
35
|
+
const probe = solid ? PROBE.solid : PROBE.flat;
|
|
36
|
+
let taken = 0;
|
|
37
|
+
for (let layer = 0; layer < (solid ? probe : 1); layer++) {
|
|
38
|
+
for (let row = 0; row < probe; row++) {
|
|
39
|
+
for (let column = 0; column < probe; column++) {
|
|
40
|
+
if (takes(((column + 0.5) / probe) * width, ((row + 0.5) / probe) * height, solid ? ((layer + 0.5) / probe) * depth : 0)) {
|
|
41
|
+
taken += 1;
|
|
42
|
+
}
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
|
-
if (count === 0 ||
|
|
46
|
+
if (count === 0 || taken === 0) {
|
|
48
47
|
return [];
|
|
49
48
|
}
|
|
50
49
|
const random = seeded(count);
|
|
51
|
-
|
|
50
|
+
const room = (taken / probe ** (solid ? 3 : 2)) * width * height * (solid ? depth : 1);
|
|
51
|
+
let step = (room / count) ** (1 / (solid ? 3 : 2));
|
|
52
52
|
for (let tries = 0; tries < 8; tries++) {
|
|
53
53
|
const places = [];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
const layers = solid ? Math.ceil(depth / step) : 1;
|
|
55
|
+
for (let layer = 0; layer < layers; layer++) {
|
|
56
|
+
for (let y = step / 2; y < height; y += step) {
|
|
57
|
+
for (let x = step / 2; x < width; x += step) {
|
|
58
|
+
const place = {
|
|
59
|
+
x: x + (random() - 0.5) * step * 0.8,
|
|
60
|
+
y: y + (random() - 0.5) * step * 0.8,
|
|
61
|
+
...(solid ? { z: (layer + 0.5 + (random() - 0.5) * 0.8) * step } : {}),
|
|
62
|
+
};
|
|
63
|
+
if (takes(place.x, place.y, place.z ?? 0)) {
|
|
64
|
+
places.push(place);
|
|
65
|
+
}
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
}
|
|
@@ -69,53 +73,127 @@ export const placesIn = (inside, width, height, count) => {
|
|
|
69
73
|
}
|
|
70
74
|
return places.slice(0, count);
|
|
71
75
|
}
|
|
72
|
-
step *=
|
|
76
|
+
step *= (places.length / count) ** (1 / (solid ? 3 : 2)) * 0.97 || 0.5;
|
|
73
77
|
}
|
|
74
78
|
return [];
|
|
75
79
|
};
|
|
80
|
+
const nested = (items, keys) => {
|
|
81
|
+
const [key, ...rest] = keys;
|
|
82
|
+
if (!key) {
|
|
83
|
+
return items;
|
|
84
|
+
}
|
|
85
|
+
const sorted = [...items].sort((one, other) => key(one) - key(other));
|
|
86
|
+
if (rest.length === 0) {
|
|
87
|
+
return sorted;
|
|
88
|
+
}
|
|
89
|
+
const slice = Math.ceil(items.length ** (rest.length / keys.length));
|
|
90
|
+
const out = [];
|
|
91
|
+
for (let from = 0; from < sorted.length; from += slice) {
|
|
92
|
+
out.push(...nested(sorted.slice(from, from + slice), rest));
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
};
|
|
76
96
|
export const fitToStencil = (positions, stencil) => {
|
|
77
97
|
const notes = [...positions].map(([id, at]) => ({ at, id }));
|
|
78
98
|
if (notes.length === 0) {
|
|
79
99
|
return new Map();
|
|
80
100
|
}
|
|
81
|
-
const
|
|
101
|
+
const field = fieldOf(stencil);
|
|
102
|
+
const places = placesIn(field, notes.length, {
|
|
103
|
+
place: stencil.place ?? 'fill',
|
|
104
|
+
shell: stencil.shell ?? SHELL,
|
|
105
|
+
});
|
|
82
106
|
if (places.length < notes.length) {
|
|
83
107
|
return new Map(positions);
|
|
84
108
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
const solid = field.depth > 0;
|
|
110
|
+
const low = {
|
|
111
|
+
x: Number.POSITIVE_INFINITY,
|
|
112
|
+
y: Number.POSITIVE_INFINITY,
|
|
113
|
+
z: Number.POSITIVE_INFINITY,
|
|
114
|
+
};
|
|
115
|
+
const high = {
|
|
116
|
+
x: Number.NEGATIVE_INFINITY,
|
|
117
|
+
y: Number.NEGATIVE_INFINITY,
|
|
118
|
+
z: Number.NEGATIVE_INFINITY,
|
|
119
|
+
};
|
|
89
120
|
for (const { at } of notes) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
for (const axis of ['x', 'y', 'z']) {
|
|
122
|
+
const value = at[axis] ?? 0;
|
|
123
|
+
low[axis] = Math.min(low[axis], value);
|
|
124
|
+
high[axis] = Math.max(high[axis], value);
|
|
125
|
+
}
|
|
94
126
|
}
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
const { depth, height, width } = field;
|
|
128
|
+
const scale = Math.max(high.x - low.x, high.y - low.y, solid ? high.z - low.z : 0, 1) /
|
|
129
|
+
Math.max(width, height, depth);
|
|
130
|
+
const middle = { x: (low.x + high.x) / 2, y: (low.y + high.y) / 2, z: (low.z + high.z) / 2 };
|
|
131
|
+
const spots = places.map((place) => {
|
|
132
|
+
const z = place.z ?? depth / 2;
|
|
133
|
+
return {
|
|
134
|
+
across: place.x + WAVE * width * Math.sin((place.y / height) * Math.PI * 3 + 1.3),
|
|
135
|
+
at: {
|
|
136
|
+
x: middle.x + (place.x - width / 2) * scale,
|
|
137
|
+
y: middle.y - (place.y - height / 2) * scale,
|
|
138
|
+
...(solid ? { z: middle.z + (z - depth / 2) * scale } : {}),
|
|
139
|
+
},
|
|
140
|
+
down: -place.y + WAVE * height * Math.sin((place.x / width) * Math.PI * 3 + 0.4),
|
|
141
|
+
};
|
|
142
|
+
});
|
|
109
143
|
const fitted = new Map();
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
if (solid) {
|
|
145
|
+
const surface = (stencil.place ?? 'fill') === 'surface';
|
|
146
|
+
const way = (point) => {
|
|
147
|
+
if (!surface) {
|
|
148
|
+
return point;
|
|
149
|
+
}
|
|
150
|
+
const x = point.x - middle.x;
|
|
151
|
+
const y = point.y - middle.y;
|
|
152
|
+
const z = (point.z ?? 0) - middle.z;
|
|
153
|
+
const length = Math.hypot(x, y, z) || 1;
|
|
154
|
+
return { x: x / length, y: y / length, z: z / length };
|
|
155
|
+
};
|
|
156
|
+
const widest = (points) => {
|
|
157
|
+
let best = 'x';
|
|
158
|
+
let spread = Number.NEGATIVE_INFINITY;
|
|
159
|
+
for (const axis of ['x', 'y', 'z']) {
|
|
160
|
+
let low = Number.POSITIVE_INFINITY;
|
|
161
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
162
|
+
for (const point of points) {
|
|
163
|
+
const value = point[axis] ?? 0;
|
|
164
|
+
low = Math.min(low, value);
|
|
165
|
+
high = Math.max(high, value);
|
|
166
|
+
}
|
|
167
|
+
if (high - low > spread) {
|
|
168
|
+
spread = high - low;
|
|
169
|
+
best = axis;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return best;
|
|
173
|
+
};
|
|
174
|
+
const halve = (ours, theirs) => {
|
|
175
|
+
if (ours.length === 1) {
|
|
176
|
+
fitted.set(ours[0].id, theirs[0].at);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (ours.length === 0) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const mine = widest(ours.map((note) => note.key));
|
|
183
|
+
const yours = widest(theirs.map((spot) => spot.key));
|
|
184
|
+
ours.sort((one, other) => (one.key[mine] ?? 0) - (other.key[mine] ?? 0));
|
|
185
|
+
theirs.sort((one, other) => (one.key[yours] ?? 0) - (other.key[yours] ?? 0));
|
|
186
|
+
const half = Math.floor(ours.length / 2);
|
|
187
|
+
halve(ours.slice(0, half), theirs.slice(0, half));
|
|
188
|
+
halve(ours.slice(half), theirs.slice(half));
|
|
189
|
+
};
|
|
190
|
+
halve(notes.map((note) => ({ ...note, key: way(note.at) })), spots.map((spot) => ({ at: spot.at, key: way(spot.at) })));
|
|
191
|
+
return fitted;
|
|
119
192
|
}
|
|
193
|
+
const ours = nested(notes, [(note) => note.at.x, (note) => note.at.y]);
|
|
194
|
+
const theirs = nested(spots, [(spot) => spot.across, (spot) => spot.down]);
|
|
195
|
+
ours.forEach((note, at) => {
|
|
196
|
+
fitted.set(note.id, theirs[at].at);
|
|
197
|
+
});
|
|
120
198
|
return fitted;
|
|
121
199
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { gesturesHeld, groupsOver, signalsOver } from '../overlay/overlay.js';
|
|
1
|
+
import { gesturesHeld, groupsOver, namesOver, signalsOver, } from '../overlay/overlay.js';
|
|
2
2
|
const SHAPES = { circle: 0, diamond: 3, ring: 0, square: 1 };
|
|
3
3
|
const STEP = 250;
|
|
4
4
|
const FAINT = 0.16;
|
|
@@ -81,61 +81,13 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
81
81
|
const held = gesturesHeld(container);
|
|
82
82
|
const signals = signalsOver(container, onScreen);
|
|
83
83
|
const groups = groupsOver(container, onScreen);
|
|
84
|
-
const names =
|
|
85
|
-
names.setAttribute('aria-hidden', 'true');
|
|
86
|
-
names.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none';
|
|
87
|
-
container.append(names);
|
|
84
|
+
const names = namesOver(container, onScreen);
|
|
88
85
|
const drawNames = () => {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const width = Math.round(container.clientWidth * ratio);
|
|
95
|
-
const height = Math.round(container.clientHeight * ratio);
|
|
96
|
-
if (names.width !== width || names.height !== height) {
|
|
97
|
-
names.width = width;
|
|
98
|
-
names.height = height;
|
|
99
|
-
}
|
|
100
|
-
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
101
|
-
context.clearRect(0, 0, width, height);
|
|
102
|
-
const taken = [];
|
|
103
|
-
const shown = [selected, hovered === undefined ? undefined : ids[hovered], ...near];
|
|
104
|
-
for (const id of shown) {
|
|
105
|
-
if (id === undefined) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const at = onScreen(id);
|
|
109
|
-
const label = looks.node(id);
|
|
110
|
-
if (!at || label.hidden) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const name = drawnNow.get(id)?.label ?? id;
|
|
114
|
-
const strong = id === selected || id === ids[hovered ?? -1];
|
|
115
|
-
context.font = `${strong ? 600 : 500} ${strong ? 13 : 12}px ${looks.font}`;
|
|
116
|
-
const x = at.x + at.radius + 4;
|
|
117
|
-
const y = at.y + 4;
|
|
118
|
-
const box = {
|
|
119
|
-
bottom: y + 4,
|
|
120
|
-
left: x - 2,
|
|
121
|
-
right: x + context.measureText(name).width + 2,
|
|
122
|
-
top: y - 13,
|
|
123
|
-
};
|
|
124
|
-
const free = taken.every((other) => box.right < other.left ||
|
|
125
|
-
box.left > other.right ||
|
|
126
|
-
box.bottom < other.top ||
|
|
127
|
-
box.top > other.bottom);
|
|
128
|
-
if (!free && !strong) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
taken.push(box);
|
|
132
|
-
context.lineJoin = 'round';
|
|
133
|
-
context.lineWidth = 3;
|
|
134
|
-
context.strokeStyle = looks.ground;
|
|
135
|
-
context.strokeText(name, x, y);
|
|
136
|
-
context.fillStyle = strong ? looks.ink : looks.quiet;
|
|
137
|
-
context.fillText(name, x, y);
|
|
138
|
-
}
|
|
86
|
+
const lit = hovered === undefined ? undefined : ids[hovered];
|
|
87
|
+
const shown = [...new Set([selected, lit, ...near])].flatMap((id) => id === undefined
|
|
88
|
+
? []
|
|
89
|
+
: [{ id, name: drawnNow.get(id)?.label ?? id, strong: id === selected || id === lit }]);
|
|
90
|
+
names.draw(looks, shown);
|
|
139
91
|
};
|
|
140
92
|
const redraw = () => {
|
|
141
93
|
drawNames();
|
|
@@ -418,7 +370,7 @@ export const cosmosDriver = (graph, container, options = {}) => {
|
|
|
418
370
|
});
|
|
419
371
|
signals.stop();
|
|
420
372
|
groups.stop();
|
|
421
|
-
names.
|
|
373
|
+
names.stop();
|
|
422
374
|
held.stop();
|
|
423
375
|
},
|
|
424
376
|
update: (changes, animate) => {
|
|
@@ -7,6 +7,8 @@ export type OnScreen = (id: string) => {
|
|
|
7
7
|
x: number;
|
|
8
8
|
y: number;
|
|
9
9
|
} | undefined;
|
|
10
|
+
/** Whether an `rgb()` color is light, which a light ink says the ground is dark. */
|
|
11
|
+
export declare const isLight: (color: string) => boolean;
|
|
10
12
|
/** Eases in and out, so a signal leaves and arrives gently. */
|
|
11
13
|
export declare const ease: (at: number) => number;
|
|
12
14
|
/** A name's box on screen, in pixels. */
|
|
@@ -30,6 +32,22 @@ export declare const signalsOver: (container: HTMLElement, onScreen: OnScreen) =
|
|
|
30
32
|
} | undefined, animate: boolean) => void;
|
|
31
33
|
stop: () => void;
|
|
32
34
|
};
|
|
35
|
+
/** A note to name over the drawing: its name, and whether it is the one the reader is on. */
|
|
36
|
+
export interface Named {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
strong: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* **Names on a canvas of their own**, for a renderer that draws none: each
|
|
43
|
+
* note given, beside it, ringed in the ground's color so it reads over the
|
|
44
|
+
* links. The strong ones always, and the rest where there is room, in the
|
|
45
|
+
* order given.
|
|
46
|
+
*/
|
|
47
|
+
export declare const namesOver: (container: HTMLElement, onScreen: OnScreen) => {
|
|
48
|
+
draw: (looks: GraphLooks | undefined, named: readonly Named[]) => void;
|
|
49
|
+
stop: () => void;
|
|
50
|
+
};
|
|
33
51
|
/**
|
|
34
52
|
* **Group names are drawn on a canvas of their own**, under the signals', and
|
|
35
53
|
* drawn again whenever the driver says the view moved, so they follow it.
|
package/dist/overlay/overlay.js
CHANGED
|
@@ -65,7 +65,7 @@ const arrive = (context, color, at, gone) => {
|
|
|
65
65
|
context.arc(at.x, at.y, reach, 0, Math.PI * 2);
|
|
66
66
|
context.fill();
|
|
67
67
|
};
|
|
68
|
-
const isLight = (color) => {
|
|
68
|
+
export const isLight = (color) => {
|
|
69
69
|
const [red = 0, green = 0, blue = 0] = color.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
70
70
|
return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255 > 0.5;
|
|
71
71
|
};
|
|
@@ -76,6 +76,7 @@ const nameGroups = (context, groups, looks, at) => {
|
|
|
76
76
|
context.textAlign = 'center';
|
|
77
77
|
context.textBaseline = 'middle';
|
|
78
78
|
context.shadowColor = looks.ground;
|
|
79
|
+
const taken = [];
|
|
79
80
|
for (const group of groups) {
|
|
80
81
|
let x = 0;
|
|
81
82
|
let y = 0;
|
|
@@ -100,6 +101,23 @@ const nameGroups = (context, groups, looks, at) => {
|
|
|
100
101
|
continue;
|
|
101
102
|
}
|
|
102
103
|
const name = group.name.toLocaleUpperCase();
|
|
104
|
+
const half = context.measureText(name).width / 2 + 4;
|
|
105
|
+
const boxAt = (middle) => ({
|
|
106
|
+
bottom: middle + 9,
|
|
107
|
+
left: x - half,
|
|
108
|
+
right: x + half,
|
|
109
|
+
top: middle - 9,
|
|
110
|
+
});
|
|
111
|
+
const clear = (box) => taken.every((other) => box.right < other.left ||
|
|
112
|
+
box.left > other.right ||
|
|
113
|
+
box.bottom < other.top ||
|
|
114
|
+
box.top > other.bottom);
|
|
115
|
+
const line = [0, 18, -18].map((step) => y + step).find((middle) => clear(boxAt(middle)));
|
|
116
|
+
if (line === undefined) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
y = line;
|
|
120
|
+
taken.push(boxAt(y));
|
|
103
121
|
context.globalAlpha = group.faded ? 0.3 : 0.92;
|
|
104
122
|
context.fillStyle = mixRgb(group.color, looks.ink, 0.6);
|
|
105
123
|
context.shadowBlur = 10;
|
|
@@ -223,6 +241,50 @@ export const signalsOver = (container, onScreen) => {
|
|
|
223
241
|
},
|
|
224
242
|
};
|
|
225
243
|
};
|
|
244
|
+
export const namesOver = (container, onScreen) => {
|
|
245
|
+
const canvas = layer(container, 'names');
|
|
246
|
+
return {
|
|
247
|
+
draw: (looks, named) => {
|
|
248
|
+
const context = looks ? fresh(canvas, container) : null;
|
|
249
|
+
if (!context || !looks) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const taken = [];
|
|
253
|
+
for (const { id, name, strong } of named) {
|
|
254
|
+
const at = onScreen(id);
|
|
255
|
+
if (!at || looks.node(id).hidden) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
context.font = `${strong ? 600 : 500} ${strong ? 13 : 12}px ${looks.font}`;
|
|
259
|
+
const x = at.x + at.radius + 4;
|
|
260
|
+
const y = at.y + 4;
|
|
261
|
+
const box = {
|
|
262
|
+
bottom: y + 4,
|
|
263
|
+
left: x - 2,
|
|
264
|
+
right: x + context.measureText(name).width + 2,
|
|
265
|
+
top: y - 13,
|
|
266
|
+
};
|
|
267
|
+
const free = taken.every((other) => box.right < other.left ||
|
|
268
|
+
box.left > other.right ||
|
|
269
|
+
box.bottom < other.top ||
|
|
270
|
+
box.top > other.bottom);
|
|
271
|
+
if (!free && !strong) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
taken.push(box);
|
|
275
|
+
context.lineJoin = 'round';
|
|
276
|
+
context.lineWidth = 3;
|
|
277
|
+
context.strokeStyle = looks.ground;
|
|
278
|
+
context.strokeText(name, x, y);
|
|
279
|
+
context.fillStyle = strong ? looks.ink : looks.quiet;
|
|
280
|
+
context.fillText(name, x, y);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
stop: () => {
|
|
284
|
+
canvas.remove();
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
};
|
|
226
288
|
export const groupsOver = (container, onScreen) => {
|
|
227
289
|
let canvas;
|
|
228
290
|
let groups = [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, type ShapeField, } from '../connect-graph/shape-field.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, } from '../connect-graph/shape-field.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type ThreeDriverOptions, threeDriver } from './three-driver.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { threeDriver } from './three-driver.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { GraphDriver } from '../connect-graph/connect-graph.js';
|
|
2
|
+
import type { NodeLook } from '../connect-graph/looks.js';
|
|
3
|
+
/** How the three.js driver behaves where the page does not say. */
|
|
4
|
+
export interface ThreeDriverOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the graph turns slowly while nobody is handling it. On by
|
|
7
|
+
* default, and never for a reader who asks for less motion.
|
|
8
|
+
*/
|
|
9
|
+
autoRotate?: boolean | undefined;
|
|
10
|
+
/** Whether each group's name is drawn over its notes. On by default. */
|
|
11
|
+
groupLabels?: boolean | undefined;
|
|
12
|
+
/** The longest a layout runs, in milliseconds, settled or not. 10 seconds by default. */
|
|
13
|
+
layoutLimit?: number | undefined;
|
|
14
|
+
/** A note's shape where it does not say: `circle` by default, `square` or `diamond`. */
|
|
15
|
+
shape?: NodeLook['shape'];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Drives three.js for a Graph frame: the graph drawn in three dimensions, to
|
|
19
|
+
* be turned and looked into. Pass the box inside the frame the drawing goes
|
|
20
|
+
* in, and `connectGraph` fills it:
|
|
21
|
+
*
|
|
22
|
+
* ```js
|
|
23
|
+
* connectGraph(frame, threeDriver(box))
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* **A layout runs in three dimensions**, with d3-force-3d, where notes have no
|
|
27
|
+
* place; with a solid `stencil`, the notes are drawn in its shape and none
|
|
28
|
+
* runs. Dragging turns the graph, the wheel and a pinch bring it nearer, and
|
|
29
|
+
* it turns slowly by itself while nobody is handling it, unless the reader
|
|
30
|
+
* asks for less motion.
|
|
31
|
+
*/
|
|
32
|
+
export declare const threeDriver: (container: HTMLElement, options?: ThreeDriverOptions) => GraphDriver;
|