@logomarks/core 0.1.9 → 0.1.11
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/CHANGELOG.md +27 -0
- package/README.md +3 -1
- package/package.json +15 -3
- package/src/batch/args.d.ts +18 -0
- package/src/batch/args.js +44 -0
- package/src/batch/index.d.ts +7 -0
- package/src/batch/index.js +10 -0
- package/src/batch/run.d.ts +53 -0
- package/src/batch/run.js +92 -0
- package/src/batch/slug.d.ts +2 -0
- package/src/batch/slug.js +12 -0
- package/src/batch/source.d.ts +12 -0
- package/src/batch/source.js +23 -0
- package/src/fit/index.d.ts +4 -0
- package/src/fit/index.js +29 -5
- package/src/geometry/index.d.ts +1 -0
- package/src/geometry/index.js +1 -0
- package/src/geometry/mitre.d.ts +8 -0
- package/src/geometry/mitre.js +64 -0
- package/src/geometry/modular.d.ts +1 -1
- package/src/parts/index.d.ts +2 -0
- package/src/parts/index.js +2 -0
- package/src/parts/pixels.d.ts +11 -0
- package/src/parts/pixels.js +74 -0
- package/src/parts/run.js +4 -2
- package/src/parts/span.d.ts +13 -0
- package/src/parts/span.js +100 -0
- package/src/sdf/fields.js +135 -0
- package/src/sdf/gap.js +42 -0
- package/src/sdf/index.d.ts +56 -0
- package/src/sdf/index.js +12 -0
- package/src/sdf/ops.js +102 -0
- package/src/sdf/trace.js +90 -0
- package/src/skill/bundle.js +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pixel editions: a drawn mark sampled into rows of text ('#' a pixel), the format PIX7 and the pixel animals use. From
|
|
3
|
+
* the Logomarks tools' dither parts (batches 210 and 234).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A kit shape as a polygon set (the kit's primitives return one polygon, its booleans a set).
|
|
8
|
+
*
|
|
9
|
+
* @param {import('../geometry/types.js').Geom} shape The shape.
|
|
10
|
+
* @returns {import('../geometry/types.js').Shape} A polygon set.
|
|
11
|
+
*/
|
|
12
|
+
const polys = (shape) => /** @type {import('../geometry/types.js').Shape} */ (typeof shape[0][0][0] === 'number' ? [shape] : shape);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether a point is inside a kit shape, even-odd over every ring.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('../geometry/types.js').Geom} shape The shape.
|
|
18
|
+
* @param {number} x The point.
|
|
19
|
+
* @param {number} y The point.
|
|
20
|
+
* @returns {boolean} True when it is.
|
|
21
|
+
*/
|
|
22
|
+
export const insideShape = (shape, x, y) => {
|
|
23
|
+
let k = false;
|
|
24
|
+
for (const pg of polys(shape)) for (const r of pg) for (let a = 0, b = r.length - 1; a < r.length; b = a++) {
|
|
25
|
+
const [xa, ya] = r[a], [xb, yb] = r[b];
|
|
26
|
+
if ((ya > y) !== (yb > y) && x < xa + ((y - ya) * (xb - xa)) / (yb - ya)) k = !k;
|
|
27
|
+
}
|
|
28
|
+
return k;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A mark on the 512 canvas as rows of text. Each axis is a list of spans `[from, to, n]`: n pixels across from..to, or
|
|
33
|
+
* `[null, null, n]` for n empty pixels. A tiled mark gives each tile its own span and each gap one empty pixel, so the
|
|
34
|
+
* gaps stay whole pixels (Cove Rise on a 2 × 2 grid: `[[32, 245, 7], [null, null, 1], [267, 480, 7]]`).
|
|
35
|
+
*
|
|
36
|
+
* @param {import('../geometry/types.js').Geom} shape The mark.
|
|
37
|
+
* @param {readonly (readonly (number | null)[])[]} xs The spans across.
|
|
38
|
+
* @param {readonly (readonly (number | null)[])[]} ys The spans down.
|
|
39
|
+
* @param {{ min?: number, k?: number }} [options] A cell is set when the mark covers `min` (0.5) of a k × k (6) sample.
|
|
40
|
+
* @returns {string[]} The rows, '#' a pixel and '.' none.
|
|
41
|
+
*/
|
|
42
|
+
export const rasterRows = (shape, xs, ys, { min = 0.5, k = 6 } = {}) => {
|
|
43
|
+
const sh = polys(shape);
|
|
44
|
+
const cells = (/** @type {readonly (readonly (number | null)[])[]} */ spans) => spans.flatMap(([a, b, n]) => [...Array(/** @type {number} */ (n))].map((_, i) =>
|
|
45
|
+
(a == null ? null : [a + ((/** @type {number} */ (b) - a) * i) / /** @type {number} */ (n), a + ((/** @type {number} */ (b) - a) * (i + 1)) / /** @type {number} */ (n)])));
|
|
46
|
+
const X = cells(xs), Y = cells(ys);
|
|
47
|
+
return Y.map((y) => X.map((x) => {
|
|
48
|
+
if (!x || !y) return '.';
|
|
49
|
+
let n = 0;
|
|
50
|
+
for (let a = 0; a < k; a++) for (let b = 0; b < k; b++) n += +insideShape(sh, x[0] + ((x[1] - x[0]) * (a + 0.5)) / k, y[0] + ((y[1] - y[0]) * (b + 0.5)) / k);
|
|
51
|
+
return n / (k * k) >= min ? '#' : '.';
|
|
52
|
+
}).join(''));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A whole mark as rows of text on square pixels, for the pixel edition of a non-tiled mark: n pixels along its longest
|
|
57
|
+
* side, the grid starting (fx, fy) pixels before its top-left (sweep this phase so the gaps land on whole empty pixels),
|
|
58
|
+
* then each `fix` cell [i, j] toggled by hand (to fill a corner-only contact or clear a stray).
|
|
59
|
+
*
|
|
60
|
+
* @param {import('../geometry/types.js').Geom} shape The mark.
|
|
61
|
+
* @param {number} n Pixels along its longest side.
|
|
62
|
+
* @param {number} [fx] The grid's phase across, in pixels (0).
|
|
63
|
+
* @param {number} [fy] Its phase down (0).
|
|
64
|
+
* @param {{ min?: number, fix?: readonly (readonly number[])[] }} [options] The coverage a pixel needs (0.5), and cells to toggle.
|
|
65
|
+
* @returns {string[]} The rows.
|
|
66
|
+
*/
|
|
67
|
+
export const rasterMark = (shape, n, fx = 0, fy = 0, { min = 0.5, fix = [] } = {}) => {
|
|
68
|
+
const pts = polys(shape).flat(2), xs = pts.map((p) => p[0]), ys = pts.map((p) => p[1]);
|
|
69
|
+
const x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys), p = Math.max(x1 - x0, y1 - y0) / n;
|
|
70
|
+
const nx = Math.ceil((x1 - x0) / p + fx), ny = Math.ceil((y1 - y0) / p + fy), ox = x0 - fx * p, oy = y0 - fy * p;
|
|
71
|
+
const rows = rasterRows(shape, [[ox, ox + nx * p, nx]], [[oy, oy + ny * p, ny]], { min }).map((r) => [...r]);
|
|
72
|
+
for (const [i, j] of fix) rows[j][i] = rows[j][i] === '#' ? '.' : '#';
|
|
73
|
+
return rows.map((r) => r.join(''));
|
|
74
|
+
};
|
package/src/parts/run.js
CHANGED
|
@@ -7,12 +7,14 @@ import { seeded, toPath } from '../geometry/index.js';
|
|
|
7
7
|
import { exact } from './frame.js';
|
|
8
8
|
|
|
9
9
|
/** Writes a batch: every mark in `M` ({ name: { trade, style, fn(g) } }) drawn with exact() so its gap is 22 once fitted,
|
|
10
|
-
* seeded per mark (a re-run is byte-identical); `node genbatchN.mjs <workdir> ["Name,Name" | "fam:Industry · Law"]`.
|
|
10
|
+
* seeded per mark (a re-run is byte-identical); `node genbatchN.mjs <workdir> ["Name,Name" | "fam:Industry · Law"]`.
|
|
11
|
+
* `fam:` also takes the trade as written, for batches that pass a whole family as the trade ("fam:Letters · Latch").
|
|
12
|
+
* New batches use `runBatch` from `@logomarks/core/batch`. */
|
|
11
13
|
export const runIndustry = (batch, M) => {
|
|
12
14
|
const [S, only = ''] = process.argv.slice(2), OUT = path.join(S, `newraw${batch}`), slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
13
15
|
fs.mkdirSync(OUT, { recursive: true }); let n = 0;
|
|
14
16
|
for (const [name, { trade, fn }] of Object.entries(M)) {
|
|
15
|
-
if (only.startsWith('fam:') ? 'Industry · ' + trade
|
|
17
|
+
if (only.startsWith('fam:') ? !['Industry · ' + trade, trade].includes(only.slice(4)) : only && !only.split(',').includes(name)) continue;
|
|
16
18
|
try { const s = exact(fn, () => { Math.random = seeded(name); });
|
|
17
19
|
fs.writeFileSync(path.join(OUT, slug(name) + '.svg'), `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><path fill-rule="evenodd" d="${toPath(s)}"/></svg>\n`); n++; }
|
|
18
20
|
catch (e) { console.log('FAIL', name, e.message.slice(0, 160)); } }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Geom, Shape } from '../geometry/types.js';
|
|
2
|
+
import type { Cell } from './tiles.js';
|
|
3
|
+
|
|
4
|
+
/** One tile token over a w × h block of cells, the gaps inside it filled (oblong blocks: S, t<k>, d<k>, h<k>, q<k>). */
|
|
5
|
+
export function spanTile(tok: string, cs: readonly Cell[], n: number, i: number, j: number, w: number, h: number): Geom;
|
|
6
|
+
/** A grid of tiles from rows of tokens, a token spanning cells as tok^2, tok>2, tok_2 or tok@2x3. */
|
|
7
|
+
export function spanGrid(rows: readonly string[], s: number): Shape;
|
|
8
|
+
/** A shape's longest side. */
|
|
9
|
+
export function longSide(sh: Geom): number;
|
|
10
|
+
/** The cell size that draws the rows 422 at their longest with the gap 22. */
|
|
11
|
+
export function evenCell(rows: readonly string[]): number;
|
|
12
|
+
/** The span grid at evenCell(rows). */
|
|
13
|
+
export function evenGrid(rows: readonly string[]): Shape;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span tiles: a grid of tile() tokens on the 422 span with the house gap, where one token may span a block of cells (the
|
|
3
|
+
* gaps inside it filled), so a big tile sits among unit tiles. From the Logomarks tools (batch 269, shared since 280).
|
|
4
|
+
*/
|
|
5
|
+
import { rr, rect, poly, bbox, ellipse, I, U } from '../geometry/index.js';
|
|
6
|
+
import { gridCells, tile } from './tiles.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One tile token over a w × h block of cells, the gaps inside the block filled. A square block takes every tile() token;
|
|
10
|
+
* an oblong one takes `S` (a slab), `t<k>` (a right triangle keeping corner k: tl, tr, br, bl), `d<k>` (a slab with side
|
|
11
|
+
* k fully round: t, r, b, l; `dp` a pill), `h<k>` (a half ellipse filling the block, flat on side k) and `q<k>` (a
|
|
12
|
+
* quarter ellipse centred on corner k).
|
|
13
|
+
*
|
|
14
|
+
* @param {string} tok The token.
|
|
15
|
+
* @param {readonly import('./tiles.js').Cell[]} cs The grid's cells (gridCells), row by row.
|
|
16
|
+
* @param {number} n Cells across.
|
|
17
|
+
* @param {number} i The block's first column.
|
|
18
|
+
* @param {number} j Its first row.
|
|
19
|
+
* @param {number} w Its width, in cells.
|
|
20
|
+
* @param {number} h Its height, in cells.
|
|
21
|
+
* @returns {import('../geometry/types.js').Geom} The tile.
|
|
22
|
+
* @throws {Error} For a token an oblong block doesn't take.
|
|
23
|
+
*/
|
|
24
|
+
export const spanTile = (tok, cs, n, i, j, w, h) => {
|
|
25
|
+
const a = cs[j * n + i], b = cs[(j + h - 1) * n + (i + w - 1)], x = a.x, y = a.y, W = b.x + b.s - a.x, H = b.y + b.s - a.y;
|
|
26
|
+
if (Math.abs(W - H) < 1e-6) return tile(tok, { i, j, x, y, s: W });
|
|
27
|
+
if (tok === 'S') return rect(x, y, W, H);
|
|
28
|
+
if (tok[0] === 't') {
|
|
29
|
+
/** @type {Record<string, number[]>} */ const K = { tl: [x, y], tr: [x + W, y], br: [x + W, y + H], bl: [x, y + H] };
|
|
30
|
+
const O = ['tl', 'tr', 'br', 'bl'], q = O.indexOf(tok.slice(1));
|
|
31
|
+
return poly([K[O[(q + 3) % 4]], K[O[q]], K[O[(q + 1) % 4]]]);
|
|
32
|
+
}
|
|
33
|
+
if (tok[0] === 'd') {
|
|
34
|
+
const r = Math.min(W, H) / 2, k = tok[1];
|
|
35
|
+
return rr(x, y, W, H, /** @type {Record<string, number[]>} */ ({ t: [r, r, 0, 0], r: [0, r, r, 0], b: [0, 0, r, r], l: [r, 0, 0, r], p: [r, r, r, r] })[k]);
|
|
36
|
+
}
|
|
37
|
+
if (tok[0] === 'h') {
|
|
38
|
+
const k = tok[1], box = rect(x, y, W, H), v = k === 't' || k === 'b';
|
|
39
|
+
const [cx, cy] = /** @type {Record<string, number[]>} */ ({ t: [x + W / 2, y], b: [x + W / 2, y + H], l: [x, y + H / 2], r: [x + W, y + H / 2] })[k];
|
|
40
|
+
return I(ellipse(cx, cy, v ? W / 2 : W, v ? H : H / 2, 0, 400), box);
|
|
41
|
+
}
|
|
42
|
+
if (tok[0] === 'q') {
|
|
43
|
+
const k = tok.slice(1), cx = k[1] === 'r' ? x + W : x, cy = k[0] === 'b' ? y + H : y;
|
|
44
|
+
return I(ellipse(cx, cy, W, H, 0, 400), rect(x, y, W, H));
|
|
45
|
+
}
|
|
46
|
+
throw new Error('span tile ' + tok);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A grid of tiles from rows of tokens, where a token may span cells: `tok^2` a 2 × 2 block (`tok^3` 3 × 3), `tok>2` two
|
|
51
|
+
* cells across, `tok_2` two cells down, `tok@2x3` any block (2 across, 3 down). The cells a block covers are written `-`,
|
|
52
|
+
* an empty cell `.`.
|
|
53
|
+
*
|
|
54
|
+
* @param {readonly string[]} rows The rows of tokens.
|
|
55
|
+
* @param {number} s The cell's size.
|
|
56
|
+
* @returns {import('../geometry/types.js').Shape} The grid.
|
|
57
|
+
*/
|
|
58
|
+
export const spanGrid = (rows, s) => {
|
|
59
|
+
const g = rows.map((r) => r.trim().split(/\s+/)), n = Math.max(...g.map((r) => r.length)), m = g.length, cs = gridCells(n, m, { s }), out = [];
|
|
60
|
+
g.forEach((row, j) => row.forEach((t, i) => {
|
|
61
|
+
if (t === '.' || t === '-') return;
|
|
62
|
+
const bx = t.match(/^(.+?)@(\d)x(\d)$/);
|
|
63
|
+
if (bx) { out.push(spanTile(bx[1], cs, n, i, j, +bx[2], +bx[3])); return; }
|
|
64
|
+
const mm = t.match(/^(.+?)(?:\^(\d)|>(\d)|_(\d))$/);
|
|
65
|
+
if (!mm) { out.push(tile(t, cs[j * n + i])); return; }
|
|
66
|
+
const [, tok, sq, wd, ht] = mm, w = +(sq || wd || 1), h = +(sq || ht || 1);
|
|
67
|
+
out.push(spanTile(tok, cs, n, i, j, w, h));
|
|
68
|
+
}));
|
|
69
|
+
return U(...out);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A shape's longest side.
|
|
74
|
+
*
|
|
75
|
+
* @param {import('../geometry/types.js').Geom} sh The shape.
|
|
76
|
+
* @returns {number} Its longer box side.
|
|
77
|
+
*/
|
|
78
|
+
export const longSide = (sh) => { const [x0, y0, x1, y1] = bbox(sh); return Math.max(x1 - x0, y1 - y0); };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The cell size that draws `rows` so the mark (not the grid) is 422 at its longest with the gap 22, so a mark whose
|
|
82
|
+
* tiles stop short of the grid's edge fits to 448 with the family's gap. The longest side is linear in the cell, so
|
|
83
|
+
* two trial builds find it.
|
|
84
|
+
*
|
|
85
|
+
* @param {readonly string[]} rows The rows of tokens.
|
|
86
|
+
* @returns {number} The cell size.
|
|
87
|
+
*/
|
|
88
|
+
export const evenCell = (rows) => {
|
|
89
|
+
const n = Math.max(...rows.map((r) => r.trim().split(/\s+/).length), rows.length), s0 = (422 - (n - 1) * 22) / n;
|
|
90
|
+
const a = longSide(spanGrid(rows, s0)), b = longSide(spanGrid(rows, s0 * 1.2));
|
|
91
|
+
return s0 + ((422 - a) * 0.2 * s0) / (b - a);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The span grid at evenCell(rows).
|
|
96
|
+
*
|
|
97
|
+
* @param {readonly string[]} rows The rows of tokens.
|
|
98
|
+
* @returns {import('../geometry/types.js').Shape} The grid.
|
|
99
|
+
*/
|
|
100
|
+
export const evenGrid = (rows) => spanGrid(rows, evenCell(rows));
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed distance fields: a field is a function (x, y) → distance, negative inside, positive outside, on the 512 canvas.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A disc.
|
|
7
|
+
*
|
|
8
|
+
* @param {number} cx Its centre.
|
|
9
|
+
* @param {number} cy Its centre.
|
|
10
|
+
* @param {number} r Its radius.
|
|
11
|
+
* @returns {import('./index.js').Field} The field.
|
|
12
|
+
*/
|
|
13
|
+
export const sdCircle = (cx, cy, r) => (x, y) => Math.hypot(x - cx, y - cy) - r;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The distance from a point to the segment a–b, and where along it the nearest point lies.
|
|
17
|
+
*
|
|
18
|
+
* @param {readonly number[]} a The segment's start.
|
|
19
|
+
* @param {readonly number[]} b Its end.
|
|
20
|
+
* @param {number} x The point.
|
|
21
|
+
* @param {number} y The point.
|
|
22
|
+
* @returns {[number, number]} The distance, and t from 0 (at a) to 1 (at b).
|
|
23
|
+
*/
|
|
24
|
+
export const segT = ([ax, ay], [bx, by], x, y) => {
|
|
25
|
+
const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy || 1e-9;
|
|
26
|
+
const t = Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / L2));
|
|
27
|
+
return [Math.hypot(x - ax - t * dx, y - ay - t * dy), t];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A capsule: the segment a–b with round ends.
|
|
32
|
+
*
|
|
33
|
+
* @param {readonly number[]} a One end's centre.
|
|
34
|
+
* @param {readonly number[]} b The other's.
|
|
35
|
+
* @param {number} w Its width.
|
|
36
|
+
* @returns {import('./index.js').Field} The field.
|
|
37
|
+
*/
|
|
38
|
+
export const sdCapsule = (a, b, w) => (x, y) => segT(a, b, x, y)[0] - w / 2;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A stroke along a polyline with round ends. Its width may be a function of the run t (0 to 1), for a waist or a swell.
|
|
42
|
+
*
|
|
43
|
+
* @param {readonly (readonly number[])[]} pts The polyline.
|
|
44
|
+
* @param {number | ((t: number) => number)} w The width, or the width along the run.
|
|
45
|
+
* @returns {import('./index.js').Field} The field.
|
|
46
|
+
*/
|
|
47
|
+
export const sdPath = (pts, w) => {
|
|
48
|
+
const n = pts.length - 1, W = typeof w === 'function' ? w : () => w;
|
|
49
|
+
return (x, y) => {
|
|
50
|
+
let m = Infinity;
|
|
51
|
+
for (let i = 1; i <= n; i++) {
|
|
52
|
+
const [d, t] = segT(pts[i - 1], pts[i], x, y);
|
|
53
|
+
m = Math.min(m, d - W((i - 1 + t) / n) / 2);
|
|
54
|
+
}
|
|
55
|
+
return m;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Points along an arc (y down, so increasing angles run clockwise), for sdPath.
|
|
61
|
+
*
|
|
62
|
+
* @param {number} cx Its centre.
|
|
63
|
+
* @param {number} cy Its centre.
|
|
64
|
+
* @param {number} r Its radius.
|
|
65
|
+
* @param {number} a0 The start angle, in degrees.
|
|
66
|
+
* @param {number} a1 The end angle, in degrees.
|
|
67
|
+
* @param {number} [n] The number of steps (48).
|
|
68
|
+
* @returns {[number, number][]} n + 1 points.
|
|
69
|
+
*/
|
|
70
|
+
export const arcPts = (cx, cy, r, a0, a1, n = 48) => [...Array(n + 1)].map((_, i) => {
|
|
71
|
+
const a = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180;
|
|
72
|
+
return /** @type {[number, number]} */ ([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A rounded box.
|
|
77
|
+
*
|
|
78
|
+
* @param {number} cx Its centre.
|
|
79
|
+
* @param {number} cy Its centre.
|
|
80
|
+
* @param {number} w Its width.
|
|
81
|
+
* @param {number} h Its height.
|
|
82
|
+
* @param {number} [r] Its corner radius (0).
|
|
83
|
+
* @returns {import('./index.js').Field} The field.
|
|
84
|
+
*/
|
|
85
|
+
export const sdBox = (cx, cy, w, h, r = 0) => (x, y) => {
|
|
86
|
+
const qx = Math.abs(x - cx) - w / 2 + r, qy = Math.abs(y - cy) - h / 2 + r;
|
|
87
|
+
return Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) - r;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* An ellipse, as a scaled disc: exact enough for its outline, not for a gap cut against it.
|
|
92
|
+
*
|
|
93
|
+
* @param {number} cx Its centre.
|
|
94
|
+
* @param {number} cy Its centre.
|
|
95
|
+
* @param {number} rx Its radius across.
|
|
96
|
+
* @param {number} ry Its radius down.
|
|
97
|
+
* @param {number} [deg] Its turn, in degrees (0).
|
|
98
|
+
* @returns {import('./index.js').Field} The field (approximate).
|
|
99
|
+
*/
|
|
100
|
+
export const sdEllipse = (cx, cy, rx, ry, deg = 0) => {
|
|
101
|
+
const a = (deg * Math.PI) / 180, c = Math.cos(a), s = Math.sin(a), m = Math.min(rx, ry);
|
|
102
|
+
return (x, y) => {
|
|
103
|
+
const u = (x - cx) * c + (y - cy) * s, v = -(x - cx) * s + (y - cy) * c;
|
|
104
|
+
return (Math.hypot(u / rx, v / ry) - 1) * m;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A ring (an annulus).
|
|
110
|
+
*
|
|
111
|
+
* @param {number} cx Its centre.
|
|
112
|
+
* @param {number} cy Its centre.
|
|
113
|
+
* @param {number} r Its centre line's radius.
|
|
114
|
+
* @param {number} w Its width.
|
|
115
|
+
* @returns {import('./index.js').Field} The field.
|
|
116
|
+
*/
|
|
117
|
+
export const sdRing = (cx, cy, r, w) => (x, y) => Math.abs(Math.hypot(x - cx, y - cy) - r) - w / 2;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A polygon: exact, so it rounds (offset) and takes a gap cleanly.
|
|
121
|
+
*
|
|
122
|
+
* @param {readonly (readonly number[])[]} pts Its corners, in order (either winding).
|
|
123
|
+
* @returns {import('./index.js').Field} The field.
|
|
124
|
+
*/
|
|
125
|
+
export const sdPoly = (pts) => (x, y) => {
|
|
126
|
+
let d = Infinity, s = 1;
|
|
127
|
+
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
|
128
|
+
const [ax, ay] = pts[j], [bx, by] = pts[i], ex = bx - ax, ey = by - ay, wx = x - ax, wy = y - ay;
|
|
129
|
+
const t = Math.max(0, Math.min(1, (wx * ex + wy * ey) / (ex * ex + ey * ey || 1e-9))), dx = wx - ex * t, dy = wy - ey * t;
|
|
130
|
+
d = Math.min(d, dx * dx + dy * dy);
|
|
131
|
+
const c1 = y >= ay, c2 = y < by, c3 = ex * wy > ey * wx;
|
|
132
|
+
if ((c1 && c2 && c3) || (!c1 && !c2 && !c3)) s = -s;
|
|
133
|
+
}
|
|
134
|
+
return s * Math.sqrt(d);
|
|
135
|
+
};
|
package/src/sdf/gap.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The house gap in fields: cuts that stay exactly g wide, and one shape laid over another.
|
|
3
|
+
*/
|
|
4
|
+
import { segT } from './fields.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A band g wide cut along each line, its edges left crisp.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('./index.js').Field} f The field.
|
|
10
|
+
* @param {readonly (readonly (readonly number[])[])[]} lines Segments or polylines, each a list of points.
|
|
11
|
+
* @param {number} g The gap.
|
|
12
|
+
* @returns {import('./index.js').Field} The field, cut.
|
|
13
|
+
*/
|
|
14
|
+
export const gapCut = (f, lines, g) => (x, y) => {
|
|
15
|
+
let m = Infinity;
|
|
16
|
+
for (const L of lines) for (let i = 1; i < L.length; i++) m = Math.min(m, segT(L[i - 1], L[i], x, y)[0]);
|
|
17
|
+
return Math.max(f(x, y), g / 2 - m);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One field laid over another, cut free of it by the gap. `front` must be a true distance near its edge.
|
|
22
|
+
*
|
|
23
|
+
* @param {import('./index.js').Field} back The field behind.
|
|
24
|
+
* @param {import('./index.js').Field} front The field in front.
|
|
25
|
+
* @param {number} g The gap.
|
|
26
|
+
* @returns {import('./index.js').Field} Both, front over back.
|
|
27
|
+
*/
|
|
28
|
+
export const fover = (back, front, g) => (x, y) => Math.min(front(x, y), Math.max(back(x, y), g - front(x, y)));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Two fields linked: a over b where `aOver` holds (a half-plane, say), b over a elsewhere.
|
|
32
|
+
*
|
|
33
|
+
* @param {import('./index.js').Field} a One field.
|
|
34
|
+
* @param {import('./index.js').Field} b The other.
|
|
35
|
+
* @param {number} g The gap.
|
|
36
|
+
* @param {(x: number, y: number) => boolean} aOver Where a goes over.
|
|
37
|
+
* @returns {import('./index.js').Field} The link.
|
|
38
|
+
*/
|
|
39
|
+
export const flink = (a, b, g, aOver) => {
|
|
40
|
+
const ab = fover(b, a, g), ba = fover(a, b, g);
|
|
41
|
+
return (x, y) => (aOver(x, y) ? ab(x, y) : ba(x, y));
|
|
42
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** A signed distance field: (x, y) → distance, negative inside, positive outside. */
|
|
4
|
+
export type Field = (x: number, y: number) => number;
|
|
5
|
+
/** A point. */
|
|
6
|
+
export type Pt = readonly number[];
|
|
7
|
+
|
|
8
|
+
/** A disc. */
|
|
9
|
+
export function sdCircle(cx: number, cy: number, r: number): Field;
|
|
10
|
+
/** The distance to the segment a–b, and t (0 at a, 1 at b) where the nearest point lies. */
|
|
11
|
+
export function segT(a: Pt, b: Pt, x: number, y: number): [number, number];
|
|
12
|
+
/** The segment a–b with round ends, w wide. */
|
|
13
|
+
export function sdCapsule(a: Pt, b: Pt, w: number): Field;
|
|
14
|
+
/** A round-ended stroke along a polyline, w wide (or w(t) along the run). */
|
|
15
|
+
export function sdPath(pts: readonly Pt[], w: number | ((t: number) => number)): Field;
|
|
16
|
+
/** Points along an arc from a0 to a1 degrees (y down), n + 1 of them (n = 48). */
|
|
17
|
+
export function arcPts(cx: number, cy: number, r: number, a0: number, a1: number, n?: number): [number, number][];
|
|
18
|
+
/** A rounded box centred on (cx, cy). */
|
|
19
|
+
export function sdBox(cx: number, cy: number, w: number, h: number, r?: number): Field;
|
|
20
|
+
/** An ellipse turned deg, as a scaled disc (approximate). */
|
|
21
|
+
export function sdEllipse(cx: number, cy: number, rx: number, ry: number, deg?: number): Field;
|
|
22
|
+
/** A ring of centre-line radius r and width w. */
|
|
23
|
+
export function sdRing(cx: number, cy: number, r: number, w: number): Field;
|
|
24
|
+
/** A polygon, exact. */
|
|
25
|
+
export function sdPoly(pts: readonly Pt[]): Field;
|
|
26
|
+
|
|
27
|
+
/** A band w wide along a field's zero line. */
|
|
28
|
+
export function shell(f: Field, w: number): Field;
|
|
29
|
+
/** A field turned deg about (cx, cy) (256, 256). */
|
|
30
|
+
export function turn(f: Field, deg: number, cx?: number, cy?: number): Field;
|
|
31
|
+
/** A field grown (d > 0) or shrunk (d < 0). */
|
|
32
|
+
export function offset(f: Field, d: number): Field;
|
|
33
|
+
/** The polynomial smooth minimum of two distances, blend k. */
|
|
34
|
+
export function smin2(a: number, b: number, k: number): number;
|
|
35
|
+
/** A smooth union, blend k. */
|
|
36
|
+
export function smin(k: number, ...fs: Field[]): Field;
|
|
37
|
+
/** A smooth intersection, blend k. */
|
|
38
|
+
export function smax(k: number, ...fs: Field[]): Field;
|
|
39
|
+
/** a minus b, the cut's edges softened by k. */
|
|
40
|
+
export function ssub(k: number, a: Field, b: Field): Field;
|
|
41
|
+
/** A sharp union. */
|
|
42
|
+
export function fmin(...fs: Field[]): Field;
|
|
43
|
+
/** A sharp intersection. */
|
|
44
|
+
export function fmax(...fs: Field[]): Field;
|
|
45
|
+
/** a minus b, sharp. */
|
|
46
|
+
export function fsub(a: Field, b: Field): Field;
|
|
47
|
+
|
|
48
|
+
/** A band g wide cut along each line (a list of points), edges crisp. */
|
|
49
|
+
export function gapCut(f: Field, lines: readonly (readonly Pt[])[], g: number): Field;
|
|
50
|
+
/** front laid over back, cut free of it by the gap. */
|
|
51
|
+
export function fover(back: Field, front: Field, g: number): Field;
|
|
52
|
+
/** a over b where aOver holds, b over a elsewhere. */
|
|
53
|
+
export function flink(a: Field, b: Field, g: number, aOver: (x: number, y: number) => boolean): Field;
|
|
54
|
+
|
|
55
|
+
/** A field's zero contour traced into a kit shape (marching squares, step h = 1, over −64…576). */
|
|
56
|
+
export function shapeOf(f: Field, area?: { h?: number; x0?: number; y0?: number; x1?: number; y1?: number }): Shape;
|
package/src/sdf/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/sdf
|
|
3
|
+
*
|
|
4
|
+
* Shapes drawn as signed distance fields and traced to polygons, so unions melt into each other with real fillets
|
|
5
|
+
* (a smooth minimum) instead of meeting at a crease. A field is (x, y) → distance, negative inside, on the 512 canvas.
|
|
6
|
+
* Build fields, combine them (smin, smax, ssub, fmin, fmax, fsub), cut the house gap (gapCut, fover, flink), then
|
|
7
|
+
* shapeOf() traces the result into a kit shape for the usual booleans and fit.
|
|
8
|
+
*/
|
|
9
|
+
export { sdCircle, segT, sdCapsule, sdPath, arcPts, sdBox, sdEllipse, sdRing, sdPoly } from './fields.js';
|
|
10
|
+
export { shell, turn, offset, smin2, smin, smax, ssub, fmin, fmax, fsub } from './ops.js';
|
|
11
|
+
export { gapCut, fover, flink } from './gap.js';
|
|
12
|
+
export { shapeOf } from './trace.js';
|
package/src/sdf/ops.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combining and changing fields: smooth and sharp unions, intersections and cuts, turns, offsets and outlines.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A band along a field's zero line: any outline turned into a ring of width w.
|
|
7
|
+
*
|
|
8
|
+
* @param {import('./index.js').Field} f The field.
|
|
9
|
+
* @param {number} w The band's width.
|
|
10
|
+
* @returns {import('./index.js').Field} The band.
|
|
11
|
+
*/
|
|
12
|
+
export const shell = (f, w) => (x, y) => Math.abs(f(x, y)) - w / 2;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A field turned about a point.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('./index.js').Field} f The field.
|
|
18
|
+
* @param {number} deg The turn, in degrees (clockwise on the y-down canvas).
|
|
19
|
+
* @param {number} [cx] The pivot (256).
|
|
20
|
+
* @param {number} [cy] The pivot (256).
|
|
21
|
+
* @returns {import('./index.js').Field} The turned field.
|
|
22
|
+
*/
|
|
23
|
+
export const turn = (f, deg, cx = 256, cy = 256) => {
|
|
24
|
+
const a = (-deg * Math.PI) / 180, c = Math.cos(a), s = Math.sin(a);
|
|
25
|
+
return (x, y) => f(cx + (x - cx) * c - (y - cy) * s, cy + (x - cx) * s + (y - cy) * c);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A field grown (d > 0) or shrunk (d < 0). Shrinking a polygon by k and growing it back rounds its convex corners to k.
|
|
30
|
+
*
|
|
31
|
+
* @param {import('./index.js').Field} f The field.
|
|
32
|
+
* @param {number} d How far.
|
|
33
|
+
* @returns {import('./index.js').Field} The field, offset.
|
|
34
|
+
*/
|
|
35
|
+
export const offset = (f, d) => (x, y) => f(x, y) - d;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The polynomial smooth minimum of two distances: a union whose seam melts into a fillet about k wide.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} a One distance.
|
|
41
|
+
* @param {number} b The other.
|
|
42
|
+
* @param {number} k The blend (0 for a sharp minimum).
|
|
43
|
+
* @returns {number} The blended distance.
|
|
44
|
+
*/
|
|
45
|
+
export const smin2 = (a, b, k) => {
|
|
46
|
+
if (k <= 0) return Math.min(a, b);
|
|
47
|
+
const h = Math.max(k - Math.abs(a - b), 0) / k;
|
|
48
|
+
return Math.min(a, b) - (h * h * k) / 4;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A smooth union.
|
|
53
|
+
*
|
|
54
|
+
* @param {number} k The blend.
|
|
55
|
+
* @param {...import('./index.js').Field} fs The fields.
|
|
56
|
+
* @returns {import('./index.js').Field} Their union, seams filleted.
|
|
57
|
+
*/
|
|
58
|
+
export const smin = (k, ...fs) => (x, y) => fs.reduce((m, f, i) => (i ? smin2(m, f(x, y), k) : f(x, y)), 0);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A smooth intersection.
|
|
62
|
+
*
|
|
63
|
+
* @param {number} k The blend.
|
|
64
|
+
* @param {...import('./index.js').Field} fs The fields.
|
|
65
|
+
* @returns {import('./index.js').Field} Their intersection, edges softened.
|
|
66
|
+
*/
|
|
67
|
+
export const smax = (k, ...fs) => (x, y) => -fs.reduce((m, f, i) => (i ? smin2(m, -f(x, y), k) : -f(x, y)), 0);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* One field cut by another, the cut's edges softened.
|
|
71
|
+
*
|
|
72
|
+
* @param {number} k The blend.
|
|
73
|
+
* @param {import('./index.js').Field} a The field.
|
|
74
|
+
* @param {import('./index.js').Field} b What is cut out of it.
|
|
75
|
+
* @returns {import('./index.js').Field} a minus b.
|
|
76
|
+
*/
|
|
77
|
+
export const ssub = (k, a, b) => (x, y) => -smin2(-a(x, y), b(x, y), k);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A sharp union.
|
|
81
|
+
*
|
|
82
|
+
* @param {...import('./index.js').Field} fs The fields.
|
|
83
|
+
* @returns {import('./index.js').Field} Their union.
|
|
84
|
+
*/
|
|
85
|
+
export const fmin = (...fs) => (x, y) => Math.min(...fs.map((f) => f(x, y)));
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A sharp intersection.
|
|
89
|
+
*
|
|
90
|
+
* @param {...import('./index.js').Field} fs The fields.
|
|
91
|
+
* @returns {import('./index.js').Field} Their intersection.
|
|
92
|
+
*/
|
|
93
|
+
export const fmax = (...fs) => (x, y) => Math.max(...fs.map((f) => f(x, y)));
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A sharp cut.
|
|
97
|
+
*
|
|
98
|
+
* @param {import('./index.js').Field} a The field.
|
|
99
|
+
* @param {import('./index.js').Field} b What is cut out of it.
|
|
100
|
+
* @returns {import('./index.js').Field} a minus b.
|
|
101
|
+
*/
|
|
102
|
+
export const fsub = (a, b) => (x, y) => Math.max(a(x, y), -b(x, y));
|
package/src/sdf/trace.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracing a field into a kit shape: marching squares on its zero contour, so the kit's booleans work on the result.
|
|
3
|
+
*/
|
|
4
|
+
import { X } from '../geometry/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A closed ring with the points that lie (within tol) on the line through their neighbours dropped.
|
|
8
|
+
*
|
|
9
|
+
* @param {number[][]} r The ring.
|
|
10
|
+
* @param {number} [tol] How far off the line a point must be to stay (0.04).
|
|
11
|
+
* @returns {number[][]} The ring, thinned.
|
|
12
|
+
*/
|
|
13
|
+
const thin = (r, tol = 0.04) => {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (let i = 0; i < r.length; i++) {
|
|
16
|
+
const a = out.length ? out[out.length - 1] : r[r.length - 1], p = r[i], b = r[(i + 1) % r.length];
|
|
17
|
+
const L = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1e-9, d = Math.abs((b[0] - a[0]) * (a[1] - p[1]) - (a[0] - p[0]) * (b[1] - a[1])) / L;
|
|
18
|
+
if (d > tol) out.push(p);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A field's zero contour (inside < 0) as a kit shape: marching squares on a grid of step h with linear interpolation,
|
|
25
|
+
* saddles split by the cell's mean, rings XORed so holes come out as holes.
|
|
26
|
+
*
|
|
27
|
+
* @param {import('./index.js').Field} f The field.
|
|
28
|
+
* @param {{ h?: number, x0?: number, y0?: number, x1?: number, y1?: number }} [area] The grid step (1) and the area
|
|
29
|
+
* traced (−64 to 576 each way).
|
|
30
|
+
* @returns {import('../geometry/types.js').Shape} The shape (empty when the field is nowhere inside).
|
|
31
|
+
*/
|
|
32
|
+
export function shapeOf(f, { h = 1, x0 = -64, y0 = -64, x1 = 576, y1 = 576 } = {}) {
|
|
33
|
+
const nx = Math.ceil((x1 - x0) / h) + 1, ny = Math.ceil((y1 - y0) / h) + 1, v = new Float64Array(nx * ny);
|
|
34
|
+
for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
|
|
35
|
+
let d = f(x0 + i * h, y0 + j * h);
|
|
36
|
+
if (d === 0) d = 1e-9;
|
|
37
|
+
v[j * nx + i] = d;
|
|
38
|
+
}
|
|
39
|
+
const V = (/** @type {number} */ i, /** @type {number} */ j) => v[j * nx + i];
|
|
40
|
+
/** @type {Map<string, number[]>} */ const P = new Map();
|
|
41
|
+
/** @type {Map<string, string[]>} */ const adj = new Map();
|
|
42
|
+
// an edge point, keyed by its grid edge: 'h' runs from (i, j) to (i+1, j), 'v' from (i, j) to (i, j+1)
|
|
43
|
+
const pt = (/** @type {'h' | 'v'} */ kind, /** @type {number} */ i, /** @type {number} */ j) => {
|
|
44
|
+
const key = `${kind}${i},${j}`;
|
|
45
|
+
if (!P.has(key)) {
|
|
46
|
+
const a = V(i, j), b = kind === 'h' ? V(i + 1, j) : V(i, j + 1), t = a / (a - b);
|
|
47
|
+
P.set(key, kind === 'h' ? [x0 + (i + t) * h, y0 + j * h] : [x0 + i * h, y0 + (j + t) * h]);
|
|
48
|
+
}
|
|
49
|
+
return key;
|
|
50
|
+
};
|
|
51
|
+
const link = (/** @type {string} */ a, /** @type {string} */ b) => {
|
|
52
|
+
(adj.get(a) || adj.set(a, []).get(a))?.push(b);
|
|
53
|
+
(adj.get(b) || adj.set(b, []).get(b))?.push(a);
|
|
54
|
+
};
|
|
55
|
+
for (let j = 0; j < ny - 1; j++) for (let i = 0; i < nx - 1; i++) {
|
|
56
|
+
const tl = V(i, j) < 0, tr = V(i + 1, j) < 0, br = V(i + 1, j + 1) < 0, bl = V(i, j + 1) < 0;
|
|
57
|
+
const c = +tl * 8 + +tr * 4 + +br * 2 + +bl;
|
|
58
|
+
if (c === 0 || c === 15) continue;
|
|
59
|
+
const T = () => pt('h', i, j), B = () => pt('h', i, j + 1), L = () => pt('v', i, j), R = () => pt('v', i + 1, j);
|
|
60
|
+
const mid = (V(i, j) + V(i + 1, j) + V(i + 1, j + 1) + V(i, j + 1)) / 4 < 0;
|
|
61
|
+
switch (c) {
|
|
62
|
+
case 1: case 14: link(L(), B()); break;
|
|
63
|
+
case 2: case 13: link(B(), R()); break;
|
|
64
|
+
case 3: case 12: link(L(), R()); break;
|
|
65
|
+
case 4: case 11: link(T(), R()); break;
|
|
66
|
+
case 6: case 9: link(T(), B()); break;
|
|
67
|
+
case 7: case 8: link(L(), T()); break;
|
|
68
|
+
case 5: if (mid) { link(L(), T()); link(B(), R()); } else { link(T(), R()); link(L(), B()); } break;
|
|
69
|
+
case 10: if (mid) { link(T(), R()); link(L(), B()); } else { link(L(), T()); link(B(), R()); } break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const seen = new Set(), rings = [];
|
|
73
|
+
for (const start of adj.keys()) {
|
|
74
|
+
if (seen.has(start)) continue;
|
|
75
|
+
const ring = [];
|
|
76
|
+
/** @type {string | null} */ let prev = null;
|
|
77
|
+
/** @type {string | undefined} */ let cur = start;
|
|
78
|
+
while (cur && !seen.has(cur)) {
|
|
79
|
+
seen.add(cur);
|
|
80
|
+
ring.push(/** @type {number[]} */ (P.get(cur)));
|
|
81
|
+
const nb = /** @type {string[]} */ (adj.get(cur));
|
|
82
|
+
const nxt = nb[0] === prev ? nb[1] : nb[0];
|
|
83
|
+
prev = cur;
|
|
84
|
+
cur = nxt;
|
|
85
|
+
}
|
|
86
|
+
const r = thin(ring);
|
|
87
|
+
if (r.length >= 3) rings.push(r);
|
|
88
|
+
}
|
|
89
|
+
return rings.length ? X(...rings.map((r) => [[...r, r[0]]])) : [];
|
|
90
|
+
}
|
package/src/skill/bundle.js
CHANGED
|
@@ -14,7 +14,7 @@ const SRC = new URL('../', import.meta.url);
|
|
|
14
14
|
|
|
15
15
|
/** The geometry kit's modules, in dependency order. */
|
|
16
16
|
const KIT = ['geometry/core.js', 'geometry/booleans.js', 'geometry/polygon.js', 'geometry/random.js', 'geometry/shapes.js', 'geometry/stroke.js',
|
|
17
|
-
'geometry/path.js', 'geometry/offset.js', 'geometry/iso.js', 'geometry/modular.js', 'geometry/badges.js', 'geometry/letters.js',
|
|
17
|
+
'geometry/path.js', 'geometry/offset.js', 'geometry/mitre.js', 'geometry/iso.js', 'geometry/modular.js', 'geometry/badges.js', 'geometry/letters.js',
|
|
18
18
|
'svg/path.js', 'svg/shapes.js', 'svg/rings.js', 'svg/nonzero.js', 'svg/shape.js', 'geometry/glitch.js', 'geometry/needle.js'];
|
|
19
19
|
|
|
20
20
|
/** The fit's modules, in dependency order. */
|