@fusion.dev/helix-graph 0.121.0 → 0.123.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.
Files changed (37) hide show
  1. package/README.md +75 -2
  2. package/dist/connect-graph/arrange.d.ts +4 -2
  3. package/dist/connect-graph/arrange.js +45 -12
  4. package/dist/connect-graph/connect-graph.d.ts +10 -1
  5. package/dist/connect-graph/connect-graph.js +13 -4
  6. package/dist/connect-graph/diff.d.ts +2 -0
  7. package/dist/connect-graph/diff.js +2 -1
  8. package/dist/connect-graph/seed.d.ts +3 -0
  9. package/dist/connect-graph/seed.js +10 -2
  10. package/dist/connect-graph/shape-field.d.ts +61 -0
  11. package/dist/connect-graph/shape-field.js +172 -0
  12. package/dist/connect-graph/stencil.d.ts +56 -17
  13. package/dist/connect-graph/stencil.js +160 -79
  14. package/dist/connect-graph/surface-web.d.ts +18 -0
  15. package/dist/connect-graph/surface-web.js +99 -0
  16. package/dist/cosmos/cosmos-driver.js +8 -56
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/overlay/overlay.d.ts +18 -0
  20. package/dist/overlay/overlay.js +63 -1
  21. package/dist/shapes/flat.d.ts +28 -0
  22. package/dist/shapes/flat.js +63 -0
  23. package/dist/shapes/index.d.ts +5 -0
  24. package/dist/shapes/index.js +5 -0
  25. package/dist/shapes/model.d.ts +12 -0
  26. package/dist/shapes/model.js +31 -0
  27. package/dist/shapes/solids.d.ts +28 -0
  28. package/dist/shapes/solids.js +47 -0
  29. package/dist/shapes/triangles.d.ts +20 -0
  30. package/dist/shapes/triangles.js +78 -0
  31. package/dist/three/index.d.ts +1 -0
  32. package/dist/three/index.js +1 -0
  33. package/dist/three/surface.d.ts +13 -0
  34. package/dist/three/surface.js +112 -0
  35. package/dist/three/three-driver.d.ts +32 -0
  36. package/dist/three/three-driver.js +797 -0
  37. package/package.json +28 -7
package/README.md CHANGED
@@ -205,6 +205,77 @@ layout beside its neighbors. `stencil` also takes `inside(x, y)`, a test of
205
205
  whether a point in the box is in the shape, in place of `path`, and
206
206
  `fitToStencil` fits places a page has into one.
207
207
 
