@fusion.dev/helix-graph 0.120.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 +194 -8
- package/dist/connect-graph/arrange.d.ts +20 -0
- package/dist/connect-graph/arrange.js +125 -0
- package/dist/connect-graph/connect-graph.d.ts +47 -5
- package/dist/connect-graph/connect-graph.js +86 -19
- package/dist/connect-graph/diff.d.ts +3 -1
- package/dist/connect-graph/diff.js +2 -1
- package/dist/connect-graph/looks.d.ts +29 -3
- package/dist/connect-graph/looks.js +24 -6
- package/dist/connect-graph/seed.d.ts +3 -0
- package/dist/connect-graph/seed.js +21 -4
- 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 +50 -0
- package/dist/connect-graph/stencil.js +199 -0
- package/dist/cosmos/cosmos-driver.d.ts +67 -0
- package/dist/cosmos/cosmos-driver.js +419 -0
- package/dist/cosmos/index.d.ts +1 -0
- package/dist/cosmos/index.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/overlay/overlay.d.ts +77 -0
- package/dist/overlay/overlay.js +358 -0
- package/dist/shapes/index.d.ts +1 -0
- package/dist/shapes/index.js +1 -0
- package/dist/sigma/node-program.d.ts +71 -0
- package/dist/sigma/node-program.js +170 -0
- package/dist/sigma/sigma-driver.d.ts +31 -3
- package/dist/sigma/sigma-driver.js +125 -140
- 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 +47 -7
|
@@ -58,11 +58,31 @@ export interface NodeLook {
|
|
|
58
58
|
* neighbors drew their names over each other in a knot nobody could read.
|
|
59
59
|
*/
|
|
60
60
|
label: 'always' | 'never' | 'room';
|
|
61
|
+
/** How heavily it is drawn, where the note says. */
|
|
62
|
+
emphasis?: 'outlined' | 'solid' | 'subtle' | undefined;
|
|
63
|
+
/** How strongly it glows, from 0 to 1: the selected note's halo. */
|
|
64
|
+
glow: number;
|
|
65
|
+
/** What it is drawn as made of, where the note says. */
|
|
66
|
+
material?: 'frosted' | 'glass' | undefined;
|
|
61
67
|
/** Drawn over the rest, as the selected note and its neighbors are. */
|
|
62
68
|
raised: boolean;
|
|
63
69
|
/** The selected note. */
|
|
64
70
|
selected: boolean;
|
|
71
|
+
/** Its shape, where the note says. */
|
|
72
|
+
shape?: 'circle' | 'diamond' | 'ring' | 'square' | undefined;
|
|
65
73
|
}
|
|
74
|
+
/** A tone from the `tone` axis. */
|
|
75
|
+
export type GraphTone = 'accent' | 'danger' | 'info' | 'neutral' | 'primary' | 'success' | 'warning';
|
|
76
|
+
/** What the looks read off one note. */
|
|
77
|
+
export interface NoteFacts {
|
|
78
|
+
emphasis?: NodeLook['emphasis'];
|
|
79
|
+
group?: string | undefined;
|
|
80
|
+
material?: NodeLook['material'];
|
|
81
|
+
shape?: NodeLook['shape'];
|
|
82
|
+
tone?: GraphTone | undefined;
|
|
83
|
+
}
|
|
84
|
+
/** A tone's color: `neutral` is the muted ink, and no tone is `primary`. */
|
|
85
|
+
export declare const toneColor: (colors: GraphColors, tone: GraphTone | undefined) => string;
|
|
66
86
|
/** How one link is drawn. */
|
|
67
87
|
export interface LinkLook {
|
|
68
88
|
color: string;
|
|
@@ -71,12 +91,18 @@ export interface LinkLook {
|
|
|
71
91
|
}
|
|
72
92
|
/** How every note and link is drawn, asked one at a time, so ten thousand cost nothing until drawn. */
|
|
73
93
|
export interface GraphLooks {
|
|
94
|
+
/** A note's own color, its tone's or its group's, whatever the selection fades. */
|
|
95
|
+
colorOf(id: string): string;
|
|
74
96
|
/** The typeface the labels are drawn in: the first of the frame's own. */
|
|
75
97
|
font: string;
|
|
98
|
+
/** What the drawing is laid on, which a name is ringed in so it reads over the links. */
|
|
99
|
+
ground: string;
|
|
76
100
|
/** The color the labels are drawn in. */
|
|
77
101
|
ink: string;
|
|
78
102
|
/** What a name is drawn on while the pointer is over its note. */
|
|
79
103
|
overlay: string;
|
|
104
|
+
/** The quieter ink a name at rest is drawn in, beside the selected note's and its neighbors'. */
|
|
105
|
+
quiet: string;
|
|
80
106
|
link(source: string, target: string): LinkLook;
|
|
81
107
|
node(id: string): NodeLook;
|
|
82
108
|
}
|
|
@@ -84,8 +110,8 @@ export interface GraphLooks {
|
|
|
84
110
|
* How the graph is drawn for one selection.
|
|
85
111
|
*
|
|
86
112
|
* **The neighborhood stands out and the rest steps back**, which is what
|
|
87
|
-
* selecting a note in a graph of thousands is for: the selected note
|
|
88
|
-
*
|
|
113
|
+
* selecting a note in a graph of thousands is for: the selected note
|
|
114
|
+
* glowing, the notes it links to and from in their groups' colors with
|
|
89
115
|
* their names drawn where there is room, and every other note faded toward
|
|
90
116
|
* the ground, unnamed. With
|
|
91
117
|
* nothing selected, every note is in its group's color.
|
|
@@ -96,9 +122,9 @@ export interface GraphLooks {
|
|
|
96
122
|
export declare const looksOf: (options: {
|
|
97
123
|
colors: GraphColors;
|
|
98
124
|
font?: string | undefined;
|
|
99
|
-
groupOf: (id: string) => string | undefined;
|
|
100
125
|
groups: readonly string[];
|
|
101
126
|
near: ReadonlySet<string>;
|
|
127
|
+
noteOf: (id: string) => NoteFacts | undefined;
|
|
102
128
|
selected: string | undefined;
|
|
103
129
|
shown: ReadonlySet<string> | undefined;
|
|
104
130
|
}) => GraphLooks;
|
|
@@ -54,12 +54,21 @@ export const mixRgb = (from, to, amount) => {
|
|
|
54
54
|
const channel = (at) => Math.round((one[at] ?? 0) + ((other[at] ?? 0) - (one[at] ?? 0)) * amount);
|
|
55
55
|
return `rgb(${channel(0)}, ${channel(1)}, ${channel(2)})`;
|
|
56
56
|
};
|
|
57
|
+
export const toneColor = (colors, tone) => tone === undefined || tone === 'primary'
|
|
58
|
+
? colors.primary
|
|
59
|
+
: tone === 'neutral'
|
|
60
|
+
? colors.muted
|
|
61
|
+
: colors[tone];
|
|
57
62
|
const FADED = 0.75;
|
|
58
63
|
export const looksOf = (options) => {
|
|
59
|
-
const { colors,
|
|
64
|
+
const { colors, near, noteOf, selected, shown } = options;
|
|
60
65
|
const order = new Map([...new Set(options.groups)].sort().map((group, at) => [group, at]));
|
|
61
66
|
const own = (id) => {
|
|
62
|
-
const
|
|
67
|
+
const note = noteOf(id);
|
|
68
|
+
if (note?.tone) {
|
|
69
|
+
return toneColor(colors, note.tone);
|
|
70
|
+
}
|
|
71
|
+
const group = note?.group;
|
|
63
72
|
const at = group === undefined ? undefined : order.get(group);
|
|
64
73
|
return at === undefined || colors.series.length === 0
|
|
65
74
|
? colors.muted
|
|
@@ -68,24 +77,30 @@ export const looksOf = (options) => {
|
|
|
68
77
|
const focused = selected !== undefined;
|
|
69
78
|
const hidden = (id) => shown !== undefined && !shown.has(id);
|
|
70
79
|
return {
|
|
80
|
+
colorOf: own,
|
|
71
81
|
font: options.font || 'sans-serif',
|
|
82
|
+
ground: colors.ground,
|
|
72
83
|
ink: colors.ink,
|
|
73
84
|
link: (source, target) => {
|
|
74
85
|
const touches = focused && (source === selected || target === selected);
|
|
75
86
|
return {
|
|
76
87
|
color: touches
|
|
77
|
-
?
|
|
88
|
+
? own(selected)
|
|
78
89
|
: focused
|
|
79
|
-
? mixRgb(colors.line, colors.ground, 0.
|
|
80
|
-
: colors.
|
|
90
|
+
? mixRgb(colors.line, colors.ground, 0.6)
|
|
91
|
+
: mixRgb(own(source), colors.ground, 0.72),
|
|
81
92
|
hidden: hidden(source) || hidden(target),
|
|
82
93
|
raised: touches,
|
|
83
94
|
};
|
|
84
95
|
},
|
|
85
96
|
node: (id) => {
|
|
97
|
+
const note = noteOf(id);
|
|
98
|
+
const facts = { emphasis: note?.emphasis, material: note?.material, shape: note?.shape };
|
|
86
99
|
if (id === selected) {
|
|
87
100
|
return {
|
|
88
|
-
|
|
101
|
+
...facts,
|
|
102
|
+
color: own(id),
|
|
103
|
+
glow: 1,
|
|
89
104
|
hidden: hidden(id),
|
|
90
105
|
label: 'always',
|
|
91
106
|
raised: true,
|
|
@@ -94,7 +109,9 @@ export const looksOf = (options) => {
|
|
|
94
109
|
}
|
|
95
110
|
const neighbor = focused && near.has(id);
|
|
96
111
|
return {
|
|
112
|
+
...facts,
|
|
97
113
|
color: focused && !neighbor ? mixRgb(own(id), colors.ground, FADED) : own(id),
|
|
114
|
+
glow: 0,
|
|
98
115
|
hidden: hidden(id),
|
|
99
116
|
label: focused && !neighbor ? 'never' : 'room',
|
|
100
117
|
raised: neighbor,
|
|
@@ -102,5 +119,6 @@ export const looksOf = (options) => {
|
|
|
102
119
|
};
|
|
103
120
|
},
|
|
104
121
|
overlay: colors.overlay,
|
|
122
|
+
quiet: colors.muted,
|
|
105
123
|
};
|
|
106
124
|
};
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
export interface GraphPoint {
|
|
3
3
|
x: number;
|
|
4
4
|
y: number;
|
|
5
|
+
/** How near the viewer, for a renderer that draws in three dimensions. */
|
|
6
|
+
z?: number | undefined;
|
|
5
7
|
}
|
|
6
8
|
/** A note as the connector reads it off the frame. */
|
|
7
9
|
export interface SeedNode {
|
|
8
10
|
id: string;
|
|
9
11
|
x?: number | undefined;
|
|
10
12
|
y?: number | undefined;
|
|
13
|
+
z?: number | undefined;
|
|
11
14
|
}
|
|
12
15
|
/** A link as the connector reads it off the frame. */
|
|
13
16
|
export interface SeedLink {
|
|
@@ -13,7 +13,11 @@ export const seedPositions = (nodes, links, known = new Map()) => {
|
|
|
13
13
|
const unplaced = new Set();
|
|
14
14
|
for (const node of nodes) {
|
|
15
15
|
if (Number.isFinite(node.x) && Number.isFinite(node.y)) {
|
|
16
|
-
positions.set(node.id, {
|
|
16
|
+
positions.set(node.id, {
|
|
17
|
+
x: node.x,
|
|
18
|
+
y: node.y,
|
|
19
|
+
...(Number.isFinite(node.z) ? { z: node.z } : {}),
|
|
20
|
+
});
|
|
17
21
|
}
|
|
18
22
|
else {
|
|
19
23
|
const kept = known.get(node.id);
|
|
@@ -23,9 +27,18 @@ export const seedPositions = (nodes, links, known = new Map()) => {
|
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
const neighbors = new Map();
|
|
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
|
+
};
|
|
26
39
|
for (const { source, target } of links) {
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
add(source, target);
|
|
41
|
+
add(target, source);
|
|
29
42
|
}
|
|
30
43
|
let spiral = positions.size;
|
|
31
44
|
for (const node of nodes) {
|
|
@@ -37,7 +50,11 @@ export const seedPositions = (nodes, links, known = new Map()) => {
|
|
|
37
50
|
const beside = placed === undefined ? undefined : positions.get(placed);
|
|
38
51
|
if (beside) {
|
|
39
52
|
const offset = nudge(node.id);
|
|
40
|
-
positions.set(node.id, {
|
|
53
|
+
positions.set(node.id, {
|
|
54
|
+
x: beside.x + offset.x,
|
|
55
|
+
y: beside.y + offset.y,
|
|
56
|
+
...(beside.z === undefined ? {} : { z: beside.z + offset.x * offset.y }),
|
|
57
|
+
});
|
|
41
58
|
continue;
|
|
42
59
|
}
|
|
43
60
|
const radius = Math.sqrt(spiral + 0.5);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A shape as a signed distance field: for any point in its box, how far it is
|
|
3
|
+
* from the shape's edge, below zero inside and above zero outside, in the
|
|
4
|
+
* box's units. **Flat when `depth` is zero**, and solid otherwise.
|
|
5
|
+
*
|
|
6
|
+
* One form for every shape, however it was given, so placing notes inside a
|
|
7
|
+
* shape, on its surface, or at the nearest point inside it is the same work
|
|
8
|
+
* in two dimensions and three.
|
|
9
|
+
*/
|
|
10
|
+
export interface ShapeField {
|
|
11
|
+
/** How deep the box is, from front to back: zero for a flat shape. */
|
|
12
|
+
readonly depth: number;
|
|
13
|
+
/** How far a point is from the shape's edge: below zero inside. `z` is ignored for a flat shape. */
|
|
14
|
+
distance(x: number, y: number, z?: number): number;
|
|
15
|
+
/** How tall the box is. Its y points down, as an SVG's does. */
|
|
16
|
+
readonly height: number;
|
|
17
|
+
/** How wide the box is. */
|
|
18
|
+
readonly width: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A field from a test of whether a point in the box is inside, sampled at
|
|
22
|
+
* the center of each cell: flat when `depth` is zero, solid otherwise.
|
|
23
|
+
*/
|
|
24
|
+
export declare const fieldFromInside: (inside: (x: number, y: number, z: number) => boolean, box: {
|
|
25
|
+
depth?: number | undefined;
|
|
26
|
+
height: number;
|
|
27
|
+
width: number;
|
|
28
|
+
}, resolution?: number) => ShapeField;
|
|
29
|
+
/**
|
|
30
|
+
* A flat field from an SVG path in a box `width` by `height`, its holes cut by
|
|
31
|
+
* the even-odd rule. Drawn once to a canvas and read off it, which is what
|
|
32
|
+
* lets a detailed outline become a field in a moment.
|
|
33
|
+
*/
|
|
34
|
+
export declare const fieldFromPath: (path: string, width: number, height: number, resolution?: number) => ShapeField;
|
|
35
|
+
/**
|
|
36
|
+
* A solid puffed out of a flat shape, as a cushion is: as thick as `depth`
|
|
37
|
+
* where the shape is widest, rounding to nothing at its edge, and cut
|
|
38
|
+
* through wherever the flat shape has a hole.
|
|
39
|
+
*
|
|
40
|
+
* `roundness`, from 0 to 1, is how far in from the edge the rounding runs, as
|
|
41
|
+
* a share of the way to the shape's middle: 1, the default, rounds the whole
|
|
42
|
+
* way, as a pebble; less leaves a flat face, as a coin.
|
|
43
|
+
*/
|
|
44
|
+
export declare const inflate: (flat: ShapeField, { depth, roundness }: {
|
|
45
|
+
depth: number;
|
|
46
|
+
roundness?: number;
|
|
47
|
+
}) => ShapeField;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const FLAT = 512;
|
|
2
|
+
const SOLID = 96;
|
|
3
|
+
const FAR = 1e20;
|
|
4
|
+
const line = (values, count, out) => {
|
|
5
|
+
const hull = new Int32Array(count);
|
|
6
|
+
const bounds = new Float64Array(count + 1);
|
|
7
|
+
const crossing = (at, from) => (values[at] + at * at - (values[from] + from * from)) /
|
|
8
|
+
(2 * at - 2 * from);
|
|
9
|
+
let top = 0;
|
|
10
|
+
hull[0] = 0;
|
|
11
|
+
bounds[0] = -FAR;
|
|
12
|
+
bounds[1] = FAR;
|
|
13
|
+
for (let at = 1; at < count; at++) {
|
|
14
|
+
let cross = crossing(at, hull[top]);
|
|
15
|
+
while (cross <= bounds[top]) {
|
|
16
|
+
top -= 1;
|
|
17
|
+
cross = crossing(at, hull[top]);
|
|
18
|
+
}
|
|
19
|
+
top += 1;
|
|
20
|
+
hull[top] = at;
|
|
21
|
+
bounds[top] = cross;
|
|
22
|
+
bounds[top + 1] = FAR;
|
|
23
|
+
}
|
|
24
|
+
top = 0;
|
|
25
|
+
for (let at = 0; at < count; at++) {
|
|
26
|
+
while (bounds[top + 1] < at) {
|
|
27
|
+
top += 1;
|
|
28
|
+
}
|
|
29
|
+
const from = hull[top];
|
|
30
|
+
out[at] = (at - from) * (at - from) + values[from];
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const transform = (marked, sizes) => {
|
|
34
|
+
const [nx, ny, nz] = sizes;
|
|
35
|
+
const grid = new Float64Array(nx * ny * nz);
|
|
36
|
+
for (let at = 0; at < grid.length; at++) {
|
|
37
|
+
grid[at] = marked[at] ? 0 : FAR;
|
|
38
|
+
}
|
|
39
|
+
const longest = Math.max(nx, ny, nz);
|
|
40
|
+
const values = new Float64Array(longest);
|
|
41
|
+
const out = new Float64Array(longest);
|
|
42
|
+
const pass = (count, stride, starts) => {
|
|
43
|
+
for (const start of starts) {
|
|
44
|
+
for (let step = 0; step < count; step++) {
|
|
45
|
+
values[step] = grid[start + step * stride];
|
|
46
|
+
}
|
|
47
|
+
line(values, count, out);
|
|
48
|
+
for (let step = 0; step < count; step++) {
|
|
49
|
+
grid[start + step * stride] = out[step];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const along = function* (outer, inner, outerStride, innerStride) {
|
|
54
|
+
for (let one = 0; one < outer; one++) {
|
|
55
|
+
for (let two = 0; two < inner; two++) {
|
|
56
|
+
yield one * outerStride + two * innerStride;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
pass(nx, 1, along(nz, ny, nx * ny, nx));
|
|
61
|
+
pass(ny, nx, along(nz, nx, nx * ny, 1));
|
|
62
|
+
if (nz > 1) {
|
|
63
|
+
pass(nz, nx * ny, along(ny, nx, nx, 1));
|
|
64
|
+
}
|
|
65
|
+
for (let at = 0; at < grid.length; at++) {
|
|
66
|
+
grid[at] = Math.sqrt(grid[at]);
|
|
67
|
+
}
|
|
68
|
+
return grid;
|
|
69
|
+
};
|
|
70
|
+
const fieldFromCells = (inside, sizes, cell, box) => {
|
|
71
|
+
const outside = new Uint8Array(inside.length);
|
|
72
|
+
for (let at = 0; at < inside.length; at++) {
|
|
73
|
+
outside[at] = inside[at] ? 0 : 1;
|
|
74
|
+
}
|
|
75
|
+
const toInside = transform(inside, sizes);
|
|
76
|
+
const toOutside = transform(outside, sizes);
|
|
77
|
+
const [nx, ny, nz] = sizes;
|
|
78
|
+
const signed = new Float32Array(inside.length);
|
|
79
|
+
for (let at = 0; at < inside.length; at++) {
|
|
80
|
+
signed[at] = inside[at]
|
|
81
|
+
? -(toOutside[at] - 0.5) * cell
|
|
82
|
+
: (toInside[at] - 0.5) * cell;
|
|
83
|
+
}
|
|
84
|
+
const read = (x, y, z) => signed[Math.min(Math.max(z, 0), nz - 1) * nx * ny +
|
|
85
|
+
Math.min(Math.max(y, 0), ny - 1) * nx +
|
|
86
|
+
Math.min(Math.max(x, 0), nx - 1)];
|
|
87
|
+
return {
|
|
88
|
+
...box,
|
|
89
|
+
distance: (x, y, z = 0) => {
|
|
90
|
+
const fx = x / cell - 0.5;
|
|
91
|
+
const fy = y / cell - 0.5;
|
|
92
|
+
const fz = nz > 1 ? z / cell - 0.5 : 0;
|
|
93
|
+
const x0 = Math.floor(fx);
|
|
94
|
+
const y0 = Math.floor(fy);
|
|
95
|
+
const z0 = Math.floor(fz);
|
|
96
|
+
const tx = fx - x0;
|
|
97
|
+
const ty = fy - y0;
|
|
98
|
+
const tz = fz - z0;
|
|
99
|
+
const plane = (layer) => (read(x0, y0, layer) * (1 - tx) + read(x0 + 1, y0, layer) * tx) * (1 - ty) +
|
|
100
|
+
(read(x0, y0 + 1, layer) * (1 - tx) + read(x0 + 1, y0 + 1, layer) * tx) * ty;
|
|
101
|
+
const within = nz > 1 ? plane(z0) * (1 - tz) + plane(z0 + 1) * tz : plane(0);
|
|
102
|
+
const past = Math.hypot(Math.max(-x, x - box.width, 0), Math.max(-y, y - box.height, 0), nz > 1 ? Math.max(-z, z - box.depth, 0) : 0);
|
|
103
|
+
return past > 0 ? Math.max(within, 0) + past : within;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
export const fieldFromInside = (inside, box, resolution = box.depth ? SOLID : FLAT) => {
|
|
108
|
+
const depth = box.depth ?? 0;
|
|
109
|
+
const cell = Math.max(box.width, box.height, depth) / resolution;
|
|
110
|
+
const nx = Math.max(1, Math.ceil(box.width / cell));
|
|
111
|
+
const ny = Math.max(1, Math.ceil(box.height / cell));
|
|
112
|
+
const nz = depth > 0 ? Math.max(1, Math.ceil(depth / cell)) : 1;
|
|
113
|
+
const cells = new Uint8Array(nx * ny * nz);
|
|
114
|
+
for (let z = 0; z < nz; z++) {
|
|
115
|
+
for (let y = 0; y < ny; y++) {
|
|
116
|
+
for (let x = 0; x < nx; x++) {
|
|
117
|
+
cells[z * nx * ny + y * nx + x] = inside((x + 0.5) * cell, (y + 0.5) * cell, depth > 0 ? (z + 0.5) * cell : 0)
|
|
118
|
+
? 1
|
|
119
|
+
: 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return fieldFromCells(cells, [nx, ny, nz], cell, {
|
|
124
|
+
depth,
|
|
125
|
+
height: box.height,
|
|
126
|
+
width: box.width,
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
export const fieldFromPath = (path, width, height, resolution = FLAT) => {
|
|
130
|
+
const cell = Math.max(width, height, 1) / resolution;
|
|
131
|
+
const nx = Math.max(1, Math.ceil(width / cell));
|
|
132
|
+
const ny = Math.max(1, Math.ceil(height / cell));
|
|
133
|
+
const canvas = document.createElement('canvas');
|
|
134
|
+
canvas.width = nx;
|
|
135
|
+
canvas.height = ny;
|
|
136
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
137
|
+
const cells = new Uint8Array(nx * ny);
|
|
138
|
+
if (context) {
|
|
139
|
+
context.scale(1 / cell, 1 / cell);
|
|
140
|
+
context.fill(new Path2D(path), 'evenodd');
|
|
141
|
+
const alpha = context.getImageData(0, 0, nx, ny).data;
|
|
142
|
+
for (let at = 0; at < cells.length; at++) {
|
|
143
|
+
cells[at] = (alpha[at * 4 + 3] ?? 0) > 127 ? 1 : 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
cells.fill(1);
|
|
148
|
+
}
|
|
149
|
+
return fieldFromCells(cells, [nx, ny, 1], cell, { depth: 0, height, width });
|
|
150
|
+
};
|
|
151
|
+
export const inflate = (flat, { depth, roundness = 1 }) => {
|
|
152
|
+
let deepest = 0;
|
|
153
|
+
const probes = 128;
|
|
154
|
+
for (let row = 0; row < probes; row++) {
|
|
155
|
+
for (let column = 0; column < probes; column++) {
|
|
156
|
+
deepest = Math.max(deepest, -flat.distance(((column + 0.5) / probes) * flat.width, ((row + 0.5) / probes) * flat.height));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const reach = Math.max(deepest * Math.min(Math.max(roundness, 0.01), 1), 1e-6);
|
|
160
|
+
const middle = depth / 2;
|
|
161
|
+
return {
|
|
162
|
+
depth,
|
|
163
|
+
distance: (x, y, z = middle) => {
|
|
164
|
+
const across = flat.distance(x, y);
|
|
165
|
+
const inset = Math.min(Math.max(-across, 0) / reach, 1);
|
|
166
|
+
const half = middle * Math.sqrt(1 - (1 - inset) ** 2);
|
|
167
|
+
return Math.max(across, Math.abs(z - middle) - half);
|
|
168
|
+
},
|
|
169
|
+
height: flat.height,
|
|
170
|
+
width: flat.width,
|
|
171
|
+
};
|
|
172
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { GraphPoint } from './seed.js';
|
|
2
|
+
import { type ShapeField } from './shape-field.js';
|
|
3
|
+
/**
|
|
4
|
+
* A shape the notes are drawn in. **Any shape field**, flat or solid, such as
|
|
5
|
+
* `inflate` makes; or, flat, an SVG path in a box `width` by `height`, as an
|
|
6
|
+
* SVG's `viewBox="0 0 width height"` gives one, whose holes are cut by the
|
|
7
|
+
* even-odd rule; or a test of whether a point in that box is inside.
|
|
8
|
+
*/
|
|
9
|
+
export type GraphStencil = (ShapeField | {
|
|
10
|
+
height: number;
|
|
11
|
+
path: string;
|
|
12
|
+
width: number;
|
|
13
|
+
} | {
|
|
14
|
+
height: number;
|
|
15
|
+
inside: (x: number, y: number) => boolean;
|
|
16
|
+
width: number;
|
|
17
|
+
}) & {
|
|
18
|
+
/**
|
|
19
|
+
* Where in the shape the notes go: `fill`, all through it, which unset is;
|
|
20
|
+
* or `surface`, a thin shell just inside its edge, which is how a solid
|
|
21
|
+
* reads as the thing it is shaped like.
|
|
22
|
+
*/
|
|
23
|
+
place?: 'fill' | 'surface' | undefined;
|
|
24
|
+
/** How thick the surface's shell is, as a share of the shape's size. 0.04 by default. */
|
|
25
|
+
shell?: number | undefined;
|
|
26
|
+
};
|
|
27
|
+
/** The field a stencil is, or is made into. */
|
|
28
|
+
export declare const fieldOf: (stencil: GraphStencil) => ShapeField;
|
|
29
|
+
/**
|
|
30
|
+
* `count` places spread evenly over a shape: through all of it, or through a
|
|
31
|
+
* shell just inside its edge. A grid sized to the room there is, each place
|
|
32
|
+
* shaken within its cell so no rows show, and made finer until it holds
|
|
33
|
+
* enough.
|
|
34
|
+
*/
|
|
35
|
+
export declare const placesIn: (field: ShapeField, count: number, { place, shell }?: {
|
|
36
|
+
place?: 'fill' | 'surface';
|
|
37
|
+
shell?: number;
|
|
38
|
+
}) => GraphPoint[];
|
|
39
|
+
/**
|
|
40
|
+
* Places a layout or an arrangement put anywhere, moved into the stencil's
|
|
41
|
+
* shape and spread evenly over it. **The notes keep their order**, left to
|
|
42
|
+
* right, top to bottom, and front to back in a solid, so notes put together
|
|
43
|
+
* land together, and each group takes a region of the shape.
|
|
44
|
+
*
|
|
45
|
+
* Centered where the places were, and as large. The stencil's y points down,
|
|
46
|
+
* as an SVG's does, and the places' up, as Sigma, cosmos.gl and three.js draw
|
|
47
|
+
* them. Places the shape cannot hold, as when it has no inside, come back as
|
|
48
|
+
* they were.
|
|
49
|
+
*/
|
|
50
|
+
export declare const fitToStencil: (positions: ReadonlyMap<string, GraphPoint>, stencil: GraphStencil) => Map<string, GraphPoint>;
|