@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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold seams, straps and hand-cut edges, the parts the Crease, Strap and Snip marks share. A seam is half the house gap
|
|
3
|
+
* (11 once fitted, against the 22 gap): a fold or an overlap drawn as a hairline that shows at large sizes and fades at
|
|
4
|
+
* 32 px, where the mark must read as its silhouette alone. Every build takes the seam width g (exact() with a target of
|
|
5
|
+
* 11 keeps it 11 once fitted), so a part that is truly separate stands 2g clear. Laying parts one over another, each
|
|
6
|
+
* cut free by the seam, is the parts' chain().
|
|
7
|
+
* From the Logomarks tools (foldparts.mjs, batch 170).
|
|
8
|
+
*/
|
|
9
|
+
import { U, D, poly, pill, stroke, dedupe } from '../geometry/index.js';
|
|
10
|
+
import { spline } from '../parts/index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A seam along a polyline: g wide with round ends, so seams that meet at a node leave the facets' corners sharp.
|
|
14
|
+
*
|
|
15
|
+
* @param {number} g The seam's width.
|
|
16
|
+
* @param {readonly import('../geometry/types.js').Point[]} pts The seam's line (two points: a straight seam).
|
|
17
|
+
* @returns {import('../geometry/types.js').Geom} The seam.
|
|
18
|
+
*/
|
|
19
|
+
export const seamLine = (g, pts) => (pts.length === 2 ? pill(pts[0], pts[1], g) : stroke(dedupe(pts), g, { roundCaps: true, chunk: 8 }));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A shape split into facets by seams. Run a seam that ends on the outline a little past it, and end inner seams exactly
|
|
23
|
+
* on a node where they meet another.
|
|
24
|
+
*
|
|
25
|
+
* @param {import('../geometry/types.js').Geom} shape The shape.
|
|
26
|
+
* @param {number} g The seam's width.
|
|
27
|
+
* @param {...import('../geometry/types.js').Point[]} seams Each seam's points (two for a straight seam, more for a curve).
|
|
28
|
+
* @returns {import('../geometry/types.js').Shape} The facets.
|
|
29
|
+
*/
|
|
30
|
+
export const crease = (shape, g, ...seams) => D(shape, ...seams.map((s) => seamLine(g, s)));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A smooth seam through control points (Catmull-Rom, the ends held).
|
|
34
|
+
*
|
|
35
|
+
* @param {readonly import('../geometry/types.js').Point[]} pts The control points.
|
|
36
|
+
* @param {number} [n=24] Points a span.
|
|
37
|
+
* @returns {import('../geometry/types.js').Point[]} The seam's line.
|
|
38
|
+
*/
|
|
39
|
+
export const curve = (pts, n = 24) => spline(pts, n);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A quadratic Bézier from a to b pulled towards c, as n + 1 points.
|
|
43
|
+
*
|
|
44
|
+
* @param {readonly number[]} a The start.
|
|
45
|
+
* @param {readonly number[]} c The control point.
|
|
46
|
+
* @param {readonly number[]} b The end.
|
|
47
|
+
* @param {number} [n=48] Steps.
|
|
48
|
+
* @returns {import('../geometry/types.js').Point[]} The curve.
|
|
49
|
+
*/
|
|
50
|
+
export const foldQuad = (a, c, b, n = 48) => [...Array(n + 1)].map((_, i) => {
|
|
51
|
+
const t = i / n, u = 1 - t;
|
|
52
|
+
return [u * u * a[0] + 2 * u * t * c[0] + t * t * b[0], u * u * a[1] + 2 * u * t * c[1] + t * t * b[1]];
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A strap: a pill stroke w wide from a to b (strap letters lay one strap over another with the parts' chain()).
|
|
57
|
+
*
|
|
58
|
+
* @param {import('../geometry/types.js').Point} a One end.
|
|
59
|
+
* @param {import('../geometry/types.js').Point} b The other.
|
|
60
|
+
* @param {number} w The width.
|
|
61
|
+
* @returns {import('../geometry/types.js').Polygon} The strap.
|
|
62
|
+
*/
|
|
63
|
+
export const strap = (a, b, w) => pill(a, b, w);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A flat strip w wide from a to b, each end cut on a horizontal line (y0 at a, y1 at b) if given, else square.
|
|
67
|
+
*
|
|
68
|
+
* @param {readonly number[]} a One end.
|
|
69
|
+
* @param {readonly number[]} b The other.
|
|
70
|
+
* @param {number} w The width.
|
|
71
|
+
* @param {{ y0?: number, y1?: number }} [o] The heights the ends are cut on.
|
|
72
|
+
* @returns {import('../geometry/types.js').Polygon} The strip.
|
|
73
|
+
*/
|
|
74
|
+
export const strip = (a, b, w, { y0, y1 } = {}) => {
|
|
75
|
+
const dx = b[0] - a[0], dy = b[1] - a[1], L = Math.hypot(dx, dy), n = [-dy / L * w / 2, dx / L * w / 2];
|
|
76
|
+
const ends = (p, y, sgn) => {
|
|
77
|
+
if (y == null) return [[p[0] + n[0], p[1] + n[1]], [p[0] - n[0], p[1] - n[1]]];
|
|
78
|
+
// slide each edge along the strip's direction until it meets y
|
|
79
|
+
return [n, [-n[0], -n[1]]].map((o) => { const q = [p[0] + o[0], p[1] + o[1]], t = (y - q[1]) / dy; return [q[0] + dx * t, y]; });
|
|
80
|
+
};
|
|
81
|
+
const [a1, a2] = ends(a, y0), [b1, b2] = ends(b, y1); return poly([a1, b1, b2, a2]);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A hand-cut edge: every ring of a shape resampled every `step` and pushed along its normal by a smooth, seeded wobble
|
|
86
|
+
* of amplitude `amp` (three sine waves round the ring with random phases from `rnd`, about `waves` bumps per 1000 units
|
|
87
|
+
* of outline), so the cut reads as scissors on paper, not as noise. The same seed gives the same edge.
|
|
88
|
+
*
|
|
89
|
+
* @param {import('../geometry/types.js').Geom} shape The shape.
|
|
90
|
+
* @param {number} amp The wobble's amplitude.
|
|
91
|
+
* @param {() => number} [rnd=Math.random] The random source (seeded, for a repeatable edge).
|
|
92
|
+
* @param {{ step?: number, waves?: number }} [o] The resampling step (3) and the bumps per 1000 units (9).
|
|
93
|
+
* @returns {import('../geometry/types.js').Shape} The shape with its edges wobbled.
|
|
94
|
+
*/
|
|
95
|
+
export const wobble = (shape, amp, rnd = Math.random, { step = 3, waves = 9 } = {}) => {
|
|
96
|
+
const polys = shape.length && typeof shape[0][0][0] === 'number' ? [shape] : shape;
|
|
97
|
+
const ph = [rnd(), rnd(), rnd()].map((v) => v * 2 * Math.PI);
|
|
98
|
+
const ringW = (ring) => {
|
|
99
|
+
const r = dedupe(ring.slice(0, -1).concat(ring.at(-1)[0] === ring[0][0] && ring.at(-1)[1] === ring[0][1] ? [] : [ring.at(-1)]));
|
|
100
|
+
const dense = []; r.forEach((p, i) => {
|
|
101
|
+
const q = r[(i + 1) % r.length], m = Math.max(1, Math.ceil(Math.hypot(q[0] - p[0], q[1] - p[1]) / step));
|
|
102
|
+
for (let j = 0; j < m; j++) dense.push([p[0] + ((q[0] - p[0]) * j) / m, p[1] + ((q[1] - p[1]) * j) / m]);
|
|
103
|
+
});
|
|
104
|
+
const n = dense.length, S = [0]; for (let i = 1; i < n; i++) S.push(S[i - 1] + Math.hypot(dense[i][0] - dense[i - 1][0], dense[i][1] - dense[i - 1][1]));
|
|
105
|
+
const L = S[n - 1] + Math.hypot(dense[0][0] - dense[n - 1][0], dense[0][1] - dense[n - 1][1]), k = Math.max(3, Math.round((waves * L) / 1000));
|
|
106
|
+
return dense.map((p, i) => {
|
|
107
|
+
const a = dense[(i - 1 + n) % n], b = dense[(i + 1) % n], dx = b[0] - a[0], dy = b[1] - a[1], l = Math.hypot(dx, dy) || 1, t = (2 * Math.PI * S[i]) / L;
|
|
108
|
+
const w = amp * (0.6 * Math.sin(k * t + ph[0]) + 0.3 * Math.sin((2 * k + 1) * t + ph[1]) + 0.2 * Math.sin((3 * k + 2) * t + ph[2]));
|
|
109
|
+
return [p[0] - (dy / l) * w, p[1] + (dx / l) * w];
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
return U(...polys.map((pg) => D(poly(ringW(pg[0])), ...pg.slice(1).map((h) => poly(ringW(h))))));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Scissor-cut edges: every ring of a shape redrawn as straight snips about `step` long (corners sharper than `corner`°
|
|
117
|
+
* kept where they are), each snip's end nudged along the normal by up to `jit` from the seeded `rnd`, so curves turn
|
|
118
|
+
* into short straight cuts and straight edges kink slightly, as paper cut by hand.
|
|
119
|
+
*
|
|
120
|
+
* @param {import('../geometry/types.js').Geom} shape The shape.
|
|
121
|
+
* @param {number} [step=34] The snips' length.
|
|
122
|
+
* @param {number} [jit=3] The most a snip's end moves.
|
|
123
|
+
* @param {() => number} [rnd=Math.random] The random source (seeded, for a repeatable edge).
|
|
124
|
+
* @param {number} [corner=140] Corners sharper than this (degrees) stay put.
|
|
125
|
+
* @returns {import('../geometry/types.js').Shape} The shape, snipped.
|
|
126
|
+
*/
|
|
127
|
+
export const snip = (shape, step = 34, jit = 3, rnd = Math.random, corner = 140) => {
|
|
128
|
+
const polys = shape.length && typeof shape[0][0][0] === 'number' ? [shape] : shape;
|
|
129
|
+
const ringS = (ring) => {
|
|
130
|
+
const r = dedupe(ring.at(-1)[0] === ring[0][0] && ring.at(-1)[1] === ring[0][1] ? ring.slice(0, -1) : ring), n = r.length;
|
|
131
|
+
const ang = (i) => {
|
|
132
|
+
const a = r[(i - 1 + n) % n], p = r[i], b = r[(i + 1) % n], u = [a[0] - p[0], a[1] - p[1]], v = [b[0] - p[0], b[1] - p[1]];
|
|
133
|
+
return (Math.acos(Math.max(-1, Math.min(1, (u[0] * v[0] + u[1] * v[1]) / (Math.hypot(...u) * Math.hypot(...v) || 1)))) * 180) / Math.PI;
|
|
134
|
+
};
|
|
135
|
+
const out = []; let acc = 0;
|
|
136
|
+
for (let i = 0; i < n; i++) {
|
|
137
|
+
const p = r[i], q = r[(i + 1) % n], sharp = ang(i) < corner;
|
|
138
|
+
if (sharp || acc >= step || i === 0) {
|
|
139
|
+
const a = r[(i - 1 + n) % n], dx = q[0] - a[0], dy = q[1] - a[1], l = Math.hypot(dx, dy) || 1, j = sharp ? 0 : (rnd() * 2 - 1) * jit;
|
|
140
|
+
out.push([p[0] - (dy / l) * j, p[1] + (dx / l) * j]); acc = 0;
|
|
141
|
+
}
|
|
142
|
+
acc += Math.hypot(q[0] - p[0], q[1] - p[1]);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
};
|
|
146
|
+
return U(...polys.map((pg) => D(poly(ringS(pg[0])), ...pg.slice(1).map((h) => poly(ringS(h))))));
|
|
147
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Geom, Point, Polygon, Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** The base square's side (420). */
|
|
4
|
+
export const GEO_SQ: number;
|
|
5
|
+
/** The base square's left edge. */
|
|
6
|
+
export const GEO_X0: number;
|
|
7
|
+
/** The base square's right edge. */
|
|
8
|
+
export const GEO_X1: number;
|
|
9
|
+
/** The base square, side s, centred. */
|
|
10
|
+
export function sq(s?: number): Polygon;
|
|
11
|
+
/** The unit vector from a to b, and the distance. */
|
|
12
|
+
export function dirOf(a: readonly number[], b: readonly number[]): [Point, number];
|
|
13
|
+
/** A needle gap from a (width w0) to b (width w1), run on past b by ext at the same taper. */
|
|
14
|
+
export function needle(a: readonly number[], b: readonly number[], w0: number, w1: number, ext?: number): Polygon;
|
|
15
|
+
/** A straight band of width w along a → b, square ends. */
|
|
16
|
+
export function band(a: readonly number[], b: readonly number[], w: number): Polygon;
|
|
17
|
+
/** The half plane on one side (s = ±1) of an open polyline that runs right across a shape. */
|
|
18
|
+
export function side(L: readonly Point[], s: number): Polygon;
|
|
19
|
+
/** A front strand laid over a closed band near p: the band cut free round it by g within R of p. */
|
|
20
|
+
export function crossOver(all: Geom, front: Geom, p: readonly number[], R: number, g: number): Shape;
|
|
21
|
+
/** A disc with at least 180 points. */
|
|
22
|
+
export function geoDisc(cx: number, cy: number, r: number): Polygon;
|
|
23
|
+
/** A knob: a disc with one slot at deg, split right across (full) or a pointer from the centre. */
|
|
24
|
+
export function knob(cx: number, cy: number, r: number, deg: number, w: number, full?: boolean): Shape;
|
|
25
|
+
/** Knobs on a grid of tokens ('|', '-', '\\', '/', 'pN', '.'), discs of radius r on a pitch of 2r + g. */
|
|
26
|
+
export function knobs(rows: readonly (readonly string[])[], g: number, w: number, r: number): Shape;
|
|
27
|
+
/** The disc radius that makes n discs and n − 1 gaps span 448. */
|
|
28
|
+
export function rFor(n: number, g: number): number;
|
|
29
|
+
/** A roof tile: a square with a half disc on its far side, the cap pointing at deg. */
|
|
30
|
+
export function roofTile(cx: number, cy: number, a: number, deg?: number): Shape;
|
|
31
|
+
/** Tiles laid back to front, each over the ones before and cut free by g. */
|
|
32
|
+
export function layered(list: readonly Geom[], g: number): Geom;
|
|
33
|
+
/** A triangle's three corners, circumradius R about (cx, cy). */
|
|
34
|
+
export function triPts(cx: number, cy: number, R: number, deg?: number): Point[];
|
|
35
|
+
/** The half of a disc on the side sideDeg points to. */
|
|
36
|
+
export function halfDisc(cx: number, cy: number, r: number, sideDeg: number): Shape;
|
|
37
|
+
/** The split disc's radius (224). */
|
|
38
|
+
export const R7: number;
|
|
39
|
+
/** The disc parted along L by g, each half's rim moved by sa / sb. */
|
|
40
|
+
export function splitDisc(L: readonly Point[], g: number, sa: readonly number[], sb: readonly number[]): Shape;
|
|
41
|
+
/** A roof tile's side in scales() (150). */
|
|
42
|
+
export const SA: number;
|
|
43
|
+
/** Roof tiles on a turned lattice, laid back to front; tiles in `up` face back up. */
|
|
44
|
+
export function scales(cells: readonly Point[], g: number, o?: { a?: number; pi?: number; pj?: number; deg?: number; up?: readonly Point[] }): Shape;
|
|
45
|
+
/** The nine points of a 3 × 3 lattice. */
|
|
46
|
+
export const grid3: Point[];
|
package/src/forms/geo.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bold geometric parts: needle gaps that taper to nothing, knobs and dials, roof-tile scales laid back to front, half
|
|
3
|
+
* discs and a disc split along a line with each half's rim moved. Every part is on the 512 canvas, centred on C. From the
|
|
4
|
+
* Logomarks tools (geoparts.mjs, batch 212, "Bold · Geometric", and its editions in batch 220).
|
|
5
|
+
*/
|
|
6
|
+
import { C, D, I, U, circle, grown, over, polar, poly, rect, rot, stroke } from '../geometry/index.js';
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('../geometry/types.js').Point} Point */
|
|
9
|
+
/** @typedef {import('../geometry/types.js').Polygon} Polygon */
|
|
10
|
+
/** @typedef {import('../geometry/types.js').Shape} Shape */
|
|
11
|
+
/** @typedef {import('../geometry/types.js').Geom} Geom */
|
|
12
|
+
|
|
13
|
+
/** The base square's side. */
|
|
14
|
+
export const GEO_SQ = 420;
|
|
15
|
+
|
|
16
|
+
/** The base square's left edge. */
|
|
17
|
+
export const GEO_X0 = C - GEO_SQ / 2;
|
|
18
|
+
|
|
19
|
+
/** The base square's right edge. */
|
|
20
|
+
export const GEO_X1 = C + GEO_SQ / 2;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The base square, side s, centred on the canvas.
|
|
24
|
+
*
|
|
25
|
+
* @param {number} [s=GEO_SQ] Its side.
|
|
26
|
+
* @returns {Polygon} The square.
|
|
27
|
+
*/
|
|
28
|
+
export const sq = (s = GEO_SQ) => rect(C - s / 2, C - s / 2, s, s);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The unit vector from a to b, and the distance between them.
|
|
32
|
+
*
|
|
33
|
+
* @param {readonly number[]} a From.
|
|
34
|
+
* @param {readonly number[]} b To.
|
|
35
|
+
* @returns {[Point, number]} The direction and the length.
|
|
36
|
+
*/
|
|
37
|
+
export const dirOf = (a, b) => { const L = Math.hypot(b[0] - a[0], b[1] - a[1]); return [[(b[0] - a[0]) / L, (b[1] - a[1]) / L], L]; };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A needle gap from a (width w0) to b (width w1), run on past b by `ext` at the same taper, and back past a by 2, so a
|
|
41
|
+
* zero-width tip still parts the corner cleanly.
|
|
42
|
+
*
|
|
43
|
+
* @param {readonly number[]} a The start.
|
|
44
|
+
* @param {readonly number[]} b Where the taper is measured to.
|
|
45
|
+
* @param {number} w0 The width at a.
|
|
46
|
+
* @param {number} w1 The width at b.
|
|
47
|
+
* @param {number} [ext=300] How far it runs on past b.
|
|
48
|
+
* @returns {Polygon} The gap, to cut.
|
|
49
|
+
*/
|
|
50
|
+
export const needle = (a, b, w0, w1, ext = 300) => {
|
|
51
|
+
const [[ux, uy], L] = dirOf(a, b), n = [-uy, ux], w2 = w0 + ((w1 - w0) * (L + ext)) / L;
|
|
52
|
+
const s = [a[0] - ux * 2, a[1] - uy * 2], e = [a[0] + ux * (L + ext), a[1] + uy * (L + ext)];
|
|
53
|
+
return poly([[s[0] + (n[0] * w0) / 2, s[1] + (n[1] * w0) / 2], [e[0] + (n[0] * w2) / 2, e[1] + (n[1] * w2) / 2], [e[0] - (n[0] * w2) / 2, e[1] - (n[1] * w2) / 2], [s[0] - (n[0] * w0) / 2, s[1] - (n[1] * w0) / 2]]);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A straight band of width w along a → b, its ends square (make it long enough to clip).
|
|
58
|
+
*
|
|
59
|
+
* @param {readonly number[]} a The start.
|
|
60
|
+
* @param {readonly number[]} b The end.
|
|
61
|
+
* @param {number} w The width.
|
|
62
|
+
* @returns {Polygon} The band.
|
|
63
|
+
*/
|
|
64
|
+
export const band = (a, b, w) => {
|
|
65
|
+
const [[ux, uy]] = dirOf(a, b), n = [(-uy * w) / 2, (ux * w) / 2];
|
|
66
|
+
return poly([[a[0] + n[0], a[1] + n[1]], [b[0] + n[0], b[1] + n[1]], [b[0] - n[0], b[1] - n[1]], [a[0] - n[0], a[1] - n[1]]]);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The half plane on one side of an open polyline that runs right across a shape: the polyline closed by two points 3000
|
|
71
|
+
* out along the normal of its first-to-last chord.
|
|
72
|
+
*
|
|
73
|
+
* @param {readonly Point[]} L The polyline.
|
|
74
|
+
* @param {number} s Which side: 1 or −1.
|
|
75
|
+
* @returns {Polygon} The half plane.
|
|
76
|
+
*/
|
|
77
|
+
export const side = (L, s) => {
|
|
78
|
+
const a = L[0], b = L.at(-1), [[dx, dy]] = dirOf(a, b), n = [-dy * s * 3000, dx * s * 3000];
|
|
79
|
+
return poly([...L, [b[0] + n[0], b[1] + n[1]], [a[0] + n[0], a[1] + n[1]]]);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A front strand laid over a closed band where they cross near p: the band is cut free round `front` by g, only inside
|
|
84
|
+
* the radius R of p.
|
|
85
|
+
*
|
|
86
|
+
* @param {Geom} all The band (and anything else under the strand).
|
|
87
|
+
* @param {Geom} front The strand in front.
|
|
88
|
+
* @param {readonly number[]} p The crossing.
|
|
89
|
+
* @param {number} R How far round the crossing the cut reaches.
|
|
90
|
+
* @param {number} g The gap.
|
|
91
|
+
* @returns {Shape} The crossing drawn.
|
|
92
|
+
*/
|
|
93
|
+
export const crossOver = (all, front, p, R, g) => U(D(all, I(grown(front, g), circle(...p, R, 120))), front);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A disc, with at least 180 points (two a unit of diameter).
|
|
97
|
+
*
|
|
98
|
+
* @param {number} cx The centre.
|
|
99
|
+
* @param {number} cy The centre.
|
|
100
|
+
* @param {number} r The radius.
|
|
101
|
+
* @returns {Polygon} The disc.
|
|
102
|
+
*/
|
|
103
|
+
export const geoDisc = (cx, cy, r) => circle(cx, cy, r, Math.max(180, Math.round(r * 2)));
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A knob: a disc of radius r with one slot of width w at `deg` (screen degrees, 0 right, 90 down). `full` splits it along
|
|
107
|
+
* the whole diameter; otherwise the slot runs from the centre out through the rim (a pointer).
|
|
108
|
+
*
|
|
109
|
+
* @param {number} cx The centre.
|
|
110
|
+
* @param {number} cy The centre.
|
|
111
|
+
* @param {number} r The radius.
|
|
112
|
+
* @param {number} deg The slot's angle.
|
|
113
|
+
* @param {number} w The slot's width.
|
|
114
|
+
* @param {boolean} [full=true] Split right across, or a pointer.
|
|
115
|
+
* @returns {Shape} The knob.
|
|
116
|
+
*/
|
|
117
|
+
export const knob = (cx, cy, r, deg, w, full = true) => {
|
|
118
|
+
const p = polar(r + 30, deg, cx, cy), q = full ? polar(r + 30, deg + 180, cx, cy) : [cx, cy];
|
|
119
|
+
const slot = full ? band(q, p, w) : U(band(q, p, w), circle(cx, cy, w / 2, 48));
|
|
120
|
+
return D(geoDisc(cx, cy, r), slot);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Knobs on a grid, centred on the canvas: rows of tokens, each a slot angle (`|` 90, `-` 0, `\` 45, `/` −45, `pN` a
|
|
125
|
+
* pointer at N degrees, `.` no disc), discs of radius r on a pitch of 2r + g.
|
|
126
|
+
*
|
|
127
|
+
* @param {readonly (readonly string[])[]} rows The tokens, row by row.
|
|
128
|
+
* @param {number} g The gap between discs.
|
|
129
|
+
* @param {number} w The slot width.
|
|
130
|
+
* @param {number} r The disc radius.
|
|
131
|
+
* @returns {Shape} The knobs.
|
|
132
|
+
*/
|
|
133
|
+
export const knobs = (rows, g, w, r) => {
|
|
134
|
+
const n = Math.max(...rows.map((x) => x.length)), m = rows.length, p = 2 * r + g, out = [];
|
|
135
|
+
const ang = { '|': 90, '-': 0, '\\': 45, '/': -45 };
|
|
136
|
+
rows.forEach((row, j) => row.forEach((t, i) => {
|
|
137
|
+
if (t === '.') return; const cx = C + (i - (n - 1) / 2) * p, cy = C + (j - (m - 1) / 2) * p;
|
|
138
|
+
out.push(t[0] === 'p' ? knob(cx, cy, r, +t.slice(1), w, false) : knob(cx, cy, r, ang[t], w, true));
|
|
139
|
+
}));
|
|
140
|
+
return U(...out);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The disc radius that makes n discs and n − 1 gaps span 448.
|
|
145
|
+
*
|
|
146
|
+
* @param {number} n Discs across.
|
|
147
|
+
* @param {number} g The gap.
|
|
148
|
+
* @returns {number} The radius.
|
|
149
|
+
*/
|
|
150
|
+
export const rFor = (n, g) => (448 - (n - 1) * g) / n / 2;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A roof tile: a square of side a with a half disc on its far side, the cap pointing at `deg`, centred on the square.
|
|
154
|
+
*
|
|
155
|
+
* @param {number} cx The square's centre.
|
|
156
|
+
* @param {number} cy The square's centre.
|
|
157
|
+
* @param {number} a Its side.
|
|
158
|
+
* @param {number} [deg=90] Where the cap points (90: down).
|
|
159
|
+
* @returns {Shape} The tile.
|
|
160
|
+
*/
|
|
161
|
+
export const roofTile = (cx, cy, a, deg = 90) => rot(U(rect(cx - a / 2, cy - a / 2, a, a), geoDisc(cx, cy + a / 2, a / 2)), deg - 90, cx, cy);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Tiles laid back to front, each over the ones before it and cut free of them by g.
|
|
165
|
+
*
|
|
166
|
+
* @param {readonly Geom[]} list The tiles, back first.
|
|
167
|
+
* @param {number} g The gap.
|
|
168
|
+
* @returns {Geom} The stack.
|
|
169
|
+
*/
|
|
170
|
+
export const layered = (list, g) => list.reduce((acc, t) => (acc.length ? over(acc, t, g) : t), []);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A triangle's corner points (apex up by default), circumradius R, centred on (cx, cy).
|
|
174
|
+
*
|
|
175
|
+
* @param {number} cx The centre.
|
|
176
|
+
* @param {number} cy The centre.
|
|
177
|
+
* @param {number} R The circumradius.
|
|
178
|
+
* @param {number} [deg=-90] The first corner's angle.
|
|
179
|
+
* @returns {Point[]} The three corners.
|
|
180
|
+
*/
|
|
181
|
+
export const triPts = (cx, cy, R, deg = -90) => [0, 1, 2].map((k) => polar(R, deg + k * 120, cx, cy));
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Half a disc: the half on the side `sideDeg` points to.
|
|
185
|
+
*
|
|
186
|
+
* @param {number} cx The centre.
|
|
187
|
+
* @param {number} cy The centre.
|
|
188
|
+
* @param {number} r The radius.
|
|
189
|
+
* @param {number} sideDeg The direction of the half kept (90: the lower half).
|
|
190
|
+
* @returns {Shape} The half disc.
|
|
191
|
+
*/
|
|
192
|
+
export const halfDisc = (cx, cy, r, sideDeg) => I(geoDisc(cx, cy, r), rot(rect(cx - r - 2, cy, 2 * r + 4, r + 2), sideDeg - 90, cx, cy));
|
|
193
|
+
|
|
194
|
+
/** The split disc's radius. */
|
|
195
|
+
export const R7 = 224;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The disc (radius R7, centred) parted along the polyline L by g, each half's rim moved by sa / sb: the cut stays put, so
|
|
199
|
+
* the gap stays g.
|
|
200
|
+
*
|
|
201
|
+
* @param {readonly Point[]} L The cut.
|
|
202
|
+
* @param {number} g The gap.
|
|
203
|
+
* @param {readonly number[]} sa How far the rim on the left side moves.
|
|
204
|
+
* @param {readonly number[]} sb How far the rim on the right side moves.
|
|
205
|
+
* @returns {Shape} The two halves.
|
|
206
|
+
*/
|
|
207
|
+
export const splitDisc = (L, g, sa, sb) => {
|
|
208
|
+
const z = stroke(L, g, { chunk: 4 }), d = (o) => geoDisc(C + o[0], C + o[1], R7);
|
|
209
|
+
return U(D(I(d(sa), side(L, 1)), z), D(I(d(sb), side(L, -1)), z));
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** A roof tile's side in scales(). */
|
|
213
|
+
export const SA = 150;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Roof tiles at lattice points (i, j), pitch pi·a across and pj·a down (under 1, so each cap lies over the tile below and
|
|
217
|
+
* the tile beside), caps pointing down, the lattice turned by deg and laid back to front, the far rows first. A tile
|
|
218
|
+
* listed in `up` faces back up and is laid last, its cap over the tile above it.
|
|
219
|
+
*
|
|
220
|
+
* @param {readonly Point[]} cells The lattice points.
|
|
221
|
+
* @param {number} g The gap.
|
|
222
|
+
* @param {{ a?: number, pi?: number, pj?: number, deg?: number, up?: readonly Point[] }} [o] The tile side (150), the
|
|
223
|
+
* pitches (0.75), the turn (45) and the tiles that face up.
|
|
224
|
+
* @returns {Shape} The scales.
|
|
225
|
+
*/
|
|
226
|
+
export const scales = (cells, g, { a = 150, pi = 0.75, pj = 0.75, deg = 45, up = [] } = {}) => {
|
|
227
|
+
const isUp = ([i, j]) => up.some(([ui, uj]) => ui === i && uj === j);
|
|
228
|
+
const list = cells.slice().sort((p, q) => isUp(p) - isUp(q) || q[1] - p[1] || q[0] - p[0]);
|
|
229
|
+
return rot(layered(list.map((c) => roofTile(C + c[0] * a * pi, C + c[1] * a * pj, a, isUp(c) ? -90 : 90)), g), deg);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** The nine points of a 3 × 3 lattice, row by row. */
|
|
233
|
+
export const grid3 = [0, 1, 2].flatMap((j) => [0, 1, 2].map((i) => [i, j]));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Point, Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** A hook: the end it grows from, the neighbour's cap centre it wraps, and its sweep (or end angle e and past). */
|
|
4
|
+
export interface Hook {
|
|
5
|
+
end?: 0 | 1;
|
|
6
|
+
c: readonly number[];
|
|
7
|
+
rn?: number;
|
|
8
|
+
sw?: number;
|
|
9
|
+
e?: number;
|
|
10
|
+
past?: number;
|
|
11
|
+
t0?: number;
|
|
12
|
+
t1?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The capsule's width (150). */
|
|
16
|
+
export const HOOK_W: number;
|
|
17
|
+
/** The point d from p at angle a (degrees). */
|
|
18
|
+
export function hookAt(p: readonly number[], a: number, d: number): Point;
|
|
19
|
+
/** Where a cap centre sits to hook the neighbour cap at c, at angle a. */
|
|
20
|
+
export function kiss(c: readonly number[], a: number, g: number, r?: number, rn?: number): Point;
|
|
21
|
+
/** A curl: a band of inner radius ri about c, from a0 sweeping sw, tapering t0 → t1, round tip. */
|
|
22
|
+
export function curl(c: readonly number[], ri: number, a0: number, sw: number, t0: number, t1: number, n?: number): Shape;
|
|
23
|
+
/** A capsule along pts with hooks, its curl roots filleted. */
|
|
24
|
+
export function piece(pts: readonly Point[], hooks?: readonly Hook[], o?: { w?: number; bend?: number; g?: number; soft?: number; whole?: boolean }): Shape;
|
|
25
|
+
/** The Track band's width for the gap g (74 at 22). */
|
|
26
|
+
export function TB(g: number): number;
|
|
27
|
+
/** A piece's Track build: its centreline run round each hook, as two tracks. */
|
|
28
|
+
export function trackPiece(pts: readonly Point[], hooks?: readonly Hook[], o?: { g?: number; bend?: number }): Shape;
|
|
29
|
+
/** Points along an arc about c, radius r, from a0 to a1 degrees. */
|
|
30
|
+
export function arc(c: readonly number[], r: number, a0: number, a1: number, n?: number): Point[];
|
|
31
|
+
/** A neighbour's end: its cap centre and the angle it faces. */
|
|
32
|
+
export function endOf(pts: readonly Point[], end?: 0 | 1): { c: Point; e: number };
|
|
33
|
+
/** Every point of every ring of a shape. */
|
|
34
|
+
export function pts0(s: Shape): Point[];
|
|
35
|
+
/** join()'s check switch: with `on`, it logs pieces closer than the gap. */
|
|
36
|
+
export const hookCheck: { on: boolean };
|
|
37
|
+
/** The pieces as one shape, checked when hookCheck.on. */
|
|
38
|
+
export function join(g: number, ...ps: Shape[]): Shape;
|
|
39
|
+
/** A Track piece's end: c + (TB(g) + g) along d ± 90°. */
|
|
40
|
+
export function tq(c: readonly number[], d: number, side: number, g: number): Point;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bold hooks: capsules whose round end runs on into a curl that wraps a neighbour's round end, the gap exact all round,
|
|
3
|
+
* and their Track build (the same layout drawn as two tracks). Angles are degrees, 0 right, 90 down. From the Logomarks
|
|
4
|
+
* tools (hookparts.mjs, batch 214, "Bold · Hooks", and its editions in batch 220).
|
|
5
|
+
*/
|
|
6
|
+
import { I, U, circle, fillet, pill, poly, rad, soften, stroke, tracks } from '../geometry/index.js';
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('../geometry/types.js').Point} Point */
|
|
9
|
+
/** @typedef {import('../geometry/types.js').Shape} Shape */
|
|
10
|
+
/** @typedef {import('../geometry/types.js').Geom} Geom */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A hook: the end of a piece it grows from, the neighbour's cap centre it wraps, and how far it sweeps.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {object} Hook
|
|
16
|
+
* @property {0 | 1} [end] The end it grows from: 0 the first point, 1 (the default) the last.
|
|
17
|
+
* @property {readonly number[]} c The neighbour's cap centre.
|
|
18
|
+
* @property {number} [rn] The neighbour's cap radius (HOOK_W / 2).
|
|
19
|
+
* @property {number} [sw] The sweep, degrees; or give `e`.
|
|
20
|
+
* @property {number} [e] The angle the curl should end at (the neighbour's end direction), when `sw` isn't given.
|
|
21
|
+
* @property {number} [past] Degrees past `e`.
|
|
22
|
+
* @property {number} [t0] The curl's width at its root (the piece's width).
|
|
23
|
+
* @property {number} [t1] Its width at the tip (0.44 of the piece's width).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The capsule's width. */
|
|
27
|
+
export const HOOK_W = 150;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The point d from p at angle a.
|
|
31
|
+
*
|
|
32
|
+
* @param {readonly number[]} p The point.
|
|
33
|
+
* @param {number} a The angle, degrees.
|
|
34
|
+
* @param {number} d The distance.
|
|
35
|
+
* @returns {Point} The point reached.
|
|
36
|
+
*/
|
|
37
|
+
export const hookAt = (p, a, d) => [p[0] + d * Math.cos(rad(a)), p[1] + d * Math.sin(rad(a))];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The angle from p to q, degrees.
|
|
41
|
+
*
|
|
42
|
+
* @param {readonly number[]} p From.
|
|
43
|
+
* @param {readonly number[]} q To.
|
|
44
|
+
* @returns {number} The angle.
|
|
45
|
+
*/
|
|
46
|
+
const ang = (p, q) => (Math.atan2(q[1] - p[1], q[0] - p[0]) * 180) / Math.PI;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The distance from p to q.
|
|
50
|
+
*
|
|
51
|
+
* @param {readonly number[]} p From.
|
|
52
|
+
* @param {readonly number[]} q To.
|
|
53
|
+
* @returns {number} The distance.
|
|
54
|
+
*/
|
|
55
|
+
const dist = (p, q) => Math.hypot(q[0] - p[0], q[1] - p[1]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Where a piece's cap centre sits to hook the neighbour cap at c: at angle a from c, the two caps and the gap apart.
|
|
59
|
+
*
|
|
60
|
+
* @param {readonly number[]} c The neighbour's cap centre.
|
|
61
|
+
* @param {number} a The angle from it, degrees.
|
|
62
|
+
* @param {number} g The gap.
|
|
63
|
+
* @param {number} [r=HOOK_W / 2] This cap's radius.
|
|
64
|
+
* @param {number} [rn=HOOK_W / 2] The neighbour's.
|
|
65
|
+
* @returns {Point} The cap centre.
|
|
66
|
+
*/
|
|
67
|
+
export const kiss = (c, a, g, r = HOOK_W / 2, rn = HOOK_W / 2) => hookAt(c, a, r + rn + g);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The curl: a band whose inner edge is radius ri about c, from angle a0 sweeping sw degrees, tapering t0 → t1, with a
|
|
71
|
+
* round tip.
|
|
72
|
+
*
|
|
73
|
+
* @param {readonly number[]} c The centre.
|
|
74
|
+
* @param {number} ri The inner radius.
|
|
75
|
+
* @param {number} a0 The start angle, degrees.
|
|
76
|
+
* @param {number} sw The sweep, degrees.
|
|
77
|
+
* @param {number} t0 The width at the start.
|
|
78
|
+
* @param {number} t1 The width at the tip.
|
|
79
|
+
* @param {number} [n=120] Steps along it.
|
|
80
|
+
* @returns {Shape} The curl.
|
|
81
|
+
*/
|
|
82
|
+
export const curl = (c, ri, a0, sw, t0, t1, n = 120) => {
|
|
83
|
+
const inner = [], outer = [];
|
|
84
|
+
for (let i = 0; i <= n; i++) { const f = i / n, a = a0 + sw * f, t = t1 + (t0 - t1) * (1 - f) ** 2; inner.push(hookAt(c, a, ri)); outer.push(hookAt(c, a, ri + t)); }
|
|
85
|
+
return U(poly([...inner, ...outer.reverse()]), circle(...hookAt(c, a0 + sw, ri + t1 / 2), t1 / 2, 120));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A piece: a capsule w wide along `pts` (corners filleted by `bend`), with hooks. Each hook names the end it grows from,
|
|
90
|
+
* the neighbour's cap centre and the sweep; it logs when the end isn't a kiss from the neighbour. The root of each curl is
|
|
91
|
+
* filleted (`soft`) where it leaves the round end, and only there, so a same-piece gap under 2·soft (a bowl's tip near its
|
|
92
|
+
* stem) doesn't close up; `whole` fillets every inner corner of a piece with no such gap.
|
|
93
|
+
*
|
|
94
|
+
* @param {readonly Point[]} pts The capsule's centreline.
|
|
95
|
+
* @param {readonly Hook[]} [hooks=[]] Its hooks.
|
|
96
|
+
* @param {{ w?: number, bend?: number, g?: number, soft?: number, whole?: boolean }} [o] Width (HOOK_W), corner fillet
|
|
97
|
+
* (90), gap (22), root fillet (40), and whether to fillet every inner corner.
|
|
98
|
+
* @returns {Shape} The piece.
|
|
99
|
+
*/
|
|
100
|
+
export const piece = (pts, hooks = [], { w = HOOK_W, bend = 90, g = 22, soft = 40, whole = false } = {}) => {
|
|
101
|
+
const body = pts.length === 2 ? pill(pts[0], pts[1], w) : stroke(bend ? fillet(pts, bend) : pts, w, { roundCaps: true, chunk: 8 });
|
|
102
|
+
const curls = hooks.map(({ end = 1, c, rn = HOOK_W / 2, sw, e, past = 0, t0 = w, t1 = 0.44 * w }) => {
|
|
103
|
+
const q = end ? pts.at(-1) : pts[0];
|
|
104
|
+
const d = dist(c, q), want = w / 2 + rn + g; if (Math.abs(d - want) > 0.5) console.log(` hook off by ${(d - want).toFixed(1)}`);
|
|
105
|
+
const a0 = ang(c, q); if (sw == null) { const dd = ((e - a0 + 540) % 360) - 180; sw = dd + Math.sign(dd) * past; }
|
|
106
|
+
return curl(c, rn + g, a0, sw, t0, t1);
|
|
107
|
+
});
|
|
108
|
+
const roots = hooks.map(({ end = 1 }) => circle(...(end ? pts.at(-1) : pts[0]), 0.9 * w, 160));
|
|
109
|
+
if (soft && whole) return soften(U(body, ...curls), soft, 0);
|
|
110
|
+
return soft && roots.length ? U(body, ...curls, I(soften(U(body, ...curls), soft, 0), ...(roots.length > 1 ? [U(...roots)] : roots))) : U(body, ...curls);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The Track band's width for the gap g: 74 at 22, so the Track build lands on the house Track rules once fitted. Pass it
|
|
115
|
+
* as `r` and `rn` to kiss() for a Track layout.
|
|
116
|
+
*
|
|
117
|
+
* @param {number} g The gap.
|
|
118
|
+
* @returns {number} The band width.
|
|
119
|
+
*/
|
|
120
|
+
export const TB = (g) => (74 * g) / 22;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The Track build of a piece: its centreline run on round each hook's neighbour (at the kiss distance, so the curl's inner
|
|
124
|
+
* track clears the neighbour's tracks by g), drawn as two tracks 26 wide at pitch 48, both scaled with g.
|
|
125
|
+
*
|
|
126
|
+
* @param {readonly Point[]} pts The centreline.
|
|
127
|
+
* @param {readonly Hook[]} [hooks=[]] Its hooks (`end`, `c`, `sw` or `e` and `past`).
|
|
128
|
+
* @param {{ g?: number, bend?: number }} [o] The gap (22) and the corner fillet (60).
|
|
129
|
+
* @returns {Shape} The two tracks.
|
|
130
|
+
*/
|
|
131
|
+
export const trackPiece = (pts, hooks = [], { g = 22, bend = 60 } = {}) => {
|
|
132
|
+
let path = pts.slice();
|
|
133
|
+
for (const { end = 1, c, sw: sw0, e, past = 0 } of hooks) {
|
|
134
|
+
if (!end) path = path.slice().reverse(); const q = path.at(-1), a0 = ang(c, q);
|
|
135
|
+
let sw = sw0; if (sw == null) { const dd = ((e - a0 + 540) % 360) - 180; sw = dd + Math.sign(dd) * past; }
|
|
136
|
+
path = [...path, ...arc(c, dist(c, q), a0, a0 + sw, Math.max(8, Math.ceil(Math.abs(sw) / 4))).slice(1)];
|
|
137
|
+
}
|
|
138
|
+
return tracks(bend ? fillet(path, bend) : path, { n: 2, pitch: (48 * g) / 22, w: (26 * g) / 22 });
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Points along an arc about c, radius r, from a0 to a1 degrees.
|
|
143
|
+
*
|
|
144
|
+
* @param {readonly number[]} c The centre.
|
|
145
|
+
* @param {number} r The radius.
|
|
146
|
+
* @param {number} a0 The start angle.
|
|
147
|
+
* @param {number} a1 The end angle.
|
|
148
|
+
* @param {number} [n=60] Steps.
|
|
149
|
+
* @returns {Point[]} n + 1 points.
|
|
150
|
+
*/
|
|
151
|
+
export const arc = (c, r, a0, a1, n = 60) => [...Array(n + 1)].map((_, i) => hookAt(c, a0 + ((a1 - a0) * i) / n, r));
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A neighbour's end, for a hook: its cap centre and the way the end faces.
|
|
155
|
+
*
|
|
156
|
+
* @param {readonly Point[]} pts The neighbour's centreline.
|
|
157
|
+
* @param {0 | 1} [end=1] Which end.
|
|
158
|
+
* @returns {{ c: Point, e: number }} The cap centre and the angle the end faces.
|
|
159
|
+
*/
|
|
160
|
+
export const endOf = (pts, end = 1) => (end ? { c: pts.at(-1), e: ang(pts.at(-2), pts.at(-1)) } : { c: pts[0], e: ang(pts[1], pts[0]) });
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Every point of every ring of a shape.
|
|
164
|
+
*
|
|
165
|
+
* @param {Shape} s The shape.
|
|
166
|
+
* @returns {Point[]} The points.
|
|
167
|
+
*/
|
|
168
|
+
export const pts0 = (s) => s.flatMap((pg) => pg.flatMap((r) => r));
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The least distance between two shapes' outline points.
|
|
172
|
+
*
|
|
173
|
+
* @param {Shape} a One shape.
|
|
174
|
+
* @param {Shape} b The other.
|
|
175
|
+
* @returns {number} The distance.
|
|
176
|
+
*/
|
|
177
|
+
const near = (a, b) => { let m = 1e9; const pa = pts0(a), pb = pts0(b); for (const p of pa) for (const q of pb) { const d = (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2; if (d < m) m = d; } return Math.sqrt(m); };
|
|
178
|
+
|
|
179
|
+
/** join()'s check: set `on` to have it log any two pieces closer than the gap. */
|
|
180
|
+
export const hookCheck = { on: false };
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The pieces as one shape; with hookCheck.on, it logs any two that come closer than the gap.
|
|
184
|
+
*
|
|
185
|
+
* @param {number} g The gap.
|
|
186
|
+
* @param {...Shape} ps The pieces.
|
|
187
|
+
* @returns {Shape} Their union.
|
|
188
|
+
*/
|
|
189
|
+
export const join = (g, ...ps) => {
|
|
190
|
+
if (hookCheck.on) for (let i = 0; i < ps.length; i++) for (let j = i + 1; j < ps.length; j++) { const d = near(ps[i], ps[j]); if (d < g - 0.6) console.log(` pieces ${i},${j} only ${d.toFixed(1)} apart (g ${g.toFixed(1)})`); }
|
|
191
|
+
return U(...ps);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* For the Track build: a straight run must enter the curl on its tangent (a corner there kinks the outer track), so the
|
|
196
|
+
* piece's end sits a quarter turn round from where the bold build puts it: c + (TB(g) + g) along d ± 90°.
|
|
197
|
+
*
|
|
198
|
+
* @param {readonly number[]} c The neighbour's cap centre.
|
|
199
|
+
* @param {number} d The run's direction, degrees.
|
|
200
|
+
* @param {number} side Which side: 1 or −1.
|
|
201
|
+
* @param {number} g The gap.
|
|
202
|
+
* @returns {Point} The piece's end.
|
|
203
|
+
*/
|
|
204
|
+
export const tq = (c, d, side, g) => hookAt(c, d + side * 90, TB(g) + g);
|