@logomarks/core 0.1.11 → 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.
Files changed (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -0
  3. package/package.json +47 -4
  4. package/src/font/glyphs.d.ts +31 -0
  5. package/src/font/glyphs.js +228 -0
  6. package/src/font/index.d.ts +10 -0
  7. package/src/font/index.js +10 -0
  8. package/src/font/measure.d.ts +16 -0
  9. package/src/font/measure.js +201 -0
  10. package/src/font/moves.d.ts +42 -0
  11. package/src/font/moves.js +263 -0
  12. package/src/forms/discs.d.ts +20 -0
  13. package/src/forms/discs.js +114 -0
  14. package/src/forms/emblems.d.ts +22 -0
  15. package/src/forms/emblems.js +109 -0
  16. package/src/forms/flow.d.ts +21 -0
  17. package/src/forms/flow.js +80 -0
  18. package/src/forms/folds.d.ts +18 -0
  19. package/src/forms/folds.js +147 -0
  20. package/src/forms/geo.d.ts +46 -0
  21. package/src/forms/geo.js +233 -0
  22. package/src/forms/hooks.d.ts +40 -0
  23. package/src/forms/hooks.js +204 -0
  24. package/src/forms/index.d.ts +22 -0
  25. package/src/forms/index.js +24 -0
  26. package/src/forms/jog.d.ts +33 -0
  27. package/src/forms/jog.js +162 -0
  28. package/src/forms/lines.d.ts +32 -0
  29. package/src/forms/lines.js +233 -0
  30. package/src/forms/ref.d.ts +41 -0
  31. package/src/forms/ref.js +246 -0
  32. package/src/forms/shatter.d.ts +40 -0
  33. package/src/forms/shatter.js +341 -0
  34. package/src/forms/slats.d.ts +35 -0
  35. package/src/forms/slats.js +78 -0
  36. package/src/forms/swash.d.ts +23 -0
  37. package/src/forms/swash.js +74 -0
  38. package/src/forms/tech.d.ts +40 -0
  39. package/src/forms/tech.js +293 -0
  40. package/src/forms/tracks.d.ts +36 -0
  41. package/src/forms/tracks.js +121 -0
  42. package/src/index.js +2 -1
  43. package/src/iso/index.d.ts +5 -0
  44. package/src/iso/index.js +13 -0
  45. package/src/iso/objects.d.ts +168 -0
  46. package/src/iso/objects.js +1042 -0
  47. package/src/iso/planes.d.ts +27 -0
  48. package/src/iso/planes.js +138 -0
  49. package/src/iso/voxels.d.ts +43 -0
  50. package/src/iso/voxels.js +255 -0
  51. package/src/letters/blackletter.d.ts +31 -0
  52. package/src/letters/blackletter.js +199 -0
  53. package/src/letters/dotstroke.d.ts +16 -0
  54. package/src/letters/dotstroke.js +77 -0
  55. package/src/letters/echo.d.ts +42 -0
  56. package/src/letters/echo.js +246 -0
  57. package/src/letters/frame.d.ts +6 -0
  58. package/src/letters/frame.js +14 -0
  59. package/src/letters/index.d.ts +16 -0
  60. package/src/letters/index.js +18 -0
  61. package/src/letters/inktrap.d.ts +66 -0
  62. package/src/letters/inktrap.js +244 -0
  63. package/src/letters/mono.d.ts +145 -0
  64. package/src/letters/mono.js +684 -0
  65. package/src/letters/pillar.d.ts +31 -0
  66. package/src/letters/pillar.js +257 -0
  67. package/src/letters/serif.d.ts +109 -0
  68. package/src/letters/serif.js +666 -0
  69. package/src/letters/stair.d.ts +8 -0
  70. package/src/letters/stair.js +43 -0
  71. package/src/letters/stem.d.ts +29 -0
  72. package/src/letters/stem.js +205 -0
  73. package/src/objects/animals.d.ts +105 -0
  74. package/src/objects/animals.js +469 -0
  75. package/src/objects/bio.d.ts +14 -0
  76. package/src/objects/bio.js +76 -0
  77. package/src/objects/devglyphs.d.ts +34 -0
  78. package/src/objects/devglyphs.js +226 -0
  79. package/src/objects/devices.d.ts +31 -0
  80. package/src/objects/devices.js +114 -0
  81. package/src/objects/faces.d.ts +94 -0
  82. package/src/objects/faces.js +344 -0
  83. package/src/objects/food.d.ts +12 -0
  84. package/src/objects/food.js +74 -0
  85. package/src/objects/index.d.ts +13 -0
  86. package/src/objects/index.js +18 -0
  87. package/src/objects/rosettes.d.ts +14 -0
  88. package/src/objects/rosettes.js +64 -0
  89. package/src/objects/sport.d.ts +8 -0
  90. package/src/objects/sport.js +37 -0
  91. package/src/objects/weapons.d.ts +31 -0
  92. package/src/objects/weapons.js +179 -0
  93. package/src/pixels/dither.d.ts +78 -0
  94. package/src/pixels/dither.js +399 -0
  95. package/src/pixels/emote.d.ts +6 -0
  96. package/src/pixels/emote.js +76 -0
  97. package/src/pixels/index.d.ts +7 -0
  98. package/src/pixels/index.js +8 -0
  99. package/src/sdf/index.d.ts +2 -0
  100. package/src/sdf/index.js +2 -0
  101. package/src/sdf/y2k.d.ts +12 -0
  102. package/src/sdf/y2k.js +80 -0
  103. package/src/util/vec.d.ts +14 -0
  104. package/src/util/vec.js +61 -0
@@ -0,0 +1,27 @@
1
+ import type { Shape } from '../geometry/types.js';
2
+
3
+ /** A view for planeScene(): its projection, the floor point under a screen point, and the way to the viewer. */
4
+ export interface PlaneView {
5
+ p: (pt: readonly number[]) => number[];
6
+ base: (X: number, Y: number) => number[];
7
+ dir: number[];
8
+ }
9
+ /** 3D points. */
10
+ type Pts3 = readonly (readonly number[])[];
11
+
12
+ /** A parallelogram from corner o along u and v. */
13
+ export function quad(o: readonly number[], u: readonly number[], v: readonly number[]): number[][];
14
+ /** A plane hinged on a→b, turned deg about it, reaching L. */
15
+ export function hinge(a: readonly number[], b: readonly number[], out: readonly number[], deg: number, L: number): number[][];
16
+ /** A strip folded along a 3D profile, one plane a segment. */
17
+ export function ribbon(pts: Pts3, depth: readonly number[]): number[][][];
18
+ /** The iso view. */
19
+ export const ISO: PlaneView;
20
+ /** An oblique view: depth swept at deg, shortened by k. */
21
+ export function OBL(k?: number, deg?: number): PlaneView;
22
+ /** Planes drawn as one mark, back to front, a mitred gap g round each. */
23
+ export function planeScene(planes: readonly (Pts3 | readonly Pts3[])[], view: PlaneView, g: number): Shape;
24
+ /** A mark from planes in a view, as a build (g) => shape. */
25
+ export function planeMark(planes: readonly (Pts3 | readonly Pts3[])[], view?: PlaneView): (g: number) => Shape;
26
+ /** A plane letter: strokes swept in oblique depth. */
27
+ export function planeGlyph(strokes: readonly Pts3[], deg?: number, d?: number, k?: number): (g: number) => Shape;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Planes: flat 3D planes (no thickness) hinged and folded, projected iso or oblique and laid back to front by
3
+ * planeScene(), each cutting a mitred gap round itself out of what's behind. A plane is a convex 3D polygon, or several
4
+ * convex pieces of one plane (a wall with a door cut) drawn as one. The plane letters sweep every stroke up and to the
5
+ * right in oblique depth, so a stem shows its side and a bar its top, and each corner is a fold.
6
+ * From the Logomarks tools (planeparts.mjs, batch 215, reused by 220).
7
+ */
8
+ import { C, D, I, U, poly, rad } from '../geometry/index.js';
9
+ import { cross3 as cross, shoelace as area2 } from '../util/vec.js';
10
+ import { mitre, dropThin } from './voxels.js';
11
+
12
+ /* ---------- vectors (private) ---------- */
13
+ const add = (a, b) => a.map((v, i) => v + b[i]), sub = (a, b) => a.map((v, i) => v - b[i]), mul = (a, k) => a.map((v) => v * k);
14
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
15
+ /** v turned by deg about the unit axis k (Rodrigues). */
16
+ const turn = (v, k, deg) => {
17
+ const t = rad(deg), c = Math.cos(t), s = Math.sin(t);
18
+ return add(add(mul(v, c), mul(cross(k, v), s)), mul(k, dot(k, v) * (1 - c)));
19
+ };
20
+ const unit = (v) => mul(v, 1 / Math.hypot(...v));
21
+
22
+ /* ---------- planes ---------- */
23
+
24
+ /**
25
+ * A parallelogram from corner o along u and v.
26
+ *
27
+ * @param {readonly number[]} o The corner.
28
+ * @param {readonly number[]} u One side.
29
+ * @param {readonly number[]} v The other.
30
+ * @returns {number[][]} Its four corners, in 3D.
31
+ */
32
+ export const quad = (o, u, v) => [o, add(o, u), add(add(o, u), v), add(o, v)];
33
+
34
+ /**
35
+ * A plane hinged on the edge a→b: it leaves the edge along `out` (a unit vector square to the edge), turned by deg
36
+ * about the edge, reaching L.
37
+ *
38
+ * @param {readonly number[]} a The hinge's start.
39
+ * @param {readonly number[]} b Its end.
40
+ * @param {readonly number[]} out The way it leaves the edge before turning (unit, square to the edge).
41
+ * @param {number} deg The turn about the edge.
42
+ * @param {number} L How far it reaches.
43
+ * @returns {number[][]} Its four corners, in 3D.
44
+ */
45
+ export const hinge = (a, b, out, deg, L) => { const k = unit(sub(b, a)), d = mul(turn(out, k, deg), L); return [a, b, add(b, d), add(a, d)]; };
46
+
47
+ /**
48
+ * A strip folded along a profile: each segment of `pts` (3D) swept by `depth` is one plane.
49
+ *
50
+ * @param {readonly (readonly number[])[]} pts The profile (3D).
51
+ * @param {readonly number[]} depth The sweep.
52
+ * @returns {number[][][]} One plane a segment.
53
+ */
54
+ export const ribbon = (pts, depth) => pts.slice(1).map((p, i) => quad(pts[i], sub(p, pts[i]), depth));
55
+
56
+ /* ---------- views ---------- */
57
+
58
+ const c30 = Math.cos(Math.PI / 6);
59
+
60
+ /**
61
+ * The iso view: x runs down-right, y down-left, z up; the viewer looks along −(1, 1, 1). `p` projects a 3D point,
62
+ * `base(X, Y)` is a point on the floor that projects to (X, Y), `dir` the way toward the viewer.
63
+ */
64
+ export const ISO = { p: ([x, y, z]) => [(x - y) * c30, (x + y) / 2 - z], base: (X, Y) => { const a = X / c30, b = 2 * Y; return [(a + b) / 2, (b - a) / 2, 0]; }, dir: [1, 1, 1] };
65
+
66
+ /**
67
+ * An oblique view: the front (x right, z up) in true shape, depth y swept up and to the right at `deg`, shortened by k.
68
+ *
69
+ * @param {number} [k=0.5] How much the depth is shortened.
70
+ * @param {number} [deg=45] The sweep's angle.
71
+ * @returns {import('./planes.js').PlaneView} The view.
72
+ */
73
+ export const OBL = (k = 0.5, deg = 45) => {
74
+ const c = k * Math.cos(rad(deg)), s = k * Math.sin(rad(deg));
75
+ return { p: ([x, y, z]) => [x + y * c, -(z + y * s)], base: (X, Y) => [X, 0, -Y], dir: [c, -1, s] };
76
+ };
77
+
78
+ const centroid = (r) => {
79
+ let A = 0, x = 0, y = 0;
80
+ r.forEach((p, i) => { const q = r[(i + 1) % r.length], w = p[0] * q[1] - q[0] * p[1]; A += w; x += (p[0] + q[0]) * w; y += (p[1] + q[1]) * w; });
81
+ return [x / (3 * A), y / (3 * A)];
82
+ };
83
+ /** How near the viewer plane `pl` is at the screen point s (larger is nearer). */
84
+ const nearness = (pl, view, s) => { const n = cross(sub(pl[1], pl[0]), sub(pl[2], pl[0])), b = view.base(...s); return dot(sub(pl[0], b), n) / dot(view.dir, n); };
85
+
86
+ /**
87
+ * Planes (convex 3D polygons, or lists of convex pieces of one plane) drawn as one mark: projected, fitted to 448 and
88
+ * centred, put in depth order (where two overlap, the one nearer at the middle of the overlap goes in front) and laid
89
+ * back to front with the gap g, each cutting a mitred gap round itself out of what is behind. A plane seen edge-on is
90
+ * dropped, and so are slivers thinner than 9 once drawn.
91
+ *
92
+ * @param {readonly any[]} planes The planes.
93
+ * @param {import('./planes.js').PlaneView} view The view (ISO, or OBL()).
94
+ * @param {number} g The gap.
95
+ * @returns {import('../geometry/types.js').Shape} The mark.
96
+ */
97
+ export const planeScene = (planes, view, g) => {
98
+ // a plane is one convex polygon, or several convex pieces of one plane (a wall with a door cut): drawn as one
99
+ let list = planes.map((pl) => (typeof pl[0][0] === 'number' ? [pl] : pl)).map((ps) => ({ pl: ps[0], q: ps.map((p) => p.map(view.p)) })).filter((o) => Math.abs(o.q.reduce((s, r) => s + area2(r), 0)) > 1e-4);
100
+ const xs = list.flatMap((o) => o.q.flat().map((p) => p[0])), ys = list.flatMap((o) => o.q.flat().map((p) => p[1]));
101
+ const k = 448 / Math.max(Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys)), cx = (Math.max(...xs) + Math.min(...xs)) / 2, cy = (Math.max(...ys) + Math.min(...ys)) / 2;
102
+ // a goes behind b when they overlap and b is nearer at the centre of the overlap
103
+ const behind = list.map(() => new Set());
104
+ list.forEach((a, i) => list.forEach((b, j) => {
105
+ if (j <= i) return; const o = I(a.q.map((r) => [r]), b.q.map((r) => [r]));
106
+ if (!o.length) return; const r = o.map((pg) => pg[0]).sort((p, q) => Math.abs(area2(q)) - Math.abs(area2(p)))[0]; if (Math.abs(area2(r)) < 1e-5) return;
107
+ const s = centroid(r), na = nearness(a.pl, view, s), nb = nearness(b.pl, view, s); if (na < nb) behind[j].add(i); else behind[i].add(j);
108
+ }));
109
+ const done = new Set(), order = [];
110
+ while (order.length < list.length) { const i = list.findIndex((_, i) => !done.has(i) && [...behind[i]].every((j) => done.has(j))); const at = i < 0 ? list.findIndex((_, i) => !done.has(i)) : i; done.add(at); order.push(at); }
111
+ const fitted = (r) => r.map(([x, y]) => [C + (x - cx) * k, C + (y - cy) * k]);
112
+ return dropThin(order.reduce((scene, i) => {
113
+ const ps = list[i].q.map(fitted);
114
+ return U(D(scene, ...ps.map((r) => poly(mitre(r, g)))), ...ps.map(poly));
115
+ }, []), 9);
116
+ };
117
+
118
+ /**
119
+ * A mark from planes in a view, as a build `(g) => shape`.
120
+ *
121
+ * @param {readonly any[]} planes The planes.
122
+ * @param {import('./planes.js').PlaneView} [view=ISO] The view.
123
+ * @returns {(g: number) => import('../geometry/types.js').Shape} The build.
124
+ */
125
+ export const planeMark = (planes, view = ISO) => (g) => planeScene(planes, view, g);
126
+
127
+ /**
128
+ * A plane letter: every stroke ([a, b] points, a across, b up) a strip swept up and to the right in oblique depth d, so
129
+ * a stem shows its side and a bar its top, and each corner is a fold. At 30° the bars and stems are both chunky; a
130
+ * diagonal near the sweep's own angle thins out.
131
+ *
132
+ * @param {readonly (readonly (readonly number[])[])[]} strokes The strokes.
133
+ * @param {number} [deg=30] The sweep's angle.
134
+ * @param {number} [d=0.9] The depth.
135
+ * @param {number} [k=0.7] The oblique shortening.
136
+ * @returns {(g: number) => import('../geometry/types.js').Shape} The build.
137
+ */
138
+ export const planeGlyph = (strokes, deg = 30, d = 0.9, k = 0.7) => planeMark(strokes.flatMap((pts) => ribbon(pts.map(([a, b]) => [a, 0, b]), [0, d, 0])), OBL(k, deg));
@@ -0,0 +1,43 @@
1
+ import type { Geom, Polygon, Shape } from '../geometry/types.js';
2
+ import type { Piece } from '../parts/iso.js';
3
+
4
+ /** The runs of '#' in each row: [c0, c1, row], c1 exclusive. */
5
+ export function runs(rows: readonly string[]): number[][];
6
+ /** A sprite on the iso grid's right-hand face, as boxes. */
7
+ export function voxRight(rows: readonly string[], d?: number, x0?: number): Piece[];
8
+ /** A sprite on the iso grid's left-hand face, as boxes. */
9
+ export function voxLeft(rows: readonly string[], d?: number, y0?: number): Piece[];
10
+ /** A plan on the ground raised h, as boxes. */
11
+ export function voxPlan(rows: readonly string[], h?: number, z0?: number): Piece[];
12
+ /** Unit cubes (or boxes of size s) as boxes. */
13
+ export function voxCubes(list: readonly (readonly number[])[], s?: number): Piece[];
14
+ /** Plans stacked bottom up, each h tall. */
15
+ export function voxLayers(layers: readonly (readonly string[])[], h?: number): Piece[];
16
+ /** A pixel sprite as one solid at pitch p. */
17
+ export function pixRuns(rows: readonly string[], p: number, x0?: number, y0?: number): Shape;
18
+ /** A pixel sprite with one stepped depth behind it, centred, longest side 448. */
19
+ export function pixDepth(rows: readonly string[], o?: { dir?: number[]; k?: number; g?: number; min?: number }): Shape;
20
+ /** A rectangle [x, y, w, h] grown squarely by g. */
21
+ export function moat(r: readonly number[], g: number): Polygon;
22
+ /** A convex polygon grown by g, corners mitred. */
23
+ export function mitre(pts: readonly (readonly number[])[], g: number): number[][];
24
+ /** Slabs laid in order, each cut free of the ones before by a square gap. */
25
+ export function slabs(list: readonly (readonly number[] | readonly (readonly number[])[])[], g?: number): Shape;
26
+ /** A unit-grid shape scaled by k and moved. */
27
+ export function onGrid<T extends Geom>(shape: T, k: number, x?: number, y?: number): T;
28
+ /** The pieces of a mark thinner than t dropped. */
29
+ export function dropThin(shape: Shape, t?: number): Shape;
30
+ /** The wall letters: a 5-row block alphabet, one cube a '#'. */
31
+ export const WALL_GLYPH: Record<string, string[]>;
32
+ /** A D with stepped, face-joined corners. */
33
+ export const D_ROUND: string[];
34
+ /** A four-wide S. */
35
+ export const S_WIDE: string[];
36
+ /** A four-wide L. */
37
+ export const L_WIDE: string[];
38
+ /** A five-wide M, its middle stem half height. */
39
+ export const M_HALF: string[];
40
+ /** Two letters on the two walls of a corner, as isoPieces() pieces. */
41
+ export function cornerPieces(a: string | readonly string[], b: string | readonly string[], inside?: boolean, o?: { depth?: number; join?: boolean }): number[][][];
42
+ /** A corner monogram, drawn with an exact 22 gap. */
43
+ export function cornerMark(pair: string | readonly (string | readonly string[])[], inside?: boolean, o?: { depth?: number; join?: boolean }): () => Shape;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Iso voxels, pixel depth, brutalist slabs and corner monograms, for the parts' scene() and the kit's booleans.
3
+ *
4
+ * - Voxels: rows of text ('#' a cell, the format of PIX7 and the pixel animals) turned into scene() boxes on one plane of
5
+ * the iso grid, each row's runs merged into one box, so a sprite is a handful of pieces and its faces in one plane merge.
6
+ * - Pixel depth: a front-facing pixel sprite with ONE oblique depth step behind it, the depth in whole pixels so the
7
+ * sides step with the sprite instead of breaking into specks.
8
+ * - Slabs: rectangles laid one over another, each cut free of what's under it by a square gap (crisp corners, unlike
9
+ * grown()).
10
+ * - Corner monograms: two initials as one-cube-deep walls meeting at a corner, drawn with isoPieces().
11
+ * From the Logomarks tools (blockparts.mjs, batch 179; corner monograms batch 181, reused by 198 and 228).
12
+ */
13
+ import { U, D, poly, rect, move, mapPts, isoPieces } from '../geometry/index.js';
14
+ import { box, exact } from '../parts/index.js';
15
+
16
+ /* ---------- voxels ---------- */
17
+
18
+ /**
19
+ * The runs of '#' (or 'X') in each row: [[c0, c1, r], …], c1 exclusive.
20
+ *
21
+ * @param {readonly string[]} rows The rows.
22
+ * @returns {number[][]} The runs.
23
+ */
24
+ export const runs = (rows) => rows.flatMap((row, r) => [...row.matchAll(/[#X]+/g)].map((m) => [m.index, m.index + m[0].length, r]));
25
+
26
+ /**
27
+ * A sprite standing on the iso grid's right-hand face (+x, the face that runs up to the right), read left to right:
28
+ * column c at y = w − c, row r at z = h − r (row 0 on top), pushed back `d` along x from x0.
29
+ *
30
+ * @param {readonly string[]} rows The sprite.
31
+ * @param {number} [d=1] Its depth.
32
+ * @param {number} [x0=0] Where it starts along x.
33
+ * @returns {import('../parts/iso.js').Piece[]} One box a run, for scene().
34
+ */
35
+ export const voxRight = (rows, d = 1, x0 = 0) => {
36
+ const w = Math.max(...rows.map((r) => r.length)), h = rows.length;
37
+ return runs(rows).map(([c0, c1, r]) => box([x0, w - c1, h - 1 - r], [x0 + d, w - c0, h - r]));
38
+ };
39
+
40
+ /**
41
+ * A sprite on the left-hand face (+y, running down to the right): column c at x = c, pushed back `d` along y from y0.
42
+ *
43
+ * @param {readonly string[]} rows The sprite.
44
+ * @param {number} [d=1] Its depth.
45
+ * @param {number} [y0=0] Where it starts along y.
46
+ * @returns {import('../parts/iso.js').Piece[]} One box a run.
47
+ */
48
+ export const voxLeft = (rows, d = 1, y0 = 0) => {
49
+ const h = rows.length;
50
+ return runs(rows).map(([c0, c1, r]) => box([c0, y0, h - 1 - r], [c1, y0 + d, h - r]));
51
+ };
52
+
53
+ /**
54
+ * A plan lying on the ground, raised `h` from z0: column c at x = c, row r at y = r.
55
+ *
56
+ * @param {readonly string[]} rows The plan.
57
+ * @param {number} [h=1] Its height.
58
+ * @param {number} [z0=0] Its floor.
59
+ * @returns {import('../parts/iso.js').Piece[]} One box a run.
60
+ */
61
+ export const voxPlan = (rows, h = 1, z0 = 0) => runs(rows).map(([c0, c1, r]) => box([c0, r, z0], [c1, r + 1, z0 + h]));
62
+
63
+ /**
64
+ * Unit cubes [[i, j, k], …] (or boxes of size s) as scene() boxes: for voxel solids built by rule rather than rows.
65
+ *
66
+ * @param {readonly (readonly number[])[]} list The cubes.
67
+ * @param {number} [s=1] Their size.
68
+ * @returns {import('../parts/iso.js').Piece[]} One box each.
69
+ */
70
+ export const voxCubes = (list, s = 1) => list.map(([i, j, k]) => box([i * s, j * s, k * s], [(i + 1) * s, (j + 1) * s, (k + 1) * s]));
71
+
72
+ /**
73
+ * Layers of plans stacked bottom up (layers[0] the ground), each `h` tall: a voxel solid drawn floor by floor.
74
+ *
75
+ * @param {readonly (readonly string[])[]} layers The plans, bottom up.
76
+ * @param {number} [h=1] Each layer's height.
77
+ * @returns {import('../parts/iso.js').Piece[]} The boxes.
78
+ */
79
+ export const voxLayers = (layers, h = 1) => layers.flatMap((rows, k) => voxPlan(rows, h, k * h));
80
+
81
+ /* ---------- pixel depth ---------- */
82
+
83
+ /**
84
+ * A pixel sprite as one solid on the 512 grid: pitch p, top-left (x0, y0), each run a rect (overlapping by 0.01 as
85
+ * pixels() does).
86
+ *
87
+ * @param {readonly string[]} rows The sprite.
88
+ * @param {number} p The pitch.
89
+ * @param {number} [x0=0] Its left.
90
+ * @param {number} [y0=0] Its top.
91
+ * @returns {import('../geometry/types.js').Shape} The sprite.
92
+ */
93
+ export const pixRuns = (rows, p, x0 = 0, y0 = 0) => U(...runs(rows).map(([c0, c1, r]) => rect(x0 + c0 * p, y0 + r * p - 0.01, (c1 - c0) * p, p + 0.02)));
94
+
95
+ /**
96
+ * A pixel sprite with one step of depth behind it: the sprite's cells copied up to `k` pixels along `dir` (±1, ±1:
97
+ * up-right is [1, -1]), so the depth is the sprite's own stepped outline, never a 45° wedge, then the face cut free of
98
+ * it by a square gap g. Worked in cells, so two kinds of scrap are dropped before drawing: a depth cell squeezed between
99
+ * two face cells (the depth seen through a one-pixel notch: the two gaps leave a tab about 11 wide), and depth pieces of
100
+ * less than `min` pixels (beside a one-pixel step the gap leaves a lone square, a speck at 32 px), so the depth shows
101
+ * only along runs of two pixels or more. The cut is the face grown by g with every squeezed notch filled first, so the
102
+ * depth stops flush across a notch.
103
+ *
104
+ * @param {readonly string[]} rows The sprite.
105
+ * @param {{ dir?: number[], k?: number, g?: number, min?: number }} [o] The depth's direction ([1, -1]), its length in
106
+ * pixels (1), the gap (22) and the smallest depth piece in pixels (2).
107
+ * @returns {import('../geometry/types.js').Shape} The mark, centred, its longest side 448.
108
+ */
109
+ export const pixDepth = (rows, { dir = [1, -1], k = 1, g = 22, min = 2 } = {}) => {
110
+ const w = Math.max(...rows.map((r) => r.length)), h = rows.length, p = 448 / Math.max(w + k, h + k);
111
+ const face = new Set(runs(rows).flatMap(([c0, c1, r]) => [...Array(c1 - c0)].map((_, q) => `${c0 + q},${r}`))), has = (i, j) => face.has(`${i},${j}`);
112
+ const deep = new Set();
113
+ for (const key of face) { const [i, j] = key.split(',').map(Number); for (let a = 0; a <= k; a++) for (let b = 0; b <= k; b++) { const c = `${i + a * dir[0]},${j + b * dir[1]}`; if (!face.has(c)) deep.add(c); } }
114
+ const squeezed = new Set([...deep].filter((key) => { const [i, j] = key.split(',').map(Number); return (has(i - 1, j) && has(i + 1, j)) || (has(i, j - 1) && has(i, j + 1)); }));
115
+ for (const key of squeezed) deep.delete(key);
116
+ const cells = [...face, ...deep].map((c) => c.split(',').map(Number)), i0 = Math.min(...cells.map((c) => c[0])), j0 = Math.min(...cells.map((c) => c[1]));
117
+ const W = Math.max(...cells.map((c) => c[0])) - i0 + 1, H = Math.max(...cells.map((c) => c[1])) - j0 + 1, x0 = 256 - (W * p) / 2, y0 = 256 - (H * p) / 2;
118
+ const draw = (set) => (set.size ? U(...[...set].map((c) => { const [i, j] = c.split(',').map(Number); return rect(x0 + (i - i0) * p - 0.01, y0 + (j - j0) * p - 0.01, p + 0.02, p + 0.02); })) : []);
119
+ // the cut is the face grown by g, with every squeezed notch filled first, so the depth stops flush across a notch
120
+ const front = draw(face), full = U(front, draw(squeezed)), cut = U(full, ...[[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]].map(([a, b]) => move(full, a * g, b * g)));
121
+ return U(D(draw(deep), cut).filter((pg) => area(pg) > min * p * (p - g)), front);
122
+ };
123
+
124
+ /** A polygon's area (its outer ring less its holes). */
125
+ const area = (pg) => pg.reduce((s, ring, i) => { let A = 0; for (let q = 0; q < ring.length; q++) { const [x1, y1] = ring[q], [x2, y2] = ring[(q + 1) % ring.length]; A += x1 * y2 - x2 * y1; } return s + (i ? -1 : 1) * Math.abs(A) / 2; }, 0);
126
+
127
+ /* ---------- slabs ---------- */
128
+
129
+ /**
130
+ * A rectangle [x, y, w, h] grown squarely by g on every side (a slab's moat: crisp corners).
131
+ *
132
+ * @param {readonly number[]} r The rectangle [x, y, w, h].
133
+ * @param {number} g The gap.
134
+ * @returns {import('../geometry/types.js').Polygon} The moat.
135
+ */
136
+ export const moat = ([x, y, w, h], g) => rect(x - g, y - g, w + 2 * g, h + 2 * g);
137
+
138
+ /**
139
+ * A convex polygon grown by g with mitred corners (a slanted slab's moat), either winding.
140
+ *
141
+ * @param {readonly (readonly number[])[]} pts The polygon's corners.
142
+ * @param {number} g How far.
143
+ * @returns {number[][]} The grown corners.
144
+ */
145
+ export const mitre = (pts, g) => {
146
+ const n = pts.length, A = pts.reduce((s, p, i) => s + p[0] * pts[(i + 1) % n][1] - pts[(i + 1) % n][0] * p[1], 0), sg = Math.sign(A) || 1;
147
+ const off = pts.map((p, i) => { const q = pts[(i + 1) % n], dx = q[0] - p[0], dy = q[1] - p[1], L = Math.hypot(dx, dy); return [dy / L * sg, -dx / L * sg]; });
148
+ return pts.map((p, i) => { const a = off[(i - 1 + n) % n], b = off[i], s = 1 + a[0] * b[0] + a[1] * b[1]; return [p[0] + (g * (a[0] + b[0])) / s, p[1] + (g * (a[1] + b[1])) / s]; });
149
+ };
150
+
151
+ /**
152
+ * Slabs laid in order, each over the ones before and cut free of them by a square gap g. Each slab is a rect
153
+ * [x, y, w, h] or a convex polygon [[x, y], …]; the later slab keeps its edge, the earlier gives way.
154
+ *
155
+ * @param {readonly any[]} list The slabs, back to front.
156
+ * @param {number} [g=22] The gap.
157
+ * @returns {import('../geometry/types.js').Shape} The mark.
158
+ */
159
+ export const slabs = (list, g = 22) => list.reduce((scene, s) => {
160
+ const isRect = typeof s[0] === 'number', shape = isRect ? rect(...s) : poly(s);
161
+ return U(D(scene, isRect ? moat(s, g) : poly(mitre(s, g))), shape);
162
+ }, []);
163
+
164
+ /**
165
+ * Scales a shape drawn on a unit grid by k and moves it by (x, y).
166
+ *
167
+ * @param {import('../geometry/types.js').Geom} shape The shape.
168
+ * @param {number} k The scale.
169
+ * @param {number} [x=0] Along x.
170
+ * @param {number} [y=0] Along y.
171
+ * @returns {import('../geometry/types.js').Geom} The shape, placed.
172
+ */
173
+ export const onGrid = (shape, k, x = 0, y = 0) => mapPts(shape, ([a, b]) => [x + a * k, y + b * k]);
174
+
175
+ /**
176
+ * Drops the pieces of a drawn mark thinner than `t` (2 × area ÷ perimeter): the wedge of an inner face that shows only
177
+ * as a sliver in the corner of a one-cube counter or notch. Its place reads as gap, as it would at 32 px anyway.
178
+ *
179
+ * @param {import('../geometry/types.js').Shape} shape The mark.
180
+ * @param {number} [t=12] The thinnest piece kept.
181
+ * @returns {import('../geometry/types.js').Shape} The mark without its slivers.
182
+ */
183
+ export const dropThin = (shape, t = 12) => shape.filter((pg) => { const A = area(pg), P = pg[0].reduce((s, q, i, r) => s + Math.hypot(r[(i + 1) % r.length][0] - q[0], r[(i + 1) % r.length][1] - q[1]), 0); return (2 * A) / P >= t; });
184
+
185
+ /* ---------- corner monograms ---------- */
186
+
187
+ /**
188
+ * The wall letters: a 5-row block alphabet, one cube per '#'. Every letter is drawn for a wall one cube deep: stems and
189
+ * bars one cube wide, counters one cube, square corners only (a lone diagonal cube turns to crumbs in iso), and a full
190
+ * stem wherever the letter can share the corner pillar (A, M, H, J, U on the left; B, E, F, H, L, M, P, U on the right).
191
+ */
192
+ export const WALL_GLYPH = {
193
+ A: ['###', '#.#', '###', '#.#', '#.#'], B: ['##.', '#.#', '###', '#.#', '###'], C: ['###', '#..', '#..', '#..', '###'],
194
+ D: ['##.', '#.#', '#.#', '#.#', '##.'], E: ['###', '#..', '###', '#..', '###'], F: ['###', '#..', '###', '#..', '#..'],
195
+ G: ['###', '#..', '#.#', '#.#', '###'], H: ['#.#', '#.#', '###', '#.#', '#.#'], J: ['..#', '..#', '..#', '#.#', '###'],
196
+ K: ['#.#', '#.#', '##.', '#.#', '#.#'], L: ['#..', '#..', '#..', '#..', '###'], M: ['#####', '#.#.#', '#.#.#', '#.#.#', '#.#.#'],
197
+ P: ['###', '#.#', '###', '#..', '#..'], R: ['###', '#.#', '##.', '#.#', '#.#'], S: ['###', '#..', '###', '..#', '###'],
198
+ T: ['###', '.#.', '.#.', '.#.', '.#.'], U: ['#.#', '#.#', '#.#', '#.#', '###'], W: ['#.#.#', '#.#.#', '#.#.#', '#.#.#', '#####'],
199
+ };
200
+
201
+ /**
202
+ * A D rounded off with stepped, face-joined corners (WALL_GLYPH's D, its corner cubes taken off, touches its right stem
203
+ * only at an edge). Drop the step crumbs in the counter with dropThin(…, 20).
204
+ */
205
+ export const D_ROUND = ['####.', '#..##', '#...#', '#..##', '####.'];
206
+
207
+ /** A four-wide S, to balance a five-wide D or M. */
208
+ export const S_WIDE = ['####', '#...', '####', '...#', '####'];
209
+
210
+ /** A four-wide L, to balance a five-wide D or M. */
211
+ export const L_WIDE = ['#...', '#...', '#...', '#...', '####'];
212
+
213
+ /** An M five wide with its middle stem half height (five full-height strokes stripe at 32 px). */
214
+ export const M_HALF = ['#####', '#.#.#', '#.#.#', '#...#', '#...#'];
215
+
216
+ /** The cubes of a glyph as [column, height] pairs (height 0 at the base row). */
217
+ const cellsOf = (rows) => rows.flatMap((row, r) => [...row].flatMap((ch, c) => (ch === '#' ? [[c, rows.length - 1 - r]] : [])));
218
+ /** Whether a glyph's first (side 0) or last (side 1) column is a full stem. */
219
+ const fullStem = (rows, side) => rows.every((row) => row[side ? row.length - 1 : 0] === '#');
220
+
221
+ /**
222
+ * Two letters on the two walls of a corner, as isoPieces() pieces: the first letter reads towards the corner, the second
223
+ * away from it. `inside` false is the outside corner of a block (the first wall's front face is the left-hand face on
224
+ * screen, the corner nearest the viewer); true is the inside corner of a room (two walls seen from inside, the corner
225
+ * farthest away, the letters standing in an open book). A shared stem makes the pair one piece, the stem the corner
226
+ * pillar; otherwise each wall stops short of the corner and is a piece of its own, so the two letters stand a gap apart
227
+ * round it.
228
+ *
229
+ * @param {string | readonly string[]} a The first letter (a WALL_GLYPH key or rows of its own).
230
+ * @param {string | readonly string[]} b The second.
231
+ * @param {boolean} [inside=false] An inside corner.
232
+ * @param {{ depth?: number, join?: boolean }} [o] The walls' depth in cubes (1, each wall thickening behind its front
233
+ * face) and `join`, to make the pair one piece round the corner even without two full stems.
234
+ * @returns {number[][][]} The pieces (lists of cube positions).
235
+ */
236
+ export const cornerPieces = (a, b, inside = false, { depth = 1, join = false } = {}) => {
237
+ const A = typeof a === 'string' ? WALL_GLYPH[a] : a, B = typeof b === 'string' ? WALL_GLYPH[b] : b, w1 = A[0].length, w2 = B[0].length;
238
+ const shared = join || (fullStem(A, 1) && fullStem(B, 0)), s = shared ? 0 : 1, e = depth - 1, deep = (f) => [...Array(depth)].map((_, t) => f(t));
239
+ const left = cellsOf(A).flatMap(([c, k]) => deep((t) => (inside ? [-t, w1 - 1 - c + s, k] : [c, w2 - 1 + e - t, k])));
240
+ const right = cellsOf(B).flatMap(([c, k]) => deep((t) => (inside ? [c + s, -t, k] : [w1 - 1 + s + e - t, w2 - 1 + e - c, k])));
241
+ if (!shared) return [left, right];
242
+ const seen = new Set(left.map((p) => p.join()));
243
+ return [[...left, ...right.filter((p) => !seen.has(p.join()))]];
244
+ };
245
+
246
+ /**
247
+ * A corner monogram for a pair ("JB", or two glyphs as [rows, rows]), drawn with an exact 22 gap once fitted: a mark
248
+ * function for a batch's table.
249
+ *
250
+ * @param {string | readonly (string | readonly string[])[]} pair The two letters.
251
+ * @param {boolean} [inside] An inside corner.
252
+ * @param {{ depth?: number, join?: boolean }} [o] cornerPieces()'s options.
253
+ * @returns {() => import('../geometry/types.js').Shape} The mark.
254
+ */
255
+ export const cornerMark = (pair, inside, o) => () => exact((g) => isoPieces(cornerPieces(pair[0], pair[1], inside, o), 60, g));
@@ -0,0 +1,31 @@
1
+ import type { Geom, Point, Polygon, Shape } from '../geometry/types.js';
2
+
3
+ /** A broad nib: its width, thickness (the hairline) and rising angle in degrees. */
4
+ export interface Nib {
5
+ w?: number;
6
+ th?: number;
7
+ deg?: number;
8
+ }
9
+
10
+ /** The face's nib (width W, hairline TH, angle DEG) and its cap height CAP. */
11
+ export const NIB: { W: number; TH: number; DEG: number; CAP: number };
12
+ /** The nib swept along a polyline. */
13
+ export function nib(pts: readonly (readonly number[])[], o?: Nib): Shape;
14
+ /** Several nib strokes united. */
15
+ export function nibs(list: readonly (readonly (readonly number[])[])[], o?: Nib): Shape;
16
+ /** A square on its corner, s from corner to corner, centred on (x, y). */
17
+ export function lozenge(x: number, y: number, s: number): Polygon;
18
+ /** A cut g wide along the line through a and b, run far past both. */
19
+ export function cutLine(a: readonly number[], b: readonly number[], g: number): Polygon;
20
+ /** A slit g wide down an upright at x from above the cap to depth y. */
21
+ export function splitHead(x: number, y: number, g: number): Polygon;
22
+ /** A slit g wide up an upright at x from below the base to height y. */
23
+ export function splitFoot(x: number, y: number, g: number): Polygon;
24
+ /** An upright from y0 to y1 with a flag head and a lozenge foot, as a polyline for nib(). */
25
+ export function minim(x: number, y0: number, y1: number, o?: { head?: boolean; foot?: boolean; d?: number }): Point[];
26
+ /** A lozenge parted from the ink by the gap g. */
27
+ export function parted(ink: Geom, z: Polygon, g: number): Shape;
28
+ /** The blackletter capitals, each drawn with the gap g on the 440 cap. */
29
+ export const BLACKLETTERS: Record<string, (g: number) => Shape>;
30
+ /** A blackletter capital drawn with the gap g, centred on (256, 256). */
31
+ export function blackLetter(k: string, g: number): Shape;