@fusion.dev/helix-graph 0.126.0 → 0.127.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 (39) hide show
  1. package/README.md +70 -38
  2. package/dist/connect-graph/arrange.d.ts +4 -2
  3. package/dist/connect-graph/arrange.js +5 -22
  4. package/dist/connect-graph/connect-graph.d.ts +18 -6
  5. package/dist/connect-graph/connect-graph.js +76 -32
  6. package/dist/connect-graph/diff.d.ts +7 -0
  7. package/dist/connect-graph/looks.d.ts +8 -0
  8. package/dist/connect-graph/looks.js +6 -3
  9. package/dist/connect-graph/seed.d.ts +9 -0
  10. package/dist/connect-graph/seed.js +21 -15
  11. package/dist/connect-graph/stencil.d.ts +3 -1
  12. package/dist/connect-graph/stencil.js +32 -10
  13. package/dist/cosmos/cosmos-driver.d.ts +5 -4
  14. package/dist/cosmos/cosmos-driver.js +122 -39
  15. package/dist/index.d.ts +4 -3
  16. package/dist/index.js +2 -2
  17. package/dist/model/index.d.ts +1 -0
  18. package/dist/model/index.js +1 -0
  19. package/dist/{shapes → model}/model.d.ts +4 -3
  20. package/dist/{shapes → model}/model.js +10 -5
  21. package/dist/overlay/overlay.d.ts +14 -8
  22. package/dist/overlay/overlay.js +64 -62
  23. package/dist/shapes/flat.d.ts +1 -1
  24. package/dist/shapes/flat.js +9 -5
  25. package/dist/shapes/index.d.ts +3 -4
  26. package/dist/shapes/index.js +3 -4
  27. package/dist/{connect-graph → shapes}/shape-field.d.ts +7 -3
  28. package/dist/{connect-graph → shapes}/shape-field.js +4 -2
  29. package/dist/shapes/solids.d.ts +3 -3
  30. package/dist/shapes/solids.js +25 -9
  31. package/dist/{three → shapes}/surface.d.ts +1 -1
  32. package/dist/shapes/triangles.d.ts +2 -2
  33. package/dist/shapes/triangles.js +49 -30
  34. package/dist/sigma/sigma-driver.d.ts +33 -17
  35. package/dist/sigma/sigma-driver.js +69 -17
  36. package/dist/three/three-driver.d.ts +12 -3
  37. package/dist/three/three-driver.js +376 -201
  38. package/package.json +9 -5
  39. /package/dist/{three → shapes}/surface.js +0 -0
@@ -1,55 +1,28 @@
1
- import { mixRgb } from '../connect-graph/looks.js';
1
+ import { mixRgb, rgbOf } from '../connect-graph/looks.js';
2
2
  const SPEED = 340;
3
3
  const QUICKEST = 450;
4
4
  const SLOWEST = 1400;
5
5
  const ARRIVAL = 520;
6
6
  const FLYING = 400;
7
7
  const withAlpha = (color, alpha) => {
8
- const [red = 128, green = 128, blue = 128] = color.match(/[\d.]+/g)?.map(Number) ?? [];
8
+ const [red, green, blue] = rgbOf(color);
9
9
  return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
10
10
  };
11
11
  const TAIL = 120;
12
12
  const PIECES = 14;
13
- const BOW = 0.16;
14
- const BOW_MOST = 90;
15
13
  const SPARK = 0.22;