208
+ **Every shape becomes a distance field**: for each point in its box, how far
209
+ it is from the shape's edge, below zero inside. `@fusion.dev/helix-graph/shapes`
210
+ makes one from what a page has, and a field is a stencil itself:
211
+
212
+ | Function | Makes |
213
+ |---|---|
214
+ | `shapeFromModel(url)` | A solid from a glTF model, `.glb` or `.gltf`, every mesh where the model places it. Async; three.js reads the file |
215
+ | `shapeFromTriangles(positions, index)` | A solid from a closed surface of triangles a page already holds |
216
+ | `shapeFromSvg(svg)` | A flat field from an `<svg>` or its markup: every path, rectangle, circle, ellipse and polygon in its `viewBox` |
217
+ | `shapeFromImage(image, { threshold })` | A flat field from an image's opaque pixels |
218
+ | `shapeFromPath(path, width, height)` | A flat field from an SVG path |
219
+ | `shapeFromInside(inside, { width, height, depth })` | A flat field, or a solid where `depth` is given, from a test of what is inside |
220
+ | `inflate(field, { depth, roundness })` | A solid puffed out of a flat field, as a cushion is, cut through where the flat shape has a hole |
221
+ | `sphere`, `torus`, `box` | Solids measured exactly from their formulas |
222
+ | `onSphere(field, { radius, thickness })` | A flat map wrapped round a ball, as a world map is round a globe |
223
+
224
+ ```js
225
+ const stencil = { ...(await shapeFromModel('/models/brain.glb')), place: 'surface' }
226
+ connectGraph(frame, threeDriver(box), { stencil })
227
+ ```
228
+
229
+ **A model is read a column of cells at a time**, so one of a hundred thousand
230
+ triangles becomes a field in a moment. Any closed mesh works; a mesh with
231
+ holes is read as closed where it can be. A model served from another origin
232
+ needs that origin to allow it, as any `fetch` does. Use a model whose license
233
+ lets your readers copy it, since the examples a reader takes away name it.
234
+
235
+ ## In three dimensions
236
+
237
+ **`threeDriver(box)` draws the graph in three dimensions** with three.js, to
238
+ be turned and looked into. The frame, the list, selection, names and signals
239
+ are the same as in two:
240
+
241
+ ```sh
242
+ bun add three d3-force-3d
243
+ ```
244
+
245
+ ```js
246
+ import { connectGraph } from '@fusion.dev/helix-graph'
247
+ import { inflate, shapeFromPath } from '@fusion.dev/helix-graph/shapes'
248
+ import { threeDriver } from '@fusion.dev/helix-graph/three'
249
+
250
+ connectGraph(frame, threeDriver(box), {
251
+ stencil: { ...inflate(shapeFromPath(path, 220, 170), { depth: 140 }), place: 'surface' },
252
+ })
253
+ ```
254
+
255
+ - **A solid stencil draws the notes in the solid**: all through it, or, with
256
+ `place: 'surface'`, in a thin shell just inside its edge, which is how a
257
+ solid reads as the thing it is shaped like. Each group takes one piece of
258
+ the surface, measured across it, so a group on a knot is a stretch of its
259
+ tube, and each hub's notes a patch around it.
260
+ - **On a surface, the shape itself is drawn** in the ground's color just
261
+ inside the notes, so what is behind it is hidden, and the pointer finds
262
+ only the notes in front.
263
+ - **Without a stencil, a layout runs in three dimensions** with d3-force-3d.
264
+ - **Dragging turns the graph, and the wheel and a pinch bring it nearer.** It
265
+ turns slowly by itself while nobody is handling it, and never for a reader
266
+ who asks for less motion.
267
+ - **The far side recedes.** Through a graph, its far notes are a little
268
+ quieter than its near ones.
269
+ - **Each group is named over its near side**, and a group's name moves a line
270
+ where it would cover another's.
271
+
272
+ | `threeDriver` option | Default | What else it takes |
273
+ |---|---|---|
274
+ | `autoRotate` | `true` | `false` leaves the graph still until the reader turns it |
275
+ | `shape` | `circle` | `square` or `diamond`, for every note that does not say |
276
+ | `groupLabels` | `true` | `false` leaves each group unnamed |
277
+ | `layoutLimit` | `10000` | The longest a layout runs, in milliseconds |
278
+
208
279
  ## What it exports
209
280
 
210
281
  | Import | What |
@@ -212,8 +283,10 @@ whether a point in the box is in the shape, in place of `path`, and
212
283
  | `@fusion.dev/helix-graph` | `connectGraph`, `diffGraph`, `fitToStencil`, `graphColorsFrom`, `seedPositions`, and the `GraphDriver`, `GraphChanges` and `GraphStencil` types |
213
284
  | `@fusion.dev/helix-graph/sigma` | `sigmaDriver(sigma, options)` |
214
285
  | `@fusion.dev/helix-graph/cosmos` | `cosmosDriver(graph, box, options)` |
286
+ | `@fusion.dev/helix-graph/three` | `threeDriver(box, options)` |
287
+ | `@fusion.dev/helix-graph/shapes` | `shapeFromModel`, `shapeFromTriangles`, `shapeFromSvg`, `shapeFromImage`, `shapeFromPath`, `shapeFromInside`, `inflate`, `sphere`, `torus`, `box`, `onSphere`, and the `ShapeField` type |
215
288
 
216
- **Sigma and cosmos.gl are optional peers, and graphology is the page's.** Each
289
+ **Sigma, cosmos.gl, three.js and d3-force-3d are optional peers, and graphology is the page's.** Each
217
290
  driver types its renderer by the calls it makes on it, so a page brings
218
291
  whichever version it has, and a spec holds those shapes to the real classes,
219
292
  so a renamed method fails a typecheck here before it reaches a page. `@fusion.dev/helix-graph/sigma` imports
@@ -223,7 +296,7 @@ Sigma 3 is a peer dependency.
223
296
  | `connectGraph` option | Default | What else it takes |
224
297
  |---|---|---|
225
298
  | `center` | `true` | `false` leaves the view where it is when a note is chosen in the list |
