@logomarks/core 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +10 -1
- package/package.json +59 -4
- 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/font/glyphs.d.ts +31 -0
- package/src/font/glyphs.js +228 -0
- package/src/font/index.d.ts +10 -0
- package/src/font/index.js +10 -0
- package/src/font/measure.d.ts +16 -0
- package/src/font/measure.js +201 -0
- package/src/font/moves.d.ts +42 -0
- package/src/font/moves.js +263 -0
- package/src/forms/discs.d.ts +20 -0
- package/src/forms/discs.js +114 -0
- package/src/forms/emblems.d.ts +22 -0
- package/src/forms/emblems.js +109 -0
- package/src/forms/flow.d.ts +21 -0
- package/src/forms/flow.js +80 -0
- package/src/forms/folds.d.ts +18 -0
- package/src/forms/folds.js +147 -0
- package/src/forms/geo.d.ts +46 -0
- package/src/forms/geo.js +233 -0
- package/src/forms/hooks.d.ts +40 -0
- package/src/forms/hooks.js +204 -0
- package/src/forms/index.d.ts +22 -0
- package/src/forms/index.js +24 -0
- package/src/forms/jog.d.ts +33 -0
- package/src/forms/jog.js +162 -0
- package/src/forms/lines.d.ts +32 -0
- package/src/forms/lines.js +233 -0
- package/src/forms/ref.d.ts +41 -0
- package/src/forms/ref.js +246 -0
- package/src/forms/shatter.d.ts +40 -0
- package/src/forms/shatter.js +341 -0
- package/src/forms/slats.d.ts +35 -0
- package/src/forms/slats.js +78 -0
- package/src/forms/swash.d.ts +23 -0
- package/src/forms/swash.js +74 -0
- package/src/forms/tech.d.ts +40 -0
- package/src/forms/tech.js +293 -0
- package/src/forms/tracks.d.ts +36 -0
- package/src/forms/tracks.js +121 -0
- 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/index.js +2 -1
- package/src/iso/index.d.ts +5 -0
- package/src/iso/index.js +13 -0
- package/src/iso/objects.d.ts +168 -0
- package/src/iso/objects.js +1042 -0
- package/src/iso/planes.d.ts +27 -0
- package/src/iso/planes.js +138 -0
- package/src/iso/voxels.d.ts +43 -0
- package/src/iso/voxels.js +255 -0
- package/src/letters/blackletter.d.ts +31 -0
- package/src/letters/blackletter.js +199 -0
- package/src/letters/dotstroke.d.ts +16 -0
- package/src/letters/dotstroke.js +77 -0
- package/src/letters/echo.d.ts +42 -0
- package/src/letters/echo.js +246 -0
- package/src/letters/frame.d.ts +6 -0
- package/src/letters/frame.js +14 -0
- package/src/letters/index.d.ts +16 -0
- package/src/letters/index.js +18 -0
- package/src/letters/inktrap.d.ts +66 -0
- package/src/letters/inktrap.js +244 -0
- package/src/letters/mono.d.ts +145 -0
- package/src/letters/mono.js +684 -0
- package/src/letters/pillar.d.ts +31 -0
- package/src/letters/pillar.js +257 -0
- package/src/letters/serif.d.ts +109 -0
- package/src/letters/serif.js +666 -0
- package/src/letters/stair.d.ts +8 -0
- package/src/letters/stair.js +43 -0
- package/src/letters/stem.d.ts +29 -0
- package/src/letters/stem.js +205 -0
- package/src/objects/animals.d.ts +105 -0
- package/src/objects/animals.js +469 -0
- package/src/objects/bio.d.ts +14 -0
- package/src/objects/bio.js +76 -0
- package/src/objects/devglyphs.d.ts +34 -0
- package/src/objects/devglyphs.js +226 -0
- package/src/objects/devices.d.ts +31 -0
- package/src/objects/devices.js +114 -0
- package/src/objects/faces.d.ts +94 -0
- package/src/objects/faces.js +344 -0
- package/src/objects/food.d.ts +12 -0
- package/src/objects/food.js +74 -0
- package/src/objects/index.d.ts +13 -0
- package/src/objects/index.js +18 -0
- package/src/objects/rosettes.d.ts +14 -0
- package/src/objects/rosettes.js +64 -0
- package/src/objects/sport.d.ts +8 -0
- package/src/objects/sport.js +37 -0
- package/src/objects/weapons.d.ts +31 -0
- package/src/objects/weapons.js +179 -0
- 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/pixels/dither.d.ts +78 -0
- package/src/pixels/dither.js +399 -0
- package/src/pixels/emote.d.ts +6 -0
- package/src/pixels/emote.js +76 -0
- package/src/pixels/index.d.ts +7 -0
- package/src/pixels/index.js +8 -0
- package/src/sdf/fields.js +135 -0
- package/src/sdf/gap.js +42 -0
- package/src/sdf/index.d.ts +58 -0
- package/src/sdf/index.js +14 -0
- package/src/sdf/ops.js +102 -0
- package/src/sdf/trace.js +90 -0
- package/src/sdf/y2k.d.ts +12 -0
- package/src/sdf/y2k.js +80 -0
- package/src/skill/bundle.js +1 -1
- package/src/util/vec.d.ts +14 -0
- package/src/util/vec.js +61 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning slats: a row of flat slabs, each seen turning on its own axis, so the row reads as one panel opening, a page
|
|
3
|
+
* turning or a fan. A slab turned by τ (the sine of its turn, −1 to 1) shows a projected width of W·cos and keeps its
|
|
4
|
+
* edges upright; its near edge grows and its far edge shrinks by the perspective k (half-heights H/2 · (1 ± k·τ)), so
|
|
5
|
+
* its top and bottom edges slope towards the far side; τ > 0 turns the right edge away. The gaps are upright strips, so
|
|
6
|
+
* they stay exactly g, and no slab comes out narrower than wMin (an edge-on slab is a sliver, never a hairline).
|
|
7
|
+
* From the Logomarks tools (slatparts.mjs, batch 236, "Bold · Slats").
|
|
8
|
+
*/
|
|
9
|
+
import { poly, U } from '../geometry/index.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The projected width of a slab of full width W turned by τ, never under wMin.
|
|
13
|
+
*
|
|
14
|
+
* @param {number} W The slab's full width.
|
|
15
|
+
* @param {number} tau The sine of its turn (−1 to 1).
|
|
16
|
+
* @param {number} wMin The narrowest it may show.
|
|
17
|
+
* @returns {number} Its projected width.
|
|
18
|
+
*/
|
|
19
|
+
export const turnWidth = (W, tau, wMin) => Math.max(wMin, W * Math.sqrt(Math.max(0, 1 - tau * tau)));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A row of turning slabs, side by side with the gap g between every two, the row centred on x = cx. Each slab may carry
|
|
23
|
+
* its own full width W, height H and centre shift dy (a bowed row, or slabs shrinking along it); `align` stands every
|
|
24
|
+
* slab on one floor line ('base') or hangs it from one ('top').
|
|
25
|
+
*
|
|
26
|
+
* @param {readonly { tau: number, W?: number, H?: number, dy?: number }[]} slabs Each slab's turn, full width, full
|
|
27
|
+
* height and centre shift.
|
|
28
|
+
* @param {{ W: number, H: number, k: number, g: number, wMin: number, cx?: number, cy?: number, align?: 'centre' | 'base' | 'top' }} o
|
|
29
|
+
* The slabs' full width and height, the perspective k, the gap g, the narrowest slab wMin, the row's centre
|
|
30
|
+
* (cx, cy; 256, 256) and how the slabs line up (centre).
|
|
31
|
+
* @returns {[number, number, number][][]} Each slab as its two edges [[x, top, bottom], [x, top, bottom]], left then right.
|
|
32
|
+
*/
|
|
33
|
+
export function slatRow(slabs, { W, H, k, g, wMin, cx = 256, cy = 256, align = 'centre' }) {
|
|
34
|
+
const ws = slabs.map((s) => turnWidth(s.W ?? W, s.tau, wMin)), total = ws.reduce((a, b) => a + b, 0) + g * (slabs.length - 1);
|
|
35
|
+
let x = cx - total / 2;
|
|
36
|
+
return slabs.map((s, i) => {
|
|
37
|
+
const h = s.H ?? H, c = cy + (s.dy ?? 0), hl = (h / 2) * (1 + k * s.tau), hr = (h / 2) * (1 - k * s.tau);
|
|
38
|
+
const edge = (xx, hh) => (align === 'base' ? [xx, c + h / 2 - 2 * hh, c + h / 2] : align === 'top' ? [xx, c - h / 2, c - h / 2 + 2 * hh] : [xx, c - hh, c + hh]);
|
|
39
|
+
const out = [edge(x, hl), edge(x + ws[i], hr)]; x += ws[i] + g; return out;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A slat row's slabs as one shape.
|
|
45
|
+
*
|
|
46
|
+
* @param {readonly (readonly (readonly number[])[])[]} row The row (slatRow()).
|
|
47
|
+
* @returns {import('../geometry/types.js').Shape} The slabs.
|
|
48
|
+
*/
|
|
49
|
+
export const quads = (row) => U(...row.map(([[x0, t0, b0], [x1, t1, b1]]) => poly([[x0, t0], [x1, t1], [x1, b1], [x0, b0]])));
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A slat row stood on its side as one shape: every point (x, y) becomes (y, x), so upright slabs turning on a vertical
|
|
53
|
+
* axis become flat ones turning on a horizontal axis.
|
|
54
|
+
*
|
|
55
|
+
* @param {readonly (readonly (readonly number[])[])[]} row The row (slatRow()).
|
|
56
|
+
* @returns {import('../geometry/types.js').Shape} The slabs.
|
|
57
|
+
*/
|
|
58
|
+
export const quadsT = (row) => U(...row.map(([[x0, t0, b0], [x1, t1, b1]]) => poly([[t0, x0], [t1, x1], [b1, x1], [b0, x0]])));
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* n turns easing from a to b (a sine ease, so the turn speeds up through the middle).
|
|
62
|
+
*
|
|
63
|
+
* @param {number} n How many.
|
|
64
|
+
* @param {number} a The first.
|
|
65
|
+
* @param {number} b The last.
|
|
66
|
+
* @returns {number[]} The turns.
|
|
67
|
+
*/
|
|
68
|
+
export const ease = (n, a, b) => [...Array(n)].map((_, i) => a + ((b - a) * (1 - Math.cos((Math.PI * i) / (n - 1)))) / 2);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* n turns spaced evenly from a to b.
|
|
72
|
+
*
|
|
73
|
+
* @param {number} n How many.
|
|
74
|
+
* @param {number} a The first.
|
|
75
|
+
* @param {number} b The last.
|
|
76
|
+
* @returns {number[]} The turns.
|
|
77
|
+
*/
|
|
78
|
+
export const lerpN = (n, a, b) => [...Array(n)].map((_, i) => a + ((b - a) * i) / (n - 1));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Point, Shape, Geom } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** A swash's width and balls: swell()'s w0, w1, peak and pow, the balls' radii r0 and r1, or a width profile. */
|
|
4
|
+
export interface SwashOptions {
|
|
5
|
+
w0?: number;
|
|
6
|
+
w1?: number;
|
|
7
|
+
peak?: number;
|
|
8
|
+
pow?: number;
|
|
9
|
+
r0?: number;
|
|
10
|
+
r1?: number;
|
|
11
|
+
width?: (t: number) => number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The swash width profile: w0 at both ends swelling to w1 at `peak`. */
|
|
15
|
+
export function swell(o?: { w0?: number; w1?: number; peak?: number; pow?: number }): (t: number) => number;
|
|
16
|
+
/** A swash along a polyline, ball terminals r0 and r1. */
|
|
17
|
+
export function swashAlong(p: readonly Point[], o?: SwashOptions): Shape;
|
|
18
|
+
/** A swash along a turtle path (walk()). */
|
|
19
|
+
export function swash(at: Point, deg: number, steps: readonly (['L', number] | ['A', number, number])[], o?: SwashOptions): Shape;
|
|
20
|
+
/** A shape mirrored top to bottom across the canvas centre. */
|
|
21
|
+
export function flipY(g: Geom): Geom;
|
|
22
|
+
/** A quarter set into the top right and mirrored into four, each axis parted by g or joined. */
|
|
23
|
+
export function swashQuad(q: Geom, g: number, o?: { x?: 'gap' | 'join'; y?: 'gap' | 'join'; join?: number }): Shape;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swashes: a calligraphic stroke along a turtle path or any polyline, thick in its bowl and thin at both ends, finished
|
|
3
|
+
* with ball terminals (walked as a turtle path, its curls turn smoothly); and the mirror layout that sets a quarter
|
|
4
|
+
* into four, each axis either joined or parted by exactly the gap. Drawn about the origin, y down.
|
|
5
|
+
* From the Logomarks tools (swashparts.mjs, batch 256).
|
|
6
|
+
*/
|
|
7
|
+
import { C, U, circle, walk, mapPts, flipX, move, bbox } from '../geometry/index.js';
|
|
8
|
+
import { dense, taper } from '../parts/index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The swash's width profile: w0 at both ends swelling to w1 at `peak` (0 to 1 along it); `pow` sharpens the swell.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ w0?: number, w1?: number, peak?: number, pow?: number }} [o] The end width (20), the widest (60), where it
|
|
14
|
+
* is widest (0.55) and the swell's power (1.4).
|
|
15
|
+
* @returns {(t: number) => number} The width at t (0 to 1 along the swash).
|
|
16
|
+
*/
|
|
17
|
+
export const swell = ({ w0 = 20, w1 = 60, peak = 0.55, pow = 1.4 } = {}) => {
|
|
18
|
+
const e = Math.log(0.5) / Math.log(peak);
|
|
19
|
+
return (t) => w0 + (w1 - w0) * Math.sin(Math.PI * t ** e) ** pow;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A swash along any polyline (dense, open): its width `width(t)` (t from 0 to 1 along it; swell(o) unless given), a ball
|
|
24
|
+
* of radius r0 on its start and r1 on its end (0: none).
|
|
25
|
+
*
|
|
26
|
+
* @param {readonly import('../geometry/types.js').Point[]} p The polyline.
|
|
27
|
+
* @param {{ w0?: number, w1?: number, peak?: number, r0?: number, r1?: number, pow?: number, width?: (t: number) => number }} [o]
|
|
28
|
+
* swell()'s options, the balls' radii (26, 28) and a width profile in its place.
|
|
29
|
+
* @returns {import('../geometry/types.js').Shape} The swash.
|
|
30
|
+
*/
|
|
31
|
+
export const swashAlong = (p, o = {}) => {
|
|
32
|
+
const { r0 = 26, r1 = 28, width = swell(o) } = o;
|
|
33
|
+
const parts = [taper(p, width)];
|
|
34
|
+
if (r0) parts.push(circle(...p[0], r0)); if (r1) parts.push(circle(...p.at(-1), r1));
|
|
35
|
+
return U(...parts);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A swash along a turtle path (the kit's walk(): from `at`, heading `deg`, through exact-radius turns), w0 wide at both
|
|
40
|
+
* ends swelling to w1 at `peak`, a ball of radius r0 on its start and r1 on its end (0: none); `width(t)` replaces the
|
|
41
|
+
* swell.
|
|
42
|
+
*
|
|
43
|
+
* @param {import('../geometry/types.js').Point} at The start.
|
|
44
|
+
* @param {number} deg The heading at the start (degrees, y down).
|
|
45
|
+
* @param {readonly (['L', number] | ['A', number, number])[]} steps The walk: runs and turns.
|
|
46
|
+
* @param {{ w0?: number, w1?: number, peak?: number, r0?: number, r1?: number, pow?: number, width?: (t: number) => number }} [o]
|
|
47
|
+
* swashAlong()'s options.
|
|
48
|
+
* @returns {import('../geometry/types.js').Shape} The swash.
|
|
49
|
+
*/
|
|
50
|
+
export const swash = (at, deg, steps, o = {}) => swashAlong(dense(walk(at, deg, steps), 2, false), o);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A shape mirrored top to bottom across the canvas centre.
|
|
54
|
+
*
|
|
55
|
+
* @param {import('../geometry/types.js').Geom} g The shape.
|
|
56
|
+
* @returns {import('../geometry/types.js').Geom} Its mirror.
|
|
57
|
+
*/
|
|
58
|
+
export const flipY = (g) => mapPts(g, ([x, y]) => [x, 2 * C - y]);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A quarter (drawn anywhere) set into the top right of the canvas and mirrored into four. Each axis is either parted by
|
|
62
|
+
* exactly g (the copies' nearest points are their points nearest the axis, so the gap is twice that distance) or joined
|
|
63
|
+
* (the copies overlap by `join`).
|
|
64
|
+
*
|
|
65
|
+
* @param {import('../geometry/types.js').Geom} q The quarter.
|
|
66
|
+
* @param {number} g The gap.
|
|
67
|
+
* @param {{ x?: 'gap' | 'join', y?: 'gap' | 'join', join?: number }} [o] What each axis does (gap) and the overlap (6).
|
|
68
|
+
* @returns {import('../geometry/types.js').Shape} The four quarters.
|
|
69
|
+
*/
|
|
70
|
+
export const swashQuad = (q, g, { x = 'gap', y = 'gap', join = 6 } = {}) => {
|
|
71
|
+
const [x0, , , y1] = bbox(U(q));
|
|
72
|
+
const m = move(q, (x === 'gap' ? C + g / 2 : C - join) - x0, (y === 'gap' ? C - g / 2 : C + join) - y1);
|
|
73
|
+
const half = U(m, flipX(m)); return U(half, flipY(half));
|
|
74
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Geom, Point, Polygon, Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
export { box } from '../util/vec.js';
|
|
4
|
+
|
|
5
|
+
/** A parallelogram: bottom edge from (x, yb) w long, top edge h higher shifted dx. */
|
|
6
|
+
export function para(x: number, yb: number, w: number, h: number, dx: number): Polygon;
|
|
7
|
+
/** Points of the circle through a, m and b, from a through m to b. */
|
|
8
|
+
export function arc3(a: readonly number[], m: readonly number[], b: readonly number[], n?: number): Point[];
|
|
9
|
+
/** A concave star of n tips at radius R, its sides pulled towards radius rc. */
|
|
10
|
+
export function star(n: number, R: number, rc?: number, deg?: number, tips?: readonly number[]): Polygon;
|
|
11
|
+
/** A gap g wide across a star's arm at deg, d from the centre. */
|
|
12
|
+
export function armCut(deg: number, d: number, g: number): Polygon;
|
|
13
|
+
/** A folded ribbon B wide along pts, drawn as outlines t wide. */
|
|
14
|
+
export function ribbon(pts: readonly Point[], B: number, t: number): Shape;
|
|
15
|
+
/** Equal discs on a lattice of pitch p, the gap g between neighbours. */
|
|
16
|
+
export function beads(cells: readonly (readonly number[])[], g: number, p?: number): Shape;
|
|
17
|
+
/** √3 / 2. */
|
|
18
|
+
export const H3: number;
|
|
19
|
+
/** A bar w wide leaning `lean`° through (x, ym), cut flat at y0 and y1. */
|
|
20
|
+
export function slash(x: number, ym: number, w: number, lean: number, y0: number, y1: number): Polygon;
|
|
21
|
+
/** The lens marks' disc (radius 224, centred). */
|
|
22
|
+
export function DISC(): Polygon;
|
|
23
|
+
/** A lens gap from p to q, w wide at its middle. */
|
|
24
|
+
export function lensGap(p: readonly number[], q: readonly number[], w: number): Polygon;
|
|
25
|
+
/** A chevron: apex P, arms along A and B, w wide inward. */
|
|
26
|
+
export function techChevron(P: readonly number[], A: readonly number[], B: readonly number[], w: number, L?: number): Polygon;
|
|
27
|
+
/** A slab from a to b, its b end cut to a 90° point (a Keyed letter's key). */
|
|
28
|
+
export function pointed(a: readonly number[], b: readonly number[], w: number): Polygon;
|
|
29
|
+
/** The notched slab less the key grown by g, plus the key. */
|
|
30
|
+
export function keyed(notched: Geom, key: Geom, g: number): Shape;
|
|
31
|
+
/** The Kerf tile: a 448 square, corners rounded 78. */
|
|
32
|
+
export function TILE(): Polygon;
|
|
33
|
+
/** The folded ribbon letters' width (124). */
|
|
34
|
+
export const RB: number;
|
|
35
|
+
/** Their outline width once fitted (24). */
|
|
36
|
+
export const RT: number;
|
|
37
|
+
/** A folded ribbon letter along pts, as a build of the gap. */
|
|
38
|
+
export function rib(pts: readonly Point[]): (g: number) => Shape;
|
|
39
|
+
/** The four-point star the sparkles break. */
|
|
40
|
+
export function SPARK(): Polygon;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bold tech parts: split sparkles, the folded ribbon (Folio), beads on a lattice, slashes, lens gaps, keyed chevrons and
|
|
3
|
+
* the Kerf tile. Angles are degrees, 0 right, 90 down. From the Logomarks tools (techparts.mjs, batch 213, "Bold · Tech",
|
|
4
|
+
* and its editions in batches 220 and 227).
|
|
5
|
+
*/
|
|
6
|
+
import { C, D, I, U, bar, circle, grow, poly, rad, rr, sample } from '../geometry/index.js';
|
|
7
|
+
import { sub, add, mul } from '../util/vec.js';
|
|
8
|
+
|
|
9
|
+
// A rectangle from its corners, box(x0, y0, x1, y1): the tech marks draw their bars with it.
|
|
10
|
+
export { box } from '../util/vec.js';
|
|
11
|
+
|
|
12
|
+
/** @typedef {import('../geometry/types.js').Point} Point */
|
|
13
|
+
/** @typedef {import('../geometry/types.js').Polygon} Polygon */
|
|
14
|
+
/** @typedef {import('../geometry/types.js').Shape} Shape */
|
|
15
|
+
/** @typedef {import('../geometry/types.js').Geom} Geom */
|
|
16
|
+
|
|
17
|
+
/** Far enough to run a half plane off any canvas. */
|
|
18
|
+
const BIG = 3000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A 2D vector at unit length.
|
|
22
|
+
*
|
|
23
|
+
* @param {readonly number[]} a The vector.
|
|
24
|
+
* @returns {Point} It, unit.
|
|
25
|
+
*/
|
|
26
|
+
const norm = (a) => { const l = Math.hypot(...a); return [a[0] / l, a[1] / l]; };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A 2D vector turned a quarter (to its left on screen, y down).
|
|
30
|
+
*
|
|
31
|
+
* @param {readonly number[]} a The vector.
|
|
32
|
+
* @returns {Point} It, turned.
|
|
33
|
+
*/
|
|
34
|
+
const perp = (a) => [-a[1], a[0]];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The dot product in 2D.
|
|
38
|
+
*
|
|
39
|
+
* @param {readonly number[]} a One vector.
|
|
40
|
+
* @param {readonly number[]} b The other.
|
|
41
|
+
* @returns {number} a · b.
|
|
42
|
+
*/
|
|
43
|
+
const dot2 = (a, b) => a[0] * b[0] + a[1] * b[1];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The cross product in 2D (its z).
|
|
47
|
+
*
|
|
48
|
+
* @param {readonly number[]} a One vector.
|
|
49
|
+
* @param {readonly number[]} b The other.
|
|
50
|
+
* @returns {number} a × b.
|
|
51
|
+
*/
|
|
52
|
+
const cross2 = (a, b) => a[0] * b[1] - a[1] * b[0];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The half plane left of the directed line through p along d (left: the side `perp(d)` points to).
|
|
56
|
+
*
|
|
57
|
+
* @param {readonly number[]} p A point on the line.
|
|
58
|
+
* @param {readonly number[]} d Its direction.
|
|
59
|
+
* @returns {Polygon} The half plane.
|
|
60
|
+
*/
|
|
61
|
+
const half = (p, d) => {
|
|
62
|
+
const u = norm(d), n = perp(u);
|
|
63
|
+
return poly([add(p, mul(u, -BIG)), add(p, mul(u, BIG)), add(add(p, mul(u, BIG)), mul(n, BIG)), add(add(p, mul(u, -BIG)), mul(n, BIG))]);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The half plane on the side of the line (p, d) that holds the point q.
|
|
68
|
+
*
|
|
69
|
+
* @param {readonly number[]} p A point on the line.
|
|
70
|
+
* @param {readonly number[]} d Its direction.
|
|
71
|
+
* @param {readonly number[]} q The point to keep.
|
|
72
|
+
* @returns {Polygon} The half plane.
|
|
73
|
+
*/
|
|
74
|
+
const halfWith = (p, d, q) => (cross2(norm(d), sub(q, p)) * cross2(norm(d), perp(norm(d))) > 0 ? half(p, d) : half(p, mul(d, -1)));
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A parallelogram: its bottom edge from (x, yb) w long, its top edge h higher and shifted dx right.
|
|
78
|
+
*
|
|
79
|
+
* @param {number} x The bottom edge's left end.
|
|
80
|
+
* @param {number} yb The bottom edge's height.
|
|
81
|
+
* @param {number} w The width.
|
|
82
|
+
* @param {number} h The height.
|
|
83
|
+
* @param {number} dx The top edge's shift.
|
|
84
|
+
* @returns {Polygon} The parallelogram.
|
|
85
|
+
*/
|
|
86
|
+
export const para = (x, yb, w, h, dx) => poly([[x, yb], [x + w, yb], [x + w + dx, yb - h], [x + dx, yb - h]]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Points of the circle through a, m and b, from a through m to b.
|
|
90
|
+
*
|
|
91
|
+
* @param {readonly number[]} a The start.
|
|
92
|
+
* @param {readonly number[]} m A point on the way.
|
|
93
|
+
* @param {readonly number[]} b The end.
|
|
94
|
+
* @param {number} [n=60] Steps.
|
|
95
|
+
* @returns {Point[]} The arc.
|
|
96
|
+
*/
|
|
97
|
+
export const arc3 = (a, m, b, n = 60) => {
|
|
98
|
+
const ax = a[0], ay = a[1], bx = m[0], by = m[1], cx = b[0], cy = b[1];
|
|
99
|
+
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
|
100
|
+
const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
|
|
101
|
+
const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
|
|
102
|
+
const r = Math.hypot(ax - ux, ay - uy), t = (p) => Math.atan2(p[1] - uy, p[0] - ux); let t0 = t(a), t1 = t(m), t2 = t(b);
|
|
103
|
+
const ccw = (x, y) => ((y - x) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI); // sweep from x to y anticlockwise (in y-down terms, increasing angle)
|
|
104
|
+
const s = ccw(t0, t1) < ccw(t0, t2) ? ccw(t0, t2) : ccw(t0, t2) - 2 * Math.PI;
|
|
105
|
+
return sample((u) => [ux + r * Math.cos(t0 + s * u), uy + r * Math.sin(t0 + s * u)], 0, 1, n);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The point at radius r and angle deg from a centre.
|
|
110
|
+
*
|
|
111
|
+
* @param {number} r The radius.
|
|
112
|
+
* @param {number} deg The angle.
|
|
113
|
+
* @param {readonly number[]} [c=[C, C]] The centre.
|
|
114
|
+
* @returns {Point} The point.
|
|
115
|
+
*/
|
|
116
|
+
const at = (r, deg, c = [C, C]) => [c[0] + r * Math.cos(rad(deg)), c[1] + r * Math.sin(rad(deg))];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A concave star about the canvas centre: n tips at radius R (`tips` scales single tips), each side a quadratic curve
|
|
120
|
+
* pulled in towards a control point at radius rc on the mid angle (rc 0 pulls every side to the centre), so the sides meet
|
|
121
|
+
* the tips without crossing.
|
|
122
|
+
*
|
|
123
|
+
* @param {number} n The tips.
|
|
124
|
+
* @param {number} R Their radius.
|
|
125
|
+
* @param {number} [rc=0] The sides' control radius.
|
|
126
|
+
* @param {number} [deg=-90] The first tip's angle.
|
|
127
|
+
* @param {readonly number[]} [tips=[]] Per-tip radius factors.
|
|
128
|
+
* @returns {Polygon} The star.
|
|
129
|
+
*/
|
|
130
|
+
export const star = (n, R, rc = 0, deg = -90, tips = []) => {
|
|
131
|
+
const pts = [];
|
|
132
|
+
for (let i = 0; i < n; i++) {
|
|
133
|
+
const a = deg + (i * 360) / n, b = a + 360 / n, p0 = at(R * (tips[i] ?? 1), a), p1 = at(rc, a + 180 / n), p2 = at(R * (tips[(i + 1) % n] ?? 1), b);
|
|
134
|
+
pts.push(...sample((t) => [(1 - t) ** 2 * p0[0] + 2 * t * (1 - t) * p1[0] + t * t * p2[0], (1 - t) ** 2 * p0[1] + 2 * t * (1 - t) * p1[1] + t * t * p2[1]], 0, 1, 64).slice(0, -1));
|
|
135
|
+
}
|
|
136
|
+
return poly(pts);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A gap g wide across a star's arm at angle deg, d from the centre.
|
|
141
|
+
*
|
|
142
|
+
* @param {number} deg The arm's angle.
|
|
143
|
+
* @param {number} d The distance from the centre.
|
|
144
|
+
* @param {number} g The gap.
|
|
145
|
+
* @returns {Polygon} The cut.
|
|
146
|
+
*/
|
|
147
|
+
export const armCut = (deg, d, g) => {
|
|
148
|
+
const p = at(d, deg), u = [Math.cos(rad(deg)), Math.sin(rad(deg))], n = perp(u);
|
|
149
|
+
return bar(add(p, mul(n, -400)), add(p, mul(n, 400)), g);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A ribbon B wide along the polyline pts, drawn as outlines t wide. Each turn is a fold: the crease runs through the
|
|
154
|
+
* corner along the bisector of the two directions (so the outer corner is cut on it), and each later facet lies over the
|
|
155
|
+
* one before, hiding its lines. Ends are cut square. Every facet's outline is drawn t/2 inside it, and the silhouette's
|
|
156
|
+
* another t/2, so shared folds and the outer edge are all t wide.
|
|
157
|
+
*
|
|
158
|
+
* @param {readonly Point[]} pts The ribbon's centreline.
|
|
159
|
+
* @param {number} B Its width.
|
|
160
|
+
* @param {number} t The outline's width.
|
|
161
|
+
* @returns {Shape} The ribbon's lines.
|
|
162
|
+
*/
|
|
163
|
+
export const ribbon = (pts, B, t) => {
|
|
164
|
+
const n = pts.length - 1, v = [...Array(n)].map((_, i) => norm(sub(pts[i + 1], pts[i])));
|
|
165
|
+
const crease = (k) => (k === 0 ? perp(v[0]) : k === n ? perp(v[n - 1]) : (Math.hypot(...add(v[k - 1], v[k])) < 1e-6 ? perp(v[k]) : add(v[k - 1], v[k])));
|
|
166
|
+
const facets = v.map((u, i) => {
|
|
167
|
+
const a = pts[i], b = pts[i + 1], nn = perp(u);
|
|
168
|
+
const strip = poly([add(add(a, mul(u, -BIG)), mul(nn, -B / 2)), add(add(a, mul(u, BIG)), mul(nn, -B / 2)), add(add(a, mul(u, BIG)), mul(nn, B / 2)), add(add(a, mul(u, -BIG)), mul(nn, B / 2))]);
|
|
169
|
+
return I(strip, halfWith(a, crease(i), b), halfWith(b, crease(i + 1), a));
|
|
170
|
+
});
|
|
171
|
+
const vis = facets.map((f, i) => (i < n - 1 ? D(f, ...facets.slice(i + 1)) : f)); // what each facet shows under the later ones
|
|
172
|
+
const all = U(...facets), ring = (s, w) => D(s, grow(s, -w)); // a line w wide just inside the outline (sharp corners kept)
|
|
173
|
+
return U(ring(all, t / 2), ...vis.map((s) => ring(s, t / 2)));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Equal discs on a lattice of pitch p, the gap g between neighbours: cells as [col, row] in pitch units.
|
|
178
|
+
*
|
|
179
|
+
* @param {readonly (readonly number[])[]} cells The lattice points.
|
|
180
|
+
* @param {number} g The gap.
|
|
181
|
+
* @param {number} [p=130] The pitch.
|
|
182
|
+
* @returns {Shape} The beads.
|
|
183
|
+
*/
|
|
184
|
+
export const beads = (cells, g, p = 130) => U(...cells.map(([x, y]) => circle(x * p, y * p, (p - g) / 2, 160)));
|
|
185
|
+
|
|
186
|
+
/** √3 / 2: a hexagonal lattice's row pitch, in pitch units. */
|
|
187
|
+
export const H3 = Math.sqrt(3) / 2;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A bar w wide leaning `lean`° right of upright, its centreline through (x, ym), cut flat at y0 and y1.
|
|
191
|
+
*
|
|
192
|
+
* @param {number} x The centreline's x at ym.
|
|
193
|
+
* @param {number} ym The height it passes x at.
|
|
194
|
+
* @param {number} w The width (square to the bar).
|
|
195
|
+
* @param {number} lean The lean, degrees.
|
|
196
|
+
* @param {number} y0 The top cut.
|
|
197
|
+
* @param {number} y1 The bottom cut.
|
|
198
|
+
* @returns {Polygon} The slash.
|
|
199
|
+
*/
|
|
200
|
+
export const slash = (x, ym, w, lean, y0, y1) => {
|
|
201
|
+
const t = Math.tan(rad(lean)), xa = (y) => x + (ym - y) * t, hw = w / 2 / Math.cos(rad(lean));
|
|
202
|
+
return poly([[xa(y1) - hw, y1], [xa(y1) + hw, y1], [xa(y0) + hw, y0], [xa(y0) - hw, y0]]);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The lens marks' disc: radius 224, centred, 720 points.
|
|
207
|
+
*
|
|
208
|
+
* @returns {Polygon} The disc.
|
|
209
|
+
*/
|
|
210
|
+
export const DISC = () => circle(C, C, 224, 720);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A lens gap from p to q: two arcs bowing w apart at the middle and meeting at p and q (run it on past the rim).
|
|
214
|
+
*
|
|
215
|
+
* @param {readonly number[]} p One end.
|
|
216
|
+
* @param {readonly number[]} q The other.
|
|
217
|
+
* @param {number} w Its width at the middle.
|
|
218
|
+
* @returns {Polygon} The gap.
|
|
219
|
+
*/
|
|
220
|
+
export const lensGap = (p, q, w) => {
|
|
221
|
+
const m = mul(add(p, q), 0.5), n = perp(norm(sub(q, p)));
|
|
222
|
+
return poly([...arc3(p, add(m, mul(n, w / 2)), q, 60), ...arc3(q, add(m, mul(n, -w / 2)), p, 60).slice(1, -1)]);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A chevron: outer apex P, arms running out along A and B (directions from P), w wide inward; long, so a box clips the
|
|
227
|
+
* ends.
|
|
228
|
+
*
|
|
229
|
+
* @param {readonly number[]} P The apex.
|
|
230
|
+
* @param {readonly number[]} A One arm's direction.
|
|
231
|
+
* @param {readonly number[]} B The other's.
|
|
232
|
+
* @param {number} w The width.
|
|
233
|
+
* @param {number} [L=1400] The arms' length.
|
|
234
|
+
* @returns {Polygon} The chevron.
|
|
235
|
+
*/
|
|
236
|
+
export const techChevron = (P, A, B, w, L = 1400) => {
|
|
237
|
+
const u1 = norm(A), u2 = norm(B), bis = norm(add(u1, u2)), th = Math.acos(dot2(u1, u2));
|
|
238
|
+
const side = (u, o) => { const n = perp(u); return dot2(n, o) > 0 ? n : mul(n, -1); }, n1 = side(u1, u2), n2 = side(u2, u1);
|
|
239
|
+
const Q = add(P, mul(bis, w / Math.sin(th / 2)));
|
|
240
|
+
return poly([P, add(P, mul(u1, L)), add(add(P, mul(u1, L)), mul(n1, w)), Q, add(add(P, mul(u2, L)), mul(n2, w)), add(P, mul(u2, L))]);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* A slab w wide from a to b whose b end is cut to a 90° point (two 45° chamfers): the key of a Keyed letter.
|
|
245
|
+
*
|
|
246
|
+
* @param {readonly number[]} a The square end.
|
|
247
|
+
* @param {readonly number[]} b The point.
|
|
248
|
+
* @param {number} w The width.
|
|
249
|
+
* @returns {Polygon} The slab.
|
|
250
|
+
*/
|
|
251
|
+
export const pointed = (a, b, w) => {
|
|
252
|
+
const u = norm(sub(b, a)), n = perp(u), h = w / 2, back = add(b, mul(u, -h));
|
|
253
|
+
return poly([add(a, mul(n, h)), add(back, mul(n, h)), b, add(back, mul(n, -h)), add(a, mul(n, -h))]);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Two Keyed slabs parted: the notched slab loses the key grown by g, so the gap is even round the point.
|
|
258
|
+
*
|
|
259
|
+
* @param {Geom} notched The slab the key goes into.
|
|
260
|
+
* @param {Geom} key The key.
|
|
261
|
+
* @param {number} g The gap.
|
|
262
|
+
* @returns {Shape} Both.
|
|
263
|
+
*/
|
|
264
|
+
export const keyed = (notched, key, g) => U(D(notched, grow(key, g)), key);
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The Kerf tile: a 448 square from the origin, its corners rounded 78 (about 17%: a rounded square, never a squircle).
|
|
268
|
+
*
|
|
269
|
+
* @returns {Polygon} The tile.
|
|
270
|
+
*/
|
|
271
|
+
export const TILE = () => rr(0, 0, 448, 448, 78);
|
|
272
|
+
|
|
273
|
+
/** The folded ribbon letters' width. */
|
|
274
|
+
export const RB = 124;
|
|
275
|
+
|
|
276
|
+
/** The folded ribbon letters' outline width once fitted. */
|
|
277
|
+
export const RT = 24;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* A folded ribbon letter along pts, RB wide, its outline RT wide once fitted.
|
|
281
|
+
*
|
|
282
|
+
* @param {readonly Point[]} pts The centreline.
|
|
283
|
+
* @returns {(g: number) => Shape} The build of the gap.
|
|
284
|
+
*/
|
|
285
|
+
export const rib = (pts) => (g) => ribbon(pts, RB, (g * RT) / 22);
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The four-point star both sparkles break: radius 224, its sides pulled to a control point 40 out, so the arms stay
|
|
289
|
+
* chunky.
|
|
290
|
+
*
|
|
291
|
+
* @returns {Polygon} The star.
|
|
292
|
+
*/
|
|
293
|
+
export const SPARK = () => star(4, 224, 40);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Point, Ring, Polygon, Shape, Geom } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** The source-unit helpers trackKit(K) returns. */
|
|
4
|
+
export interface TrackKit {
|
|
5
|
+
/** A solid built in source units, drawn K larger. */
|
|
6
|
+
S(g: Geom): Geom;
|
|
7
|
+
/** A loop's centreline: a region's boundary, corners rounded r1 (inner) and r2 (outer), resampled evenly. */
|
|
8
|
+
ring(region: Geom, r1?: number, r2?: number): Point[];
|
|
9
|
+
/** A solid cut a house gap clear of tracks. */
|
|
10
|
+
off(sol: Geom, ...tracks: Geom[]): Shape;
|
|
11
|
+
/** A solid kept only outside a loop's centreline, a gap clear of its outer track. */
|
|
12
|
+
out(sol: Geom, o: Point[]): Shape;
|
|
13
|
+
/** A solid kept only inside a loop's centreline, a gap clear of the inner track. */
|
|
14
|
+
within(sol: Geom, o: Point[]): Shape;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Source units to drawing units, for the fixed-K editions (1.6). */
|
|
18
|
+
export const TRACK_K: number;
|
|
19
|
+
/** A closed ring resampled every `step` along its length. */
|
|
20
|
+
export function evenRing(o: readonly Point[], step?: number): Point[];
|
|
21
|
+
/** The source-unit helpers at a fixed K. */
|
|
22
|
+
export function trackKit(K?: number): TrackKit;
|
|
23
|
+
/** A closed region from a radius function round (cx, cy), every 2 degrees. */
|
|
24
|
+
export function radial(cx: number, cy: number, f: (a: number) => number): Polygon;
|
|
25
|
+
/** A drop pointing up: a disc of radius r at (x, y), its point h above the centre. */
|
|
26
|
+
export function drop(x: number, y: number, r: number, h: number): Shape;
|
|
27
|
+
/** A shape's area: outer rings less their holes. */
|
|
28
|
+
export function sArea(s: Shape): number;
|
|
29
|
+
/** Whether a point lies inside a ring (even-odd). */
|
|
30
|
+
export function inRing(r: readonly Point[], p: readonly number[]): boolean;
|
|
31
|
+
/** A ring's box centre. */
|
|
32
|
+
export function mid(r: Ring): Point;
|
|
33
|
+
/** The largest outer ring of a shape, its closing point dropped. */
|
|
34
|
+
export function largest(s: Shape): Point[];
|
|
35
|
+
/** A cut shape's parts, less any piece under `min`. */
|
|
36
|
+
export function parts2(s: Shape, min?: number): Shape;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track editions: a mark redrawn as two-track loops (the kit's trackLoop with the house Duo, the gap between the tracks)
|
|
3
|
+
* round its solid parts. A source is drawn in source units and scaled K into drawing units; trackKit(K) gives the
|
|
4
|
+
* source-unit helpers at a fixed K, and the rest split a live source into its parts.
|
|
5
|
+
* From the Logomarks tools (trackparts.mjs, the helpers every Track edition generator had copied; shared since Drop 35).
|
|
6
|
+
*/
|
|
7
|
+
import { U, D, I, poly, circle, grow, scale, soften, area, bbox } from '../geometry/index.js';
|
|
8
|
+
import { inside } from '../objects/animals.js';
|
|
9
|
+
|
|
10
|
+
/** Source units to drawing units, for the editions drawn at a fixed K. */
|
|
11
|
+
export const TRACK_K = 1.6;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A closed ring resampled every `step` along its length (soften()'s rings crowd points at the arcs, and two points a
|
|
15
|
+
* hair apart turn a track's offset normals: the loop frays).
|
|
16
|
+
*
|
|
17
|
+
* @param {readonly import('../geometry/types.js').Point[]} o The ring (open: its first point not repeated).
|
|
18
|
+
* @param {number} [step=6] The spacing.
|
|
19
|
+
* @returns {import('../geometry/types.js').Point[]} The resampled ring.
|
|
20
|
+
*/
|
|
21
|
+
export function evenRing(o, step = 6) {
|
|
22
|
+
const n = o.length, seg = o.map((p, i) => Math.hypot(o[(i + 1) % n][0] - p[0], o[(i + 1) % n][1] - p[1])), L = seg.reduce((a, b) => a + b, 0), m = Math.round(L / step), out = [];
|
|
23
|
+
let i = 0, acc = 0;
|
|
24
|
+
for (let k = 0; k < m; k++) { const d = (k * L) / m; while (acc + seg[i] < d) acc += seg[i++]; const t = seg[i] ? (d - acc) / seg[i] : 0, a = o[i], b = o[(i + 1) % n]; out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]); }
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The source-unit helpers at a fixed K:
|
|
30
|
+
*
|
|
31
|
+
* - `S(g)`: a solid built in source units, drawn K larger;
|
|
32
|
+
* - `ring(region, r1, r2)`: a loop's centreline, the boundary of a region drawn in source units, its inner corners
|
|
33
|
+
* rounded r1 and outer r2 (drawing units, 60 or more): the largest ring, its closing point dropped, resampled evenly;
|
|
34
|
+
* - `off(sol, ...tracks)`: a solid (source units) cut a house gap clear of tracks (drawing units);
|
|
35
|
+
* - `out(sol, o)`: a solid (source units) kept only outside a loop's centreline o, a gap clear of its outer track;
|
|
36
|
+
* - `within(sol, o)`: a solid (source units) kept only inside a loop's centreline o, a gap clear of the inner track.
|
|
37
|
+
*
|
|
38
|
+
* @param {number} [K=TRACK_K] Source units to drawing units.
|
|
39
|
+
* @returns {{ S: (g: import('../geometry/types.js').Geom) => import('../geometry/types.js').Geom, ring: (region: import('../geometry/types.js').Geom, r1?: number, r2?: number) => import('../geometry/types.js').Point[], off: (sol: import('../geometry/types.js').Geom, ...tracks: import('../geometry/types.js').Geom[]) => import('../geometry/types.js').Shape, out: (sol: import('../geometry/types.js').Geom, o: import('../geometry/types.js').Point[]) => import('../geometry/types.js').Shape, within: (sol: import('../geometry/types.js').Geom, o: import('../geometry/types.js').Point[]) => import('../geometry/types.js').Shape }}
|
|
40
|
+
* The helpers.
|
|
41
|
+
*/
|
|
42
|
+
export const trackKit = (K = TRACK_K) => {
|
|
43
|
+
const S = (g) => scale(g, K, 0, 0);
|
|
44
|
+
const ring = (region, r1 = 64, r2 = 64) => {
|
|
45
|
+
const g = soften(S(region), r1, r2), area = (r) => Math.abs(r.reduce((s, p, i) => { const q = r[(i + 1) % r.length]; return s + p[0] * q[1] - q[0] * p[1]; }, 0));
|
|
46
|
+
let best = null; for (const pg of g) if (!best || area(pg[0]) > area(best)) best = pg[0];
|
|
47
|
+
const o = best.slice(); if (Math.hypot(o[0][0] - o.at(-1)[0], o[0][1] - o.at(-1)[1]) < 0.01) o.pop(); return evenRing(o);
|
|
48
|
+
};
|
|
49
|
+
const off = (sol, ...trs) => D(S(sol), grow(U(...trs), 22));
|
|
50
|
+
const out = (sol, o) => D(S(sol), grow(poly(o), 37 + 22));
|
|
51
|
+
const within = (sol, o) => I(S(sol), inside(o));
|
|
52
|
+
return { S, ring, off, out, within };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A closed region from a radius function round (cx, cy), sampled every 2 degrees.
|
|
57
|
+
*
|
|
58
|
+
* @param {number} cx The centre.
|
|
59
|
+
* @param {number} cy The centre.
|
|
60
|
+
* @param {(a: number) => number} f The radius at the angle a (radians).
|
|
61
|
+
* @returns {import('../geometry/types.js').Polygon} The region.
|
|
62
|
+
*/
|
|
63
|
+
export const radial = (cx, cy, f) => poly(Array.from({ length: 180 }, (_, i) => { const a = (i * 2 * Math.PI) / 180, r = f(a); return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }));
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A drop pointing up: a disc of radius r at (x, y) and its point h above the centre.
|
|
67
|
+
*
|
|
68
|
+
* @param {number} x The disc's centre.
|
|
69
|
+
* @param {number} y The disc's centre.
|
|
70
|
+
* @param {number} r Its radius.
|
|
71
|
+
* @param {number} h How far above the centre the point is.
|
|
72
|
+
* @returns {import('../geometry/types.js').Shape} The drop.
|
|
73
|
+
*/
|
|
74
|
+
export const drop = (x, y, r, h) => { const t = Math.asin(r / h); return U(circle(x, y, r), poly([[x, y - h], [x + r * Math.cos(t), y - r * Math.sin(t)], [x - r * Math.cos(t), y - r * Math.sin(t)]])); };
|
|
75
|
+
|
|
76
|
+
/* ---------- splitting a live source ---------- */
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A shape's area: its outer rings less their holes.
|
|
80
|
+
*
|
|
81
|
+
* @param {import('../geometry/types.js').Shape} s The shape.
|
|
82
|
+
* @returns {number} The area.
|
|
83
|
+
*/
|
|
84
|
+
export const sArea = (s) => s.reduce((t, pg) => t + area(pg[0]) - pg.slice(1).reduce((h, r) => h + area(r), 0), 0);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether a point lies inside a ring (even-odd).
|
|
88
|
+
*
|
|
89
|
+
* @param {readonly import('../geometry/types.js').Point[]} r The ring.
|
|
90
|
+
* @param {readonly number[]} p The point.
|
|
91
|
+
* @returns {boolean} True inside.
|
|
92
|
+
*/
|
|
93
|
+
export const inRing = (r, [px, py]) => { let c = false; for (let i = 0, j = r.length - 1; i < r.length; j = i++) { const [a, b] = [r[i], r[j]]; if ((a[1] > py) !== (b[1] > py) && px < ((b[0] - a[0]) * (py - a[1])) / (b[1] - a[1]) + a[0]) c = !c; } return c; };
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A ring's box centre.
|
|
97
|
+
*
|
|
98
|
+
* @param {import('../geometry/types.js').Ring} r The ring.
|
|
99
|
+
* @returns {import('../geometry/types.js').Point} The centre of its box.
|
|
100
|
+
*/
|
|
101
|
+
export const mid = (r) => { const [x0, y0, x1, y1] = bbox([[r]]); return [(x0 + x1) / 2, (y0 + y1) / 2]; };
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The largest outer ring of a shape, its closing point dropped.
|
|
105
|
+
*
|
|
106
|
+
* @param {import('../geometry/types.js').Shape} s The shape.
|
|
107
|
+
* @returns {import('../geometry/types.js').Point[]} The ring.
|
|
108
|
+
*/
|
|
109
|
+
export const largest = (s) => {
|
|
110
|
+
let best = null; for (const pg of s) if (!best || area(pg[0]) > area(best)) best = pg[0];
|
|
111
|
+
const o = best.slice(); if (Math.hypot(o[0][0] - o.at(-1)[0], o[0][1] - o.at(-1)[1]) < 0.01) o.pop(); return o;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A cut shape's parts, less any piece under `min` (drawing units): a limb's stub left between two cuts.
|
|
116
|
+
*
|
|
117
|
+
* @param {import('../geometry/types.js').Shape} s The cut shape.
|
|
118
|
+
* @param {number} [min=1000] The smallest piece kept.
|
|
119
|
+
* @returns {import('../geometry/types.js').Shape} The parts.
|
|
120
|
+
*/
|
|
121
|
+
export const parts2 = (s, min = 1000) => s.filter((pg) => area(pg[0]) >= min);
|