16
- const on = (a, c, b, t) => {
17
- const u = 1 - t;
18
- return {
19
- x: u * u * a.x + 2 * u * t * c.x + t * t * b.x,
20
- y: u * u * a.y + 2 * u * t * c.y + t * t * b.y,
21
- };
22
- };
23
- const bowed = (from, to) => {
24
- const dx = to.x - from.x;
25
- const dy = to.y - from.y;
26
- const length = Math.hypot(dx, dy) || 1;
27
- const bow = Math.min(length * BOW, BOW_MOST) * 2;
28
- return {
29
- x: (from.x + to.x) / 2 - (dy / length) * bow,
30
- y: (from.y + to.y) / 2 + (dx / length) * bow,
31
- };
32
- };
33
- const bounded = (from, to, toward) => {
34
- const middle = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
35
- const far = Math.hypot(toward.x - middle.x, toward.y - middle.y);
36
- const most = Math.max(Math.hypot(to.x - from.x, to.y - from.y) * 0.9, 12);
37
- if (far <= most) {
38
- return toward;
39
- }
40
- return {
41
- x: middle.x + ((toward.x - middle.x) * most) / far,
42
- y: middle.y + ((toward.y - middle.y) * most) / far,
43
- };
44
- };
45
- const trace = (context, a, c, b) => {
14
+ const on = (a, b, t) => ({
15
+ x: a.x + (b.x - a.x) * t,
16
+ y: a.y + (b.y - a.y) * t,
17
+ });
18
+ const trace = (context, a, b) => {
46
19
  context.beginPath();
47
20
  context.moveTo(a.x, a.y);
48
- context.quadraticCurveTo(c.x, c.y, b.x, b.y);
21
+ context.lineTo(b.x, b.y);
49
22
  };
50
- const comet = (context, leaves, arrives, from, bend, to, along, through, glowing) => {
23
+ const comet = (context, leaves, arrives, from, to, along, through, glowing) => {
51
24
  const strength = glowing ? 1.25 : 1;
52
- const length = Math.hypot(bend.x - from.x, bend.y - from.y) + Math.hypot(to.x - bend.x, to.y - bend.y) || 1;
25
+ const length = Math.hypot(to.x - from.x, to.y - from.y) || 1;
53
26
  const color = mixRgb(leaves, arrives, along);
54
27
  context.lineCap = 'round';
55
28
  const line = context.createLinearGradient(from.x, from.y, to.x, to.y);
@@ -57,15 +30,15 @@ const comet = (context, leaves, arrives, from, bend, to, along, through, glowing
57
30
  line.addColorStop(1, withAlpha(arrives, 0.16 * Math.sin(Math.PI * through)));
58
31
  context.strokeStyle = line;
59
32
  context.lineWidth = 1;
60
- trace(context, from, bend, to);
33
+ trace(context, from, to);
61
34
  context.stroke();
62
35
  const reach = Math.min(TAIL / length, along);
63
36
  context.lineCap = 'butt';
64
- let behind = on(from, bend, to, along - reach);
37
+ let behind = on(from, to, along - reach);
65
38
  for (let piece = 1; piece <= PIECES; piece++) {
66
39
  const share = piece / PIECES;
67
40
  const t = along - reach * (1 - share);
68
- const ahead = on(from, bend, to, t);
41
+ const ahead = on(from, to, t);
69
42
  context.strokeStyle = withAlpha(mixRgb(leaves, arrives, t), (glowing ? 0.85 : 1) * share * share);
70
43
  context.lineWidth = (0.4 + 2.2 * share) * strength;
71
44
  context.beginPath();
@@ -74,7 +47,7 @@ const comet = (context, leaves, arrives, from, bend, to, along, through, glowing
74
47
  context.stroke();
75
48
  behind = ahead;
76
49
  }
77
- const head = on(from, bend, to, along);
50
+ const head = on(from, to, along);
78
51
  const bloom = 9 * strength;
79
52
  const light = context.createRadialGradient(head.x, head.y, 0, head.x, head.y, bloom);
80
53
  light.addColorStop(0, glowing ? 'rgba(255, 255, 255, 0.95)' : withAlpha(color, 1));
@@ -109,24 +82,28 @@ const arrive = (context, color, at, gone) => {
109
82
  context.stroke();
110
83
  };
111
84
  export const isLight = (color) => {
112
- const [red = 0, green = 0, blue = 0] = color.match(/[\d.]+/g)?.map(Number) ?? [];
85
+ const [red, green, blue] = rgbOf(color, 0);
113
86
  return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255 > 0.5;
114
87
  };
115
88
  export const ease = (at) => (at < 0.5 ? 2 * at * at : 1 - (-2 * at + 2) ** 2 / 2);
116
- const nameGroups = (context, groups, looks, at) => {
89
+ const SAMPLED = 96;
90
+ const nameGroups = (context, groups, looks, at, avoid) => {
117
91
  context.font = `650 11px ${looks.font}`;
118
92
  context.letterSpacing = '0.14em';
119
93
  context.textAlign = 'center';
120
94
  context.textBaseline = 'middle';
121
95
  context.shadowColor = looks.ground;
122
- const taken = [];
96
+ const taken = [...avoid];
123
97
  for (const group of groups) {
124
98
  let x = 0;
125
99
  let y = 0;
126
100
  let count = 0;
127
101
  const points = [];
128
- for (const id of group.ids) {
129
- const point = at(id);
102
+ const step = Math.max(1, Math.floor(group.ids.length / SAMPLED));
103
+ let asked = 0;
104
+ for (let index = 0; index < group.ids.length; index += step) {
105
+ asked += 1;
106
+ const point = at(group.ids[index]);
130
107
  if (point) {
131
108
  x += point.x;
132
109
  y += point.y;
@@ -134,7 +111,7 @@ const nameGroups = (context, groups, looks, at) => {
134
111
  points.push(point);
135
112
  }
136
113
  }
137
- if (count < Math.max(2, group.ids.length * 0.3)) {
114
+ if (count < Math.max(2, asked * 0.3)) {
138
115
  continue;
139
116
  }
140
117
  x /= count;
@@ -186,12 +163,13 @@ const layer = (container, kind) => {
186
163
  }
187
164
  return canvas;
188
165
  };
166
+ export const overlayDensity = () => Math.min(Math.max(window.devicePixelRatio || 1, 2), 3);
189
167
  const fresh = (canvas, container) => {
190
168
  const context = canvas.getContext('2d');
191
169
  if (!context) {
192
170
  return null;
193
171
  }
194
- const ratio = window.devicePixelRatio || 1;
172
+ const ratio = overlayDensity();
195
173
  const width = Math.round(container.clientWidth * ratio);
196
174
  const height = Math.round(container.clientHeight * ratio);
197
175
  if (canvas.width !== width || canvas.height !== height) {
@@ -202,14 +180,14 @@ const fresh = (canvas, container) => {
202
180
  context.clearRect(0, 0, width, height);
203
181
  return context;
204
182
  };
205
- export const signalsOver = (container, onScreen, bend) => {
183
+ export const signalsOver = (container, onScreen) => {
206
184
  let overlay;
207
185
  let flights = [];
208
186
  let flying = 0;
209
187
  let ink = 'rgb(128, 128, 128)';
210
188
  let glowing = false;
211
189
  const fly = (now) => {
212
- const context = overlay ? fresh(overlay, container) : null;
190
+ const context = overlay && container.isConnected ? fresh(overlay, container) : null;
213
191
  if (!context) {
214
192
  flights = [];
215
193
  return;
@@ -221,17 +199,16 @@ export const signalsOver = (container, onScreen, bend) => {
221
199
  if (!from || !to) {
222
200
  return false;
223
201
  }
224
- flight.crossing ??= Math.min(Math.max(((Math.hypot(to.x - from.x, to.y - from.y) * 1.08) / SPEED) * 1000, QUICKEST), SLOWEST);
202
+ flight.crossing ??= Math.min(Math.max((Math.hypot(to.x - from.x, to.y - from.y) / SPEED) * 1000, QUICKEST), SLOWEST);
225
203
  const crossing = flight.crossing;
226
204
  const gone = (now - flight.started) / crossing;
227
205
  if (gone >= 1 + ARRIVAL / crossing) {
228
206
  return false;
229
207
  }
230
- const toward = bounded(from, to, bend?.(flight.from, flight.to) ?? bowed(from, to));
231
208
  context.save();
232
209
  if (gone < 1) {
233
210
  if (flight.animate) {
234
- comet(context, flight.leaves, flight.arrives, from, toward, to, ease(gone), gone, glowing);
211
+ comet(context, flight.leaves, flight.arrives, from, to, ease(gone), gone, glowing);
235
212
  if (gone < SPARK) {
236
213
  spark(context, flight.leaves, from, gone / SPARK);
237
214
  }
@@ -243,7 +220,7 @@ export const signalsOver = (container, onScreen, bend) => {
243
220
  context.globalAlpha = 1 - gone;
244
221
  context.strokeStyle = line;
245
222
  context.lineWidth = 2;
246
- trace(context, from, toward, to);
223
+ trace(context, from, to);
247
224
  context.stroke();
248
225
  }
249
226
  }
@@ -261,6 +238,9 @@ export const signalsOver = (container, onScreen, bend) => {
261
238
  glowing = isLight(looks.ink);
262
239
  },
263
240
  signal: (from, to, colors, animate) => {
241
+ if (!container.isConnected) {
242
+ return;
243
+ }
264
244
  overlay ??= layer(container, 'signals');
265
245
  flights.push({
266
246
  animate,
@@ -288,13 +268,15 @@ export const signalsOver = (container, onScreen, bend) => {
288
268
  };
289
269
  export const namesOver = (container, onScreen) => {
290
270
  const canvas = layer(container, 'names');
271
+ let taken = [];
291
272
  return {
273
+ boxes: () => taken,
292
274
  draw: (looks, named) => {
293
275
  const context = looks ? fresh(canvas, container) : null;
294
276
  if (!context || !looks) {
295
277
  return;
296
278
  }
297
- const taken = [];
279
+ taken = [];
298
280
  for (const { id, name, strong } of named) {
299
281
  const at = onScreen(id);
300
282
  if (!at || looks.node(id).hidden) {
@@ -330,14 +312,14 @@ export const namesOver = (container, onScreen) => {
330
312
  },
331
313
  };
332
314
  };
333
- export const groupsOver = (container, onScreen) => {
315
+ export const groupsOver = (container, onScreen, avoid = () => []) => {
334
316
  let canvas;
335
317
  let groups = [];
336
318
  let looks;
337
319
  const draw = () => {
338
320
  const context = canvas && looks ? fresh(canvas, container) : null;
339
321
  if (context && looks) {
340
- nameGroups(context, groups, looks, onScreen);
322
+ nameGroups(context, groups, looks, onScreen, avoid());
341
323
  }
342
324
  };
343
325
  return {
@@ -357,14 +339,29 @@ export const groupsOver = (container, onScreen) => {
357
339
  },
358
340
  };
359
341
  };
342
+ const GESTURES = 'data-helix-graph-gestures';
343
+ const touchRule = (container) => {
344
+ const root = container.getRootNode();
345
+ const holder = root instanceof ShadowRoot ? root : document.head;
346
+ if (!holder || holder.querySelector(`style[${GESTURES}]`)) {
347
+ return;
348
+ }
349
+ const style = document.createElement('style');
350
+ style.setAttribute(GESTURES, '');
351
+ style.textContent = `[${GESTURES}="cooperative"], [${GESTURES}="cooperative"] * { touch-action: pan-x pan-y !important; }
352
+ [${GESTURES}="off"], [${GESTURES}="off"] * { touch-action: auto !important; }`;
353
+ holder.append(style);
354
+ };
360
355
  export const gesturesHeld = (container) => {
361
356
  let mode = 'free';
362
- const touchAction = container.style.touchAction;
363
357
  const hold = (event) => {
364
358
  if (mode === 'free') {
365
359
  return;
366
360
  }
367
361
  if (mode === 'off') {
362
+ if (event.type === 'click' && event.detail < 2) {
363
+ return;
364
+ }
368
365
  event.stopImmediatePropagation();
369
366
  return;
370
367
  }
@@ -383,21 +380,26 @@ export const gesturesHeld = (container) => {
383
380
  }
384
381
  }
385
382
  };
386
- const held = ['wheel', 'touchstart', 'touchmove', 'mousedown'];
383
+ const held = ['wheel', 'touchstart', 'touchmove', 'mousedown', 'click', 'dblclick'];
387
384
  for (const type of held) {
388
385
  container.addEventListener(type, hold, { capture: true, passive: false });
389
386
  }
390
387
  return {
391
388
  set: (next) => {
392
389
  mode = next;
393
- container.style.touchAction =
394
- next === 'free' ? touchAction : next === 'cooperative' ? 'pan-x pan-y' : 'auto';
390
+ if (next === 'free') {
391
+ container.removeAttribute(GESTURES);
392
+ }
393
+ else {
394
+ touchRule(container);
395
+ container.setAttribute(GESTURES, next);
396
+ }
395
397
  },
396
398
  stop: () => {
397
399
  for (const type of held) {
398
400
  container.removeEventListener(type, hold, { capture: true });
399
401
  }
400
- container.style.touchAction = touchAction;
402
+ container.removeAttribute(GESTURES);
401
403
  },
402
404
  };
403
405
  };
@@ -1,4 +1,4 @@
1
- import { type ShapeField } from '../connect-graph/shape-field.js';
1
+ import { type ShapeField } from './shape-field.js';
2
2
  /**
3
3
  * A flat field from an SVG: an `<svg>` element, or its markup. Its box is
4
4
  * its `viewBox`, and its shape every path, rectangle, circle, ellipse and
@@ -1,4 +1,4 @@
1
- import { FLAT, fieldFromCells, fieldFromPath, } from '../connect-graph/shape-field.js';
1
+ import { FLAT, fieldFromCells, fieldFromPath } from './shape-field.js';
2
2
  const pathOf = (element) => {
3
3
  const number = (name) => Number(element.getAttribute(name) ?? 0);
4
4
  switch (element.localName) {
@@ -27,24 +27,27 @@ export const shapeFromSvg = (svg, resolution = FLAT) => {
27
27
  const element = typeof svg === 'string'
28
28
  ? new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement
29
29
  : svg;
30
- const [, , width = 0, height = 0] = (element.getAttribute('viewBox') ?? '')
30
+ const [left = 0, top = 0, width = 0, height = 0] = (element.getAttribute('viewBox') ?? '')
31
31
  .trim()
32
32
  .split(/[\s,]+/)
33
33
  .map(Number);
34
+ const origin = { x: Number.isFinite(left) ? left : 0, y: Number.isFinite(top) ? top : 0 };
34
35
  const box = {
35
36
  height: height || Number(element.getAttribute('height') ?? 0),
36
37
  width: width || Number(element.getAttribute('width') ?? 0),
37
38
  };
38
39
  const shapes = [...element.querySelectorAll('path, rect, circle, ellipse, polygon')];
39
40
  const path = shapes.map(pathOf).join(' ');
40
- const field = fieldFromPath(path, box.width || 1, box.height || 1, resolution);
41
+ const field = fieldFromPath(path, box.width || 1, box.height || 1, resolution, origin);
41
42
  const named = shapes.flatMap((shape) => {
42
43
  const name = shape.closest('[id]')?.getAttribute('id');
43
44
  return name && shape.closest('[id]') !== element ? [{ name, path: pathOf(shape) }] : [];
44
45
  });
45
- return named.length > 0 ? { ...field, regionOf: partsOf(named, field, resolution) } : field;
46
+ return named.length > 0
47
+ ? { ...field, regionOf: partsOf(named, field, resolution, origin) }
48
+ : field;
46
49
  };
47
- const partsOf = (named, box, resolution) => {
50
+ const partsOf = (named, box, resolution, origin) => {
48
51
  const cell = Math.max(box.width, box.height, 1) / resolution;
49
52
  const nx = Math.max(1, Math.ceil(box.width / cell));
50
53
  const ny = Math.max(1, Math.ceil(box.height / cell));
@@ -55,6 +58,7 @@ const partsOf = (named, box, resolution) => {
55
58
  const labels = new Uint16Array(nx * ny);
56
59
  if (context) {
57
60
  context.scale(1 / cell, 1 / cell);
61
+ context.translate(-origin.x, -origin.y);
58
62
  named.forEach(({ path }, at) => {
59
63
  const label = at + 1;
60
64
  context.fillStyle = `rgb(${label & 255}, ${(label >> 8) & 255}, 0)`;
@@ -1,6 +1,5 @@
1
- export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, type ShapeField, } from '../connect-graph/shape-field.js';
2
- export { surfaceOf } from '../three/surface.js';
3
1
  export { type ImageOptions, shapeFromImage, shapeFromSvg } from './flat.js';
4
- export { shapeFromModel } from './model.js';
5
- export { box, onSphere, sphere, torus } from './solids.js';
2
+ export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, type ShapeField, } from './shape-field.js';
3
+ export { cuboid, onSphere, sphere, torus } from './solids.js';
4
+ export { surfaceOf } from './surface.js';
6
5
  export { shapeFromTriangles, type TriangleOptions } from './triangles.js';
@@ -1,6 +1,5 @@
1
- export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, } from '../connect-graph/shape-field.js';
2
- export { surfaceOf } from '../three/surface.js';
3
1
  export { shapeFromImage, shapeFromSvg } from './flat.js';
4
- export { shapeFromModel } from './model.js';
5
- export { box, onSphere, sphere, torus } from './solids.js';
2
+ export { fieldFromInside as shapeFromInside, fieldFromPath as shapeFromPath, inflate, } from './shape-field.js';
3
+ export { cuboid, onSphere, sphere, torus } from './solids.js';
4
+ export { surfaceOf } from './surface.js';
6
5
  export { shapeFromTriangles } from './triangles.js';
@@ -34,7 +34,7 @@ export interface ShapeField {
34
34
  }
35
35
  /** How many cells a field has along its longer side, flat and solid. */
36
36
  export declare const FLAT = 512;
37
- export declare const SOLID = 96;
37
+ export declare const SOLID = 144;
38
38
  /**
39
39
  * A field from a grid of cells marked inside, `cell` box units a cell, x
40
40
  * fastest and z slowest: the distance to the nearest outside cell, taken
@@ -58,9 +58,13 @@ export declare const fieldFromInside: (inside: (x: number, y: number, z: number)
58
58
  /**
59
59
  * A flat field from an SVG path in a box `width` by `height`, its holes cut by
60
60
  * the even-odd rule. Drawn once to a canvas and read off it, which is what
61
- * lets a detailed outline become a field in a moment.
61
+ * lets a detailed outline become a field in a moment. A box that starts
62
+ * somewhere other than 0, as an SVG `viewBox` can, is given as `origin`.
62
63
  */
63
- export declare const fieldFromPath: (path: string, width: number, height: number, resolution?: number) => ShapeField;
64
+ export declare const fieldFromPath: (path: string, width: number, height: number, resolution?: number, origin?: {
65
+ x: number;
66
+ y: number;
67
+ }) => ShapeField;
64
68
  /**
65
69
  * A solid puffed out of a flat shape, as a cushion is: as thick as `depth`
66
70
  * where the shape is widest, rounding to nothing at its edge, and cut
@@ -1,5 +1,5 @@
1
1
  export const FLAT = 512;
2
- export const SOLID = 96;
2
+ export const SOLID = 144;
3
3
  const FAR = 1e20;
4
4
  const line = (values, count, out) => {
5
5
  const hull = new Int32Array(count);
@@ -126,7 +126,7 @@ export const fieldFromInside = (inside, box, resolution = box.depth ? SOLID : FL
126
126
  width: box.width,
127
127
  });
128
128
  };
129
- export const fieldFromPath = (path, width, height, resolution = FLAT) => {
129
+ export const fieldFromPath = (path, width, height, resolution = FLAT, origin = { x: 0, y: 0 }) => {
130
130
  const cell = Math.max(width, height, 1) / resolution;
131
131
  const nx = Math.max(1, Math.ceil(width / cell));
132
132
  const ny = Math.max(1, Math.ceil(height / cell));
@@ -137,6 +137,7 @@ export const fieldFromPath = (path, width, height, resolution = FLAT) => {
137
137
  const cells = new Uint8Array(nx * ny);
138
138
  if (context) {
139
139
  context.scale(1 / cell, 1 / cell);
140
+ context.translate(-origin.x, -origin.y);
140
141
  context.fill(new Path2D(path), 'evenodd');
141
142
  const alpha = context.getImageData(0, 0, nx, ny).data;
142
143
  for (let at = 0; at < cells.length; at++) {
@@ -168,5 +169,6 @@ export const inflate = (flat, { depth, roundness = 1 }) => {
168
169
  },
169
170
  height: flat.height,
170
171
  width: flat.width,
172
+ ...(flat.regionOf ? { regionOf: (x, y) => flat.regionOf?.(x, y) } : {}),
171
173
  };
172
174
  };
@@ -1,4 +1,4 @@
1
- import type { ShapeField } from '../connect-graph/shape-field.js';
1
+ import type { ShapeField } from './shape-field.js';
2
2
  /** A ball `radius` across from its middle. */
3
3
  export declare const sphere: ({ radius }?: {
4
4
  radius?: number;
@@ -8,8 +8,8 @@ export declare const torus: ({ radius, tube, }?: {
8
8
  radius?: number;
9
9
  tube?: number;
10
10
  }) => ShapeField;
11
- /** A box `width` by `height` by `depth`, its edges rounded `round` in. */
12
- export declare const box: ({ depth, height, round, width, }?: {
11
+ /** A cuboid `width` by `height` by `depth`, its edges rounded `round` in. */
12
+ export declare const cuboid: ({ depth, height, round, width, }?: {
13
13
  depth?: number;
14
14
  height?: number;
15
15
  round?: number;
@@ -13,7 +13,7 @@ export const torus = ({ radius = 50, tube = 18, } = {}) => {
13
13
  width: reach * 2,
14
14
  };
15
15
  };
16
- export const box = ({ depth = 100, height = 100, round = 0, width = 100, } = {}) => ({
16
+ export const cuboid = ({ depth = 100, height = 100, round = 0, width = 100, } = {}) => ({
17
17
  depth,
18
18
  distance: (x, y, z = depth / 2) => {
19
19
  const qx = Math.abs(x - width / 2) - width / 2 + round;
@@ -28,20 +28,36 @@ export const box = ({ depth = 100, height = 100, round = 0, width = 100, } = {})
28
28
  });
29
29
  export const onSphere = (flat, { radius = 50, thickness = radius * 0.08 } = {}) => {
30
30
  const along = (Math.PI * 2 * radius) / Math.max(flat.width, 1e-9);
31
+ const onMap = (x, y, z) => {
32
+ const dx = x - radius;
33
+ const dy = y - radius;
34
+ const dz = z - radius;
35
+ const out = Math.hypot(dx, dy, dz);
36
+ const longitude = Math.atan2(dx, dz);
37
+ const latitude = Math.acos(Math.min(Math.max(dy / (out || 1), -1), 1));
38
+ return {
39
+ out,
40
+ u: ((longitude + Math.PI) / (Math.PI * 2)) * flat.width,
41
+ v: (latitude / Math.PI) * flat.height,
42
+ };
43
+ };
44
+ const regionOf = flat.regionOf;
31
45
  return {
32
46
  depth: radius * 2,
33
47
  distance: (x, y, z = radius) => {
34
- const dx = x - radius;
35
- const dy = y - radius;
36
- const dz = z - radius;
37
- const out = Math.hypot(dx, dy, dz);
48
+ const { out, u, v } = onMap(x, y, z);
38
49
  const shell = Math.abs(out - (radius - thickness / 2)) - thickness / 2;
39
- const longitude = Math.atan2(dx, dz);
40
- const latitude = Math.acos(Math.min(Math.max(dy / (out || 1), -1), 1));
41
- const map = flat.distance(((longitude + Math.PI) / (Math.PI * 2)) * flat.width, (latitude / Math.PI) * flat.height);
42
- return Math.max(shell, map * along);
50
+ return Math.max(shell, flat.distance(u, v) * along);
43
51
  },
44
52
  height: radius * 2,
45
53
  width: radius * 2,
54
+ ...(regionOf
55
+ ? {
56
+ regionOf: (x, y, z = radius) => {
57
+ const { u, v } = onMap(x, y, z);
58
+ return regionOf(u, v);
59
+ },
60
+ }
61
+ : {}),
46
62
  };
47
63
  };
@@ -1,4 +1,4 @@
1
- import type { ShapeField } from '../connect-graph/shape-field.js';
1
+ import type { ShapeField } from './shape-field.js';
2
2
  /**
3
3
  * The surface of a solid field where its distance is `at`, as triangles:
4
4
  * corners three numbers each in the field's box, and triangles three corners
@@ -1,4 +1,4 @@
1
- import { type ShapeField } from '../connect-graph/shape-field.js';
1
+ import { type ShapeField } from './shape-field.js';
2
2
  /** How a solid is read from triangles. */
3
3
  export interface TriangleOptions {
4
4
  /**
@@ -10,7 +10,7 @@ export interface TriangleOptions {
10
10
  names: readonly string[];
11
11
  ofTriangle: ArrayLike<number>;
12
12
  } | undefined;
13
- /** How many cells the field has along the solid's longest side. 96 by default. */
13
+ /** How many cells the field has along the solid's longest side. 144 by default. */
14
14
  resolution?: number | undefined;
15
15
  }
16
16
  /**
@@ -1,43 +1,62 @@
1
- import { fieldFromCells, SOLID } from '../connect-graph/shape-field.js';
1
+ import { fieldFromCells, SOLID } from './shape-field.js';
2
2
  const NUDGE = 1e-6 * Math.PI;
3
3
  const NEAREST = 40;
4
4
  const nearestPart = (middles, parts, names, box) => {
5
- const size = Math.max(box.width, box.height, box.depth) / NEAREST;
6
- const cells = new Map();
7
- const key = (cx, cy, cz) => `${cx},${cy},${cz}`;
5
+ const size = Math.max(box.width, box.height, box.depth) / NEAREST || 1;
6
+ const nx = Math.ceil(box.width / size) + 1;
7
+ const ny = Math.ceil(box.height / size) + 1;
8
+ const nz = Math.ceil(box.depth / size) + 1;
9
+ const clamp = (value, most) => Math.min(most - 1, Math.max(0, value));
10
+ const cellOf = (x, y, z) => clamp(Math.floor(x / size), nx) +
11
+ nx * (clamp(Math.floor(y / size), ny) + ny * clamp(Math.floor(z / size), nz));
8
12
  const count = middles.length / 3;
13
+ const start = new Int32Array(nx * ny * nz + 1);
14
+ const cellAt = new Int32Array(count);
9
15
  for (let at = 0; at < count; at++) {
10
- const k = key(Math.floor(middles[at * 3] / size), Math.floor(middles[at * 3 + 1] / size), Math.floor(middles[at * 3 + 2] / size));
11
- const list = cells.get(k);
12
- if (list) {
13
- list.push(at);
14
- }
15
- else {
16
- cells.set(k, [at]);
17
- }
16
+ const cell = cellOf(middles[at * 3], middles[at * 3 + 1], middles[at * 3 + 2]);
17
+ cellAt[at] = cell;
18
+ start[cell + 1] = start[cell + 1] + 1;
19
+ }
20
+ for (let cell = 0; cell < nx * ny * nz; cell++) {
21
+ start[cell + 1] = start[cell + 1] + start[cell];
22
+ }
23
+ const filled = start.slice(0, -1);
24
+ const order = new Int32Array(count);
25
+ for (let at = 0; at < count; at++) {
26
+ const cell = cellAt[at];
27
+ order[filled[cell]] = at;
28
+ filled[cell] = filled[cell] + 1;
18
29
  }
30
+ const furthest = Math.max(nx, ny, nz);
19
31
  return (x, y, z = box.depth / 2) => {
20
- const cx = Math.floor(x / size);
21
- const cy = Math.floor(y / size);
22
- const cz = Math.floor(z / size);
32
+ const cx = clamp(Math.floor(x / size), nx);
33
+ const cy = clamp(Math.floor(y / size), ny);
34
+ const cz = clamp(Math.floor(z / size), nz);
23
35
  let best = -1;
24
36
  let nearest = Number.POSITIVE_INFINITY;
25
- for (let ring = 0; ring <= NEAREST; ring++) {
26
- for (let dx = -ring; dx <= ring; dx++) {
37
+ const search = (ix, iy, iz) => {
38
+ if (ix < 0 || iy < 0 || iz < 0 || ix >= nx || iy >= ny || iz >= nz) {
39
+ return;
40
+ }
41
+ const cell = ix + nx * (iy + ny * iz);
42
+ for (let at = start[cell]; at < start[cell + 1]; at++) {
43
+ const one = order[at];
44
+ const far = (middles[one * 3] - x) ** 2 +
45
+ (middles[one * 3 + 1] - y) ** 2 +
46
+ (middles[one * 3 + 2] - z) ** 2;
47
+ if (far < nearest) {
48
+ nearest = far;
49
+ best = one;
50
+ }
51
+ }
52
+ };
53
+ for (let ring = 0; ring <= furthest; ring++) {
54
+ for (let dz = -ring; dz <= ring; dz++) {
55
+ const face = Math.abs(dz) === ring;
27
56
  for (let dy = -ring; dy <= ring; dy++) {
28
- for (let dz = -ring; dz <= ring; dz++) {
29
- if (Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz)) !== ring) {
30
- continue;
31
- }
32
- for (const at of cells.get(key(cx + dx, cy + dy, cz + dz)) ?? []) {
33
- const far = (middles[at * 3] - x) ** 2 +
34
- (middles[at * 3 + 1] - y) ** 2 +
35
- (middles[at * 3 + 2] - z) ** 2;
36
- if (far < nearest) {
37
- nearest = far;
38
- best = at;
39
- }
40
- }
57
+ const edge = face || Math.abs(dy) === ring;
58
+ for (let dx = -ring; dx <= ring; dx += edge || ring === 0 ? 1 : 2 * ring) {
59
+ search(cx + dx, cy + dy, cz + dz);
41
60
  }
42
61
  }
43
62
  }