226
- | `stencil` | none | A shape to draw the notes in: `{ path, width, height }` or `{ inside, width, height }` |
299
+ | `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` |
227
300
 
228
301
  | `sigmaDriver` option | Default | What else it takes |
229
302
  |---|---|---|
@@ -6,7 +6,7 @@ export interface ArrangedNode {
6
6
  }
7
7
  /**
8
8
  * Places for every note from the graph's structure alone, with no layout to
9
- * wait for: **each group around a circle, each hub in its group, and each
9
+ * wait for: **each group around a circle, or a sphere in a solid, each hub in its group, and each
10
10
  * note beside the hub it hangs from**, the most connected note it links to
11
11
  * in its group.
12
12
  *
@@ -15,4 +15,6 @@ export interface ArrangedNode {
15
15
  * so the shape is drawn from the first frame with each group a region and
16
16
  * each hub's notes around it.
17
17
  */
18
- export declare const arrangeByGroup: (nodes: readonly ArrangedNode[], links: readonly SeedLink[]) => Map<string, GraphPoint>;
18
+ export declare const arrangeByGroup: (nodes: readonly ArrangedNode[], links: readonly SeedLink[], { solid }?: {
19
+ solid?: boolean;
20
+ }) => Map<string, GraphPoint>;
@@ -1,9 +1,36 @@
1
1
  const TURN = Math.PI * (3 - Math.sqrt(5));
