@fusion.dev/helix-graph 0.119.0 → 0.121.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.
@@ -1,6 +1,9 @@
1
- import { HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphZoomEvent, isRtl, watchTheme, } from '@fusion.dev/helix-core';
2
- import { graphColorsFrom, looksOf } from './looks.js';
1
+ import { HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, isRtl, watchTheme, } from '@fusion.dev/helix-core';
2
+ import { arrangeByGroup } from './arrange.js';
3
+ import { diffGraph, isEmpty, linkKey, } from './diff.js';
4
+ import { graphColorsFrom, looksOf, toneColor, } from './looks.js';
3
5
  import { seedPositions } from './seed.js';
6
+ import { fitToStencil } from './stencil.js';
4
7
  const viewportOf = (frame) => {
5
8
  const box = (frame.shadowRoot?.querySelector('[part="viewport"]') ?? frame).getBoundingClientRect();
6
9
  return { height: box.height, width: box.width };
@@ -9,7 +12,13 @@ const still = () => typeof matchMedia === 'function' && matchMedia('(prefers-red
9
12
  export const connectGraph = (frame, driver, options = {}) => {
10
13
  const graph = frame;
11
14
  let known = new Map();
15
+ let drawnNodes = new Map();
16
+ let drawnLinks = new Map();
17
+ let first = true;
18
+ let waiting = new Set();
19
+ let owed = false;
12
20
  let colors = null;
21
+ let current;
13
22
  let stopSettling;
14
23
  let pressed;
15
24
  let pending = 0;
@@ -33,58 +42,153 @@ export const connectGraph = (frame, driver, options = {}) => {
33
42
  }
34
43
  }
35
44
  }
36
- const groups = new Map(nodes.map((node) => [node.id, node.group]));
37
- driver.look(looksOf({
45
+ const notes = new Map(nodes.map((node) => [node.id, node]));
46
+ current = looksOf({
38
47
  colors,
39
48
  font: getComputedStyle(frame)
40
49
  .fontFamily.split(',')[0]
41
50
  ?.trim()
42
51
  .replace(/^["']|["']$/g, ''),
43
- groupOf: (id) => groups.get(id),
44
52
  groups: nodes.flatMap((node) => (node.group === undefined ? [] : [node.group])),
45
53
  near,
54
+ noteOf: (id) => notes.get(id),
46
55
  selected,
47
56
  shown: graph.shown,
48
- }));
57
+ });
58
+ driver.look(current);
59
+ if (driver.groups) {
60
+ const members = new Map();
61
+ for (const node of nodes) {
62
+ if (node.group !== undefined) {
63
+ const list = members.get(node.group);
64
+ if (list) {
65
+ list.push(node.id);
66
+ }
67
+ else {
68
+ members.set(node.group, [node.id]);
69
+ }
70
+ }
71
+ }
72
+ const own = selected === undefined ? undefined : notes.get(selected)?.group;
73
+ const looks = current;
74
+ driver.groups([...members].map(([name, ids]) => ({
75
+ color: looks.colorOf(ids.find((id) => notes.get(id)?.tone === undefined) ?? ids[0] ?? ''),
76
+ faded: selected !== undefined && own !== name,
77
+ ids,
78
+ name,
79
+ })));
80
+ }
49
81
  };
50
- const draw = () => {
51
- stopSettling?.();
52
- stopSettling = undefined;
82
+ const draw = (whole = false) => {
83
+ if (stopSettling) {
84
+ stopSettling();
85
+ stopSettling = undefined;
86
+ }
53
87
  cancelAnimationFrame(pending);
54
88
  const nodes = graph.nodes ?? [];
55
89
  const ids = new Set(nodes.map((node) => node.id));
56
- const links = (graph.links ?? []).filter(({ source, target }) => source !== target && ids.has(source) && ids.has(target));
57
- const { positions, unplaced } = seedPositions(nodes, links, known);
58
- graph.drawn?.(false);
59
- driver.draw({
60
- links: links.map(({ label, source, target }) => ({ label, source, target })),
61
- nodes: nodes.map((node) => {
62
- const at = positions.get(node.id);
63
- return {
64
- id: node.id,
65
- label: node.label || node.id,
66
- size: Number.isFinite(node.size) ? node.size : 1,
67
- x: at.x,
68
- y: at.y,
69
- };
70
- }),
71
- });
90
+ const nextLinks = new Map();
91
+ for (const { label, source, target } of graph.links ?? []) {
92
+ if (source !== target && ids.has(source) && ids.has(target)) {
93
+ nextLinks.set(linkKey({ source, target }), { label, source, target });
94
+ }
95
+ }
96
+ 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)
99
+ : undefined;
100
+ if (shaped) {
101
+ positions = shaped;
102
+ unplaced = new Set();
103
+ known = new Map(shaped);
104
+ }
105
+ const degree = new Map();
106
+ for (const { source, target } of nextLinks.values()) {
107
+ degree.set(source, (degree.get(source) ?? 0) + 1);
108
+ degree.set(target, (degree.get(target) ?? 0) + 1);
109
+ }
110
+ const nextNodes = new Map();
111
+ for (const node of nodes) {
112
+ const at = positions.get(node.id);
113
+ nextNodes.set(node.id, {
114
+ id: node.id,
115
+ label: node.label || node.id,
116
+ size: Number.isFinite(node.size)
117
+ ? node.size
118
+ : 1 + Math.min(Math.sqrt(degree.get(node.id) ?? 0), 6),
119
+ x: at.x,
120
+ y: at.y,
121
+ });
122
+ }
123
+ for (const id of waiting) {
124
+ if (ids.has(id)) {
125
+ unplaced.add(id);
126
+ }
127
+ }
128
+ const incremental = !first && !whole && driver.update !== undefined;
129
+ if (incremental) {
130
+ const changes = diffGraph({ links: drawnLinks, nodes: drawnNodes }, { links: nextLinks, nodes: nextNodes });
131
+ if (!isEmpty(changes)) {
132
+ driver.update?.(changes, !still());
133
+ }
134
+ }
135
+ else {
136
+ graph.drawn?.(false);
137
+ driver.draw({ links: [...nextLinks.values()], nodes: [...nextNodes.values()] });
138
+ }
139
+ drawnNodes = nextNodes;
140
+ drawnLinks = nextLinks;
141
+ known = positions;
72
142
  restyle();
143
+ if (first || whole) {
144
+ owed = true;
145
+ }
146
+ first = false;
73
147
  if (unplaced.size > 0 && driver.settle) {
148
+ waiting = unplaced;
74
149
  stopSettling = driver.settle(unplaced, (settled) => {
75
150
  stopSettling = undefined;
151
+ waiting = new Set();
76
152
  known = new Map(settled);
153
+ for (const [id, at] of settled) {
154
+ const node = drawnNodes.get(id);
155
+ if (node) {
156
+ drawnNodes.set(id, { ...node, x: at.x, y: at.y });
157
+ }
158
+ }
77
159
  graph.laidOut?.(Object.fromEntries([...settled].map(([id, at]) => [id, { ...at }])));
78
- graph.drawn?.(true);
160
+ if (owed) {
161
+ owed = false;
162
+ graph.drawn?.(true);
163
+ }
79
164
  });
80
165
  return;
81
166
  }
82
- known = positions;
83
- pending = requestAnimationFrame(() => {
84
- if (connected) {
85
- graph.drawn?.(true);
86
- }
87
- });
167
+ waiting = new Set();
168
+ if (shaped) {
169
+ graph.laidOut?.(Object.fromEntries([...shaped].map(([id, at]) => [id, { ...at }])));
170
+ }
171
+ if (owed) {
172
+ pending = requestAnimationFrame(() => {
173
+ if (connected && owed) {
174
+ owed = false;
175
+ graph.drawn?.(true);
176
+ }
177
+ });
178
+ }
179
+ };
180
+ const handleSignal = (event) => {
181
+ const { from, to, tone } = event.detail;
182
+ if (!driver.signal || !drawnNodes.has(from) || !drawnNodes.has(to)) {
183
+ return;
184
+ }
185
+ colors ??= graphColorsFrom(frame);
186
+ const toned = tone !== undefined && colors ? toneColor(colors, tone) : undefined;
187
+ driver.signal(from, to, toned
188
+ ? { from: toned, to: toned }
189
+ : current
190
+ ? { from: current.colorOf(from), to: current.colorOf(to) }
191
+ : undefined, !still());
88
192
  };
89
193
  const handlePartChange = (event) => {
90
194
  const { kind } = event.detail;
@@ -95,7 +199,21 @@ export const connectGraph = (frame, driver, options = {}) => {
95
199
  restyle();
96
200
  const selected = graph.active;
97
201
  if (selected && selected !== pressed && options.center !== false) {
98
- driver.center?.(selected, !still());
202
+ const near = [selected];
203
+ for (const { source, target } of drawnLinks.values()) {
204
+ if (source === selected) {
205
+ near.push(target);
206
+ }
207
+ else if (target === selected) {
208
+ near.push(source);
209
+ }
210
+ }
211
+ if (driver.fit && near.length > 1) {
212
+ driver.fit(near, !still());
213
+ }
214
+ else {
215
+ driver.center?.(selected, !still());
216
+ }
99
217
  }
100
218
  pressed = undefined;
101
219
  }
@@ -117,6 +235,7 @@ export const connectGraph = (frame, driver, options = {}) => {
117
235
  }
118
236
  };
119
237
  frame.addEventListener(HelixGraphPartChangeEvent.eventName, handlePartChange);
238
+ frame.addEventListener(HelixGraphSignalEvent.eventName, handleSignal);
120
239
  frame.addEventListener(HelixGraphPanEvent.eventName, handlePan);
121
240
  frame.addEventListener(HelixGraphZoomEvent.eventName, handleZoom);
122
241
  const stopPress = driver.onPress?.((id) => {
@@ -147,6 +266,7 @@ export const connectGraph = (frame, driver, options = {}) => {
147
266
  stopPress?.();
148
267
  stopTheme();
149
268
  frame.removeEventListener(HelixGraphPartChangeEvent.eventName, handlePartChange);
269
+ frame.removeEventListener(HelixGraphSignalEvent.eventName, handleSignal);
150
270
  frame.removeEventListener(HelixGraphPanEvent.eventName, handlePan);
151
271
  frame.removeEventListener(HelixGraphZoomEvent.eventName, handleZoom);
152
272
  if (focusable) {
@@ -163,7 +283,8 @@ export const connectGraph = (frame, driver, options = {}) => {
163
283
  },
164
284
  relayout: () => {
165
285
  known = new Map();
166
- draw();
286
+ waiting = new Set();
287
+ draw(true);
167
288
  },
168
289
  restyle: () => {
169
290
  colors = null;
@@ -0,0 +1,50 @@
1
+ /** A note as the renderer draws it, placed. */
2
+ export interface DrawnNode {
3
+ id: string;
4
+ label: string;
5
+ /** How large it is drawn, relative to the others: the note's `size`, or from how many links it has. */
6
+ size: number;
7
+ x: number;
8
+ y: number;
9
+ }
10
+ /** A link as the renderer draws it, between two notes the graph holds. */
11
+ export interface DrawnLink {
12
+ label?: string | undefined;
13
+ source: string;
14
+ target: string;
15
+ }
16
+ /** What changed between two drawings of a graph, which a renderer applies in place. */
17
+ export interface GraphChanges {
18
+ /** Links to draw that were not drawn. */
19
+ addLinks: DrawnLink[];
20
+ /** Notes to draw that were not drawn, placed. */
21
+ addNodes: DrawnNode[];
22
+ /** Notes still drawn whose name, size or place changed. */
23
+ changeNodes: DrawnNode[];
24
+ /** Links no longer drawn. */
25
+ removeLinks: DrawnLink[];
26
+ /** Notes no longer drawn, by `id`. */
27
+ removeNodes: string[];
28
+ }
29
+ /** A link's key: its two ends, which no `id` can contain both of by accident. */
30
+ export declare const linkKey: (link: {
31
+ source: string;
32
+ target: string;
33
+ }) => string;
34
+ /** Whether a diff changed nothing. */
35
+ export declare const isEmpty: (changes: GraphChanges) => boolean;
36
+ /**
37
+ * What changed from one drawing to the next, matched by `id` and by a link's
38
+ * two ends.
39
+ *
40
+ * **Linear in the size of the graph and nothing more**, so a page can hand the
41
+ * frame a new array for every message on a busy feed and the renderer is told
42
+ * only what moved: ten thousand notes with one added is one note drawn.
43
+ */
44
+ export declare const diffGraph: (before: {
45
+ links: ReadonlyMap<string, DrawnLink>;
46
+ nodes: ReadonlyMap<string, DrawnNode>;
47
+ }, after: {
48
+ links: ReadonlyMap<string, DrawnLink>;
49
+ nodes: ReadonlyMap<string, DrawnNode>;
50
+ }) => GraphChanges;
@@ -0,0 +1,48 @@
1
+ export const linkKey = (link) => `${link.source}\u0000${link.target}`;
2
+ export const isEmpty = (changes) => changes.addLinks.length === 0 &&
3
+ changes.addNodes.length === 0 &&
4
+ changes.changeNodes.length === 0 &&
5
+ changes.removeLinks.length === 0 &&
6
+ changes.removeNodes.length === 0;
7
+ export const diffGraph = (before, after) => {
8
+ const changes = {
9
+ addLinks: [],
10
+ addNodes: [],
11
+ changeNodes: [],
12
+ removeLinks: [],
13
+ removeNodes: [],
14
+ };
15
+ for (const [id, node] of after.nodes) {
16
+ const was = before.nodes.get(id);
17
+ if (!was) {
18
+ changes.addNodes.push(node);
19
+ }
20
+ else if (was.label !== node.label ||
21
+ was.size !== node.size ||
22
+ was.x !== node.x ||
23
+ was.y !== node.y) {
24
+ changes.changeNodes.push(node);
25
+ }
26
+ }
27
+ for (const id of before.nodes.keys()) {
28
+ if (!after.nodes.has(id)) {
29
+ changes.removeNodes.push(id);
30
+ }
31
+ }
32
+ for (const [key, link] of after.links) {
33
+ const was = before.links.get(key);
34
+ if (!was) {
35
+ changes.addLinks.push(link);
36
+ }
37
+ else if (was.label !== link.label) {
38
+ changes.removeLinks.push(was);
39
+ changes.addLinks.push(link);
40
+ }
41
+ }
42
+ for (const [key, link] of before.links) {
43
+ if (!after.links.has(key)) {
44
+ changes.removeLinks.push(link);
45
+ }
46
+ }
47
+ return changes;
48
+ };
@@ -3,6 +3,10 @@
3
3
  * where the frame is.
4
4
  */
5
5
  export interface GraphColors {
6
+ /** A signal meaning something to look at: `accent`. */
7
+ accent: string;
8
+ /** A signal meaning something went wrong: `danger`. */
9
+ danger: string;
6
10
  /** What the drawing is laid on, which faded notes are mixed toward: `surface-sunken`. */
7
11
  ground: string;
8
12
  /** The links: `border`. */
@@ -17,6 +21,12 @@ export interface GraphColors {
17
21
  primary: string;
18
22
  /** One color for each group, in the order of the groups' names: `series-1` and on. */
19
23
  series: string[];
24
+ /** A signal meaning something done: `success`. */
25
+ success: string;
26
+ /** A signal meaning something to know: `info`. */
27
+ info: string;
28
+ /** A signal meaning something to watch: `warning`. */
29
+ warning: string;
20
30
  }
21
31
  /**
22
32
  * The page's colors where the frame is, or `null` while they cannot be read.
@@ -48,11 +58,31 @@ export interface NodeLook {
48
58
  * neighbors drew their names over each other in a knot nobody could read.
49
59
  */
50
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;
51
67
  /** Drawn over the rest, as the selected note and its neighbors are. */
52
68
  raised: boolean;
53
69
  /** The selected note. */
54
70
  selected: boolean;
71
+ /** Its shape, where the note says. */
72
+ shape?: 'circle' | 'diamond' | 'ring' | 'square' | undefined;
55
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;
56
86
  /** How one link is drawn. */
57
87
  export interface LinkLook {
58
88
  color: string;
@@ -61,12 +91,18 @@ export interface LinkLook {
61
91
  }
62
92
  /** How every note and link is drawn, asked one at a time, so ten thousand cost nothing until drawn. */
63
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;
64
96
  /** The typeface the labels are drawn in: the first of the frame's own. */
65
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;
66
100
  /** The color the labels are drawn in. */
67
101
  ink: string;
68
102
  /** What a name is drawn on while the pointer is over its note. */
69
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;
70
106
  link(source: string, target: string): LinkLook;
71
107
  node(id: string): NodeLook;
72
108
  }
@@ -74,8 +110,8 @@ export interface GraphLooks {
74
110
  * How the graph is drawn for one selection.
75
111
  *
76
112
  * **The neighborhood stands out and the rest steps back**, which is what
77
- * selecting a note in a graph of thousands is for: the selected note in the
78
- * brand color, the notes it links to and from in their groups' colors with
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
79
115
  * their names drawn where there is room, and every other note faded toward
80
116
  * the ground, unnamed. With
81
117
  * nothing selected, every note is in its group's color.
@@ -86,9 +122,9 @@ export interface GraphLooks {
86
122
  export declare const looksOf: (options: {
87
123
  colors: GraphColors;
88
124
  font?: string | undefined;
89
- groupOf: (id: string) => string | undefined;
90
125
  groups: readonly string[];
91
126
  near: ReadonlySet<string>;
127
+ noteOf: (id: string) => NoteFacts | undefined;
92
128
  selected: string | undefined;
93
129
  shown: ReadonlySet<string> | undefined;
94
130
  }) => GraphLooks;
@@ -1,12 +1,17 @@
1
1
  import { readColor } from '@fusion.dev/helix-core';
2
2
  const SERIES = 24;
3
3
  const TOKENS = {
4
+ accent: '--helix-color-accent',
5
+ danger: '--helix-color-danger',
4
6
  ground: '--helix-color-surface-sunken',
7
+ info: '--helix-color-info',
5
8
  ink: '--helix-color-foreground',
6
9
  line: '--helix-color-border',
7
10
  muted: '--helix-color-foreground-muted',
8
11
  overlay: '--helix-color-surface-overlay',
9
12
  primary: '--helix-color-primary',
13
+ success: '--helix-color-success',
14
+ warning: '--helix-color-warning',
10
15
  };
11
16
  export const graphColorsFrom = (element) => {
12
17
  if (typeof document === 'undefined') {
@@ -49,12 +54,21 @@ export const mixRgb = (from, to, amount) => {
49
54
  const channel = (at) => Math.round((one[at] ?? 0) + ((other[at] ?? 0) - (one[at] ?? 0)) * amount);
50
55
  return `rgb(${channel(0)}, ${channel(1)}, ${channel(2)})`;
51
56
  };
57
+ export const toneColor = (colors, tone) => tone === undefined || tone === 'primary'
58
+ ? colors.primary
59
+ : tone === 'neutral'
60
+ ? colors.muted
61
+ : colors[tone];
52
62
  const FADED = 0.75;
53
63
  export const looksOf = (options) => {
54
- const { colors, groupOf, near, selected, shown } = options;
64
+ const { colors, near, noteOf, selected, shown } = options;
55
65
  const order = new Map([...new Set(options.groups)].sort().map((group, at) => [group, at]));
56
66
  const own = (id) => {
57
- const group = groupOf(id);
67
+ const note = noteOf(id);
68
+ if (note?.tone) {
69
+ return toneColor(colors, note.tone);
70
+ }
71
+ const group = note?.group;
58
72
  const at = group === undefined ? undefined : order.get(group);
59
73
  return at === undefined || colors.series.length === 0
60
74
  ? colors.muted
@@ -63,24 +77,30 @@ export const looksOf = (options) => {
63
77
  const focused = selected !== undefined;
64
78
  const hidden = (id) => shown !== undefined && !shown.has(id);
65
79
  return {
80
+ colorOf: own,
66
81
  font: options.font || 'sans-serif',
82
+ ground: colors.ground,
67
83
  ink: colors.ink,
68
84
  link: (source, target) => {
69
85
  const touches = focused && (source === selected || target === selected);
70
86
  return {
71
87
  color: touches
72
- ? colors.muted
88
+ ? own(selected)
73
89
  : focused
74
- ? mixRgb(colors.line, colors.ground, 0.5)
75
- : colors.line,
90
+ ? mixRgb(colors.line, colors.ground, 0.6)
91
+ : mixRgb(own(source), colors.ground, 0.72),
76
92
  hidden: hidden(source) || hidden(target),
77
93
  raised: touches,
78
94
  };
79
95
  },
80
96
  node: (id) => {
97
+ const note = noteOf(id);
98
+ const facts = { emphasis: note?.emphasis, material: note?.material, shape: note?.shape };
81
99
  if (id === selected) {
82
100
  return {
83
- color: colors.primary,
101
+ ...facts,
102
+ color: own(id),
103
+ glow: 1,
84
104
  hidden: hidden(id),
85
105
  label: 'always',
86
106
  raised: true,
@@ -89,7 +109,9 @@ export const looksOf = (options) => {
89
109
  }
90
110
  const neighbor = focused && near.has(id);
91
111
  return {
112
+ ...facts,
92
113
  color: focused && !neighbor ? mixRgb(own(id), colors.ground, FADED) : own(id),
114
+ glow: 0,
93
115
  hidden: hidden(id),
94
116
  label: focused && !neighbor ? 'never' : 'room',
95
117
  raised: neighbor,
@@ -97,5 +119,6 @@ export const looksOf = (options) => {
97
119
  };
98
120
  },
99
121
  overlay: colors.overlay,
122
+ quiet: colors.muted,
100
123
  };
101
124
  };
@@ -23,9 +23,18 @@ export const seedPositions = (nodes, links, known = new Map()) => {
23
23
  }
24
24
  }
25
25
  const neighbors = new Map();
26
+ const add = (from, to) => {
27
+ const list = neighbors.get(from);
28
+ if (list) {
29
+ list.push(to);
30
+ }
31
+ else {
32
+ neighbors.set(from, [to]);
33
+ }
34
+ };
26
35
  for (const { source, target } of links) {
27
- neighbors.set(source, [...(neighbors.get(source) ?? []), target]);
28
- neighbors.set(target, [...(neighbors.get(target) ?? []), source]);
36
+ add(source, target);
37
+ add(target, source);
29
38
  }
30
39
  let spiral = positions.size;
31
40
  for (const node of nodes) {
@@ -0,0 +1,34 @@
1
+ import type { GraphPoint } from './seed.js';
2
+ /**
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.
7
+ */
8
+ export type GraphStencil = {
9
+ height: number;
10
+ path: string;
11
+ width: number;
12
+ } | {
13
+ height: number;
14
+ inside: (x: number, y: number) => boolean;
15
+ width: number;
16
+ };
17
+ /**
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.
21
+ */
22
+ export declare const placesIn: (inside: (x: number, y: number) => boolean, width: number, height: number, count: number) => GraphPoint[];
23
+ /**
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.
28
+ *
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.
33
+ */
34
+ export declare const fitToStencil: (positions: ReadonlyMap<string, GraphPoint>, stencil: GraphStencil) => Map<string, GraphPoint>;