2
- const spiral = (at, spacing) => ({
3
- x: Math.cos(at * TURN) * Math.sqrt(at + 0.5) * spacing,
4
- y: Math.sin(at * TURN) * Math.sqrt(at + 0.5) * spacing,
2
+ const STEP = (Math.sqrt(5) - 1) / 2;
3
+ const spiral = (at, spacing, solid) => {
4
+ if (!solid) {
5
+ return {
6
+ x: Math.cos(at * TURN) * Math.sqrt(at + 0.5) * spacing,
7
+ y: Math.sin(at * TURN) * Math.sqrt(at + 0.5) * spacing,
8
+ };
9
+ }
10
+ const height = 1 - 2 * ((at * STEP) % 1);
11
+ const around = Math.sqrt(1 - height * height);
12
+ const out = Math.cbrt(at + 0.5) * spacing;
13
+ return {
14
+ x: Math.cos(at * TURN) * around * out,
15
+ y: height * out,
16
+ z: Math.sin(at * TURN) * around * out,
17
+ };
18
+ };
19
+ const around = (at, count, solid) => {
20
+ if (!solid) {
21
+ const turn = (at / count) * Math.PI * 2;
22
+ return { x: Math.cos(turn), y: Math.sin(turn) };
23
+ }
24
+ const height = 1 - (2 * (at + 0.5)) / count;
25
+ const ring = Math.sqrt(1 - height * height);
26
+ return { x: Math.cos(at * TURN) * ring, y: height, z: Math.sin(at * TURN) * ring };
27
+ };
28
+ const plus = (one, other) => ({
29
+ x: one.x + other.x,
30
+ y: one.y + other.y,
31
+ ...(one.z === undefined && other.z === undefined ? {} : { z: (one.z ?? 0) + (other.z ?? 0) }),
5
32
  });
6
- export const arrangeByGroup = (nodes, links) => {
33
+ export const arrangeByGroup = (nodes, links, { solid = false } = {}) => {
7
34
  const groupOf = new Map(nodes.map((node) => [node.id, node.group ?? '']));
8
35
  const degree = new Map();
9
36
  const neighbors = new Map();
@@ -65,26 +92,32 @@ export const arrangeByGroup = (nodes, links) => {
65
92
  for (const members of hubs.values()) {
66
93
  count += members.length + 1;
67
94
  }
68
- return Math.sqrt(count) * 1.5;
95
+ return (solid ? Math.cbrt(count) : Math.sqrt(count)) * 1.5;
69
96
  };
70
97
  const radii = [...groups.values()].map(radiusOf);
71
98
  const widest = Math.max(0, ...radii);
72
- const ring = groups.size > 1 ? (widest * 1.2) / Math.sin(Math.PI / groups.size) : 0;
99
+ const ring = groups.size > 1
100
+ ? solid
101
+ ? (widest * 2.4) / Math.sqrt((4 * Math.PI) / groups.size)
102
+ : (widest * 1.2) / Math.sin(Math.PI / groups.size)
103
+ : 0;
73
104
  const positions = new Map();
74
105
  [...groups.values()].forEach((hubs, at) => {
75
- const turn = (at / groups.size) * Math.PI * 2;
76
- const middle = { x: Math.cos(turn) * ring, y: Math.sin(turn) * ring };
106
+ const way = around(at, groups.size, solid);
107
+ const middle = {
108
+ x: way.x * ring,
109
+ y: way.y * ring,
110
+ ...(solid ? { z: (way.z ?? 0) * ring } : {}),
111
+ };
77
112
  const ordered = [...hubs].sort(([, one], [, other]) => other.length - one.length);
78
113
  const mean = ordered.reduce((sum, [, members]) => sum + members.length + 1, 0) / ordered.length;
79
114
  ordered.forEach(([hub, members], place) => {
80
- const spot = spiral(place, Math.sqrt(mean) * 2);
81
- const center = { x: middle.x + spot.x, y: middle.y + spot.y };
115
+ const center = plus(middle, spiral(place, (solid ? Math.cbrt(mean) : Math.sqrt(mean)) * 2, solid));
82
116
  positions.set(hub, center);
83
117
  members
84
118
  .sort((one, other) => rank(other) - rank(one))
85
119
  .forEach((member, step) => {
86
- const off = spiral(step + 1, 1);
87
- positions.set(member, { x: center.x + off.x, y: center.y + off.y });
120
+ positions.set(member, plus(center, spiral(step + 1, 1, solid)));
88
121
  });
89
122
  });
90
123
  });
@@ -2,7 +2,7 @@ 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
4
  import { type GraphPoint } from './seed.js';
5
- import { type GraphStencil } from './stencil.js';
5
+ import { type GraphStencil, type StencilFrame } from './stencil.js';
6
6
  export type { DrawnLink, DrawnNode, GraphChanges } from './diff.js';
7
7
  /**
8
8
  * What a renderer has to answer for the frame to drive it, and nothing that is
@@ -81,6 +81,15 @@ export interface GraphDriver {
81
81
  * outside the group is selected.
82
82
  */
83
83
  groups?(groups: readonly GraphGroup[]): void;
84
+ /**
85
+ * Where in a shape the notes are drawn, where a `stencil` gives one:
86
+ * `surface`, a shell, whose far side a renderer that draws in three
87
+ * dimensions sets back so its near side reads; `fill`, all through it; or
88
+ * undefined, no shape. `frame` is the shape and where it was put among the
89
+ * notes, once the connector has drawn the notes in it, for a renderer that
90
+ * draws the shape as well.
91
+ */
92
+ shape?(place: 'fill' | 'surface' | undefined, frame?: StencilFrame): void;
84
93
  /** Removes whatever the driver added to the renderer or the page, when the connection ends. */
85
94
  stop?(): void;
86
95
  }
@@ -3,7 +3,7 @@ import { arrangeByGroup } from './arrange.js';
3
3
  import { diffGraph, isEmpty, linkKey, } from './diff.js';
4
4
  import { graphColorsFrom, looksOf, toneColor, } from './looks.js';
5
5
  import { seedPositions } from './seed.js';
6
- import { fitToStencil } from './stencil.js';
6
+ import { fieldOf, fitInStencil } from './stencil.js';
7
7
  const viewportOf = (frame) => {
8
8
  const box = (frame.shadowRoot?.querySelector('[part="viewport"]') ?? frame).getBoundingClientRect();
9
9
  return { height: box.height, width: box.width };
@@ -12,6 +12,7 @@ const still = () => typeof matchMedia === 'function' && matchMedia('(prefers-red
12
12
  export const connectGraph = (frame, driver, options = {}) => {
13
13
  const graph = frame;
14
14
  let known = new Map();
15
+ let shapeFrame;
15
16
  let drawnNodes = new Map();
16
17
  let drawnLinks = new Map();
17
18
  let first = true;
@@ -94,9 +95,16 @@ export const connectGraph = (frame, driver, options = {}) => {
94
95
  }
95
96
  }
96
97
  let { positions, unplaced } = seedPositions(nodes, [...nextLinks.values()], known);
97
- const shaped = options.stencil && ids.size > 0 && unplaced.size === ids.size
98
- ? fitToStencil(arrangeByGroup(nodes, [...nextLinks.values()]), options.stencil)
98
+ const fit = options.stencil && ids.size > 0 && unplaced.size === ids.size
99
+ ? fitInStencil(arrangeByGroup(nodes, [...nextLinks.values()], {
100
+ solid: fieldOf(options.stencil).depth > 0,
101
+ }), options.stencil)
99
102
  : undefined;
103
+ const shaped = fit?.positions;
104
+ if (fit?.frame) {
105
+ shapeFrame = fit.frame;
106
+ }
107
+ driver.shape?.(options.stencil ? (options.stencil.place ?? 'fill') : undefined, shapeFrame);
100
108
  if (shaped) {
101
109
  positions = shaped;
102
110
  unplaced = new Set();
@@ -118,6 +126,7 @@ export const connectGraph = (frame, driver, options = {}) => {
118
126
  : 1 + Math.min(Math.sqrt(degree.get(node.id) ?? 0), 6),
119
127
  x: at.x,
120
128
  y: at.y,
129
+ ...(at.z === undefined ? {} : { z: at.z }),
121
130
  });
122
131
  }
123
132
  for (const id of waiting) {
@@ -153,7 +162,7 @@ export const connectGraph = (frame, driver, options = {}) => {
153
162
  for (const [id, at] of settled) {
154
163
  const node = drawnNodes.get(id);
155
164
  if (node) {
156
- drawnNodes.set(id, { ...node, x: at.x, y: at.y });
165
+ drawnNodes.set(id, { ...node, x: at.x, y: at.y, z: at.z });
157
166
  }
158
167
  }
159
168
  graph.laidOut?.(Object.fromEntries([...settled].map(([id, at]) => [id, { ...at }])));
@@ -6,6 +6,8 @@ export interface DrawnNode {
6
6
  size: number;
7
7
  x: number;
8
8
  y: number;
9
+ /** How near the viewer, for a renderer that draws in three dimensions. */
10
+ z?: number | undefined;
9
11
  }
10
12
  /** A link as the renderer draws it, between two notes the graph holds. */
11
13
  export interface DrawnLink {
@@ -20,7 +20,8 @@ export const diffGraph = (before, after) => {
20
20
  else if (was.label !== node.label ||
21
21
  was.size !== node.size ||
22
22
  was.x !== node.x ||
23
- was.y !== node.y) {
23
+ was.y !== node.y ||
24
+ was.z !== node.z) {
24
25
  changes.changeNodes.push(node);
25
26
  }
26
27
  }
@@ -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, { x: node.x, y: node.y });
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);
@@ -46,7 +50,11 @@ export const seedPositions = (nodes, links, known = new Map()) => {
46
50
  const beside = placed === undefined ? undefined : positions.get(placed);
47
51
  if (beside) {
48
52
  const offset = nudge(node.id);
49
- positions.set(node.id, { x: beside.x + offset.x, y: beside.y + offset.y });
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
+ });
50
58
  continue;
51
59
  }
52
60
  const radius = Math.sqrt(spiral + 0.5);
@@ -0,0 +1,61 @@
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
+ /** How many cells a field has along its longer side, flat and solid. */
21
+ export declare const FLAT = 512;
22
+ export declare const SOLID = 96;
23
+ /**
24
+ * A field from a grid of cells marked inside, `cell` box units a cell, x
25
+ * fastest and z slowest: the distance to the nearest outside cell, taken
26
+ * from the distance to the nearest inside one, so the sign says which side
27
+ * of the edge a point is on.
28
+ */
29
+ export declare const fieldFromCells: (inside: Uint8Array, sizes: readonly [number, number, number], cell: number, box: {
30
+ depth: number;
31
+ height: number;
32
+ width: number;
33
+ }) => ShapeField;
34
+ /**
35
+ * A field from a test of whether a point in the box is inside, sampled at
36
+ * the center of each cell: flat when `depth` is zero, solid otherwise.
37
+ */
38
+ export declare const fieldFromInside: (inside: (x: number, y: number, z: number) => boolean, box: {
39
+ depth?: number | undefined;
40
+ height: number;
41
+ width: number;
42
+ }, resolution?: number) => ShapeField;
43
+ /**
44
+ * A flat field from an SVG path in a box `width` by `height`, its holes cut by
45
+ * the even-odd rule. Drawn once to a canvas and read off it, which is what
46
+ * lets a detailed outline become a field in a moment.
47
+ */
48
+ export declare const fieldFromPath: (path: string, width: number, height: number, resolution?: number) => ShapeField;
49
+ /**
50
+ * A solid puffed out of a flat shape, as a cushion is: as thick as `depth`
51
+ * where the shape is widest, rounding to nothing at its edge, and cut
52
+ * through wherever the flat shape has a hole.
53
+ *
54
+ * `roundness`, from 0 to 1, is how far in from the edge the rounding runs, as
55
+ * a share of the way to the shape's middle: 1, the default, rounds the whole
56
+ * way, as a pebble; less leaves a flat face, as a coin.
57
+ */
58
+ export declare const inflate: (flat: ShapeField, { depth, roundness }: {
59
+ depth: number;
60
+ roundness?: number;
61
+ }) => ShapeField;
@@ -0,0 +1,172 @@
1
+ export const FLAT = 512;
2
+ export 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
+ export 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
+ };
@@ -1,11 +1,12 @@
1
1
  import type { GraphPoint } from './seed.js';
2
+ import { type ShapeField } from './shape-field.js';
2
3
  /**
3
- * A shape the notes are fitted into once a layout settles, in a box `width`
4
- * by `height`, as an SVG's `viewBox="0 0 width height"` gives one: an SVG
5
- * path, whose holes are cut by the even-odd rule, or a test of whether a
6
- * point in the box is inside.
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.
7
8
  */
8
- export type GraphStencil = {
9
+ export type GraphStencil = (ShapeField | {
9
10
  height: number;
10
11
  path: string;
11
12
  width: number;
@@ -13,22 +14,60 @@ export type GraphStencil = {
13
14
  height: number;
14
15
  inside: (x: number, y: number) => boolean;
15
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;
16
26
  };
27
+ /** The field a stencil is, or is made into. */
28
+ export declare const fieldOf: (stencil: GraphStencil) => ShapeField;
17
29
  /**
18
- * `count` places spread evenly over the inside of a shape: a grid sized to
19
- * the shape's area, each place shaken within its cell so no rows show, and
20
- * the grid made finer until it holds enough.
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.
21
34
  */
22
- export declare const placesIn: (inside: (x: number, y: number) => boolean, width: number, height: number, count: number) => GraphPoint[];
35
+ export declare const placesIn: (field: ShapeField, count: number, { place, shell }?: {
36
+ place?: 'fill' | 'surface';
37
+ shell?: number;
38
+ }) => GraphPoint[];
23
39
  /**
24
- * Places a layout put anywhere, moved into the stencil's shape and filling it
25
- * evenly. **The notes keep their order**, left to right and, within each
26
- * strip, top to bottom, so notes the layout put together land together, and
27
- * each group takes a region of the shape.
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.
28
44
  *
29
- * Centered where the layout was, and as large. The stencil's y points down,
30
- * as an SVG's does, and the layout's up, as Sigma and cosmos.gl draw it.
31
- * Places the shape cannot hold, as when it has no inside, come back as they
32
- * were.
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.
33
49
  */
34
50
  export declare const fitToStencil: (positions: ReadonlyMap<string, GraphPoint>, stencil: GraphStencil) => Map<string, GraphPoint>;
51
+ /**
52
+ * Where a stencil's box was put, in the places' units: its middle, and how
53
+ * many of the places' units one of the box's is. A point in the box at `x`,
54
+ * `y` and `z` is at `middle.x + (x - width / 2) * scale`,
55
+ * `middle.y - (y - height / 2) * scale` and `middle.z + (z - depth / 2) * scale`.
56
+ */
57
+ export interface StencilFrame {
58
+ field: ShapeField;
59
+ middle: {
60
+ x: number;
61
+ y: number;
62
+ z: number;
63
+ };
64
+ /** How many of the places' units one of the box's is. */
65
+ scale: number;
66
+ /** How thick the shell the notes were placed in is, in the box's units: none where they fill the shape. */
67
+ shell: number;
68
+ }
69
+ /** `fitToStencil`, saying where it put the stencil's box as well, where it fitted the places at all. */
70
+ export declare const fitInStencil: (positions: ReadonlyMap<string, GraphPoint>, stencil: GraphStencil) => {
71
+ frame?: StencilFrame | undefined;
72
+ positions: Map<string, GraphPoint>;
73
+ };