@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,1042 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Iso objects: the parts an iso object is built from, on top of the parts' solid engine, scene(). x runs to the front
|
|
3
|
+
* right, y to the front left, z up; a face shows when it looks along (1, 1, 1): the +x face (right), the +y face (left)
|
|
4
|
+
* and the top. Each part is one solid (its pieces merge where they share a plane); scene() parts every two parts, and
|
|
5
|
+
* every two faces that meet at a crease, by the gap. Openings (windows, slots, holes) are cut afterwards on the same
|
|
6
|
+
* fitted projection, so they stay true to the faces they sit on. Cut-outs square to the viewer (flat()) keep their true
|
|
7
|
+
* shape and are drawn by isoFlat() as a face and the band of their depth.
|
|
8
|
+
* From the Logomarks tools (isoparts.mjs, batch 180; shops, roofs, lathes and flat cut-outs added up to batch 285).
|
|
9
|
+
*/
|
|
10
|
+
import { U, D, I, poly, grow, mapPts } from '../geometry/index.js';
|
|
11
|
+
import { scene, isoFit, loft, side, front, hull, ngonPts, box, piece, cyl, glyph } from '../parts/index.js';
|
|
12
|
+
import { cross3 } from '../util/vec.js';
|
|
13
|
+
|
|
14
|
+
export { cross3 } from '../util/vec.js';
|
|
15
|
+
|
|
16
|
+
/* ---------- the projection and cuts ---------- */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The projection scene() fits round these parts, so a cut lands on their faces.
|
|
20
|
+
*
|
|
21
|
+
* @param {readonly any[]} parts The parts (each a piece or a list of pieces).
|
|
22
|
+
* @returns {import('../parts/iso.js').FittedProjection} The fitted projection.
|
|
23
|
+
*/
|
|
24
|
+
export const fitOf = (parts) => isoFit(parts.flat().flatMap((q) => q.faces.flatMap((f) => f.pts)));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The house gap in model units for these parts: a slot k wide on a face, parallel to one of its edges, shows k·u·cos 30°.
|
|
28
|
+
*
|
|
29
|
+
* @param {readonly any[]} parts The parts.
|
|
30
|
+
* @param {number} g The gap on the canvas.
|
|
31
|
+
* @returns {number} The gap in model units.
|
|
32
|
+
*/
|
|
33
|
+
export const gapIn = (parts, g) => g / (fitOf(parts).u * Math.cos(Math.PI / 6));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 2D points [a, b] on the face of axis `ax` at `at`, as 3D points: on an x face a is y, on a y face a is x, and b is z;
|
|
37
|
+
* on the top, a is x and b is y.
|
|
38
|
+
*
|
|
39
|
+
* @param {'x' | 'y' | 'z'} ax The face's axis.
|
|
40
|
+
* @param {number} at Where the face sits along it.
|
|
41
|
+
* @param {readonly (readonly number[])[]} pts The 2D points.
|
|
42
|
+
* @returns {import('../geometry/types.js').Point3[]} The 3D points.
|
|
43
|
+
*/
|
|
44
|
+
export const onFace = (ax, at, pts) => pts.map(([a, b]) => (ax === 'x' ? [at, a, b] : ax === 'y' ? [a, at, b] : [a, b, at]));
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 2D points [a, b] on any plane: o + a·u + b·v (a sloped face: a keypad, a windscreen).
|
|
48
|
+
*
|
|
49
|
+
* @param {readonly number[]} o The plane's origin.
|
|
50
|
+
* @param {readonly number[]} u Its first axis.
|
|
51
|
+
* @param {readonly number[]} v Its second axis.
|
|
52
|
+
* @param {readonly (readonly number[])[]} pts The 2D points.
|
|
53
|
+
* @returns {import('../geometry/types.js').Point3[]} The 3D points.
|
|
54
|
+
*/
|
|
55
|
+
export const onPlane = (o, u, v, pts) => pts.map(([a, b]) => [0, 1, 2].map((i) => o[i] + a * u[i] + b * v[i]));
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Rectangles [a0, b0, a1, b1] on a face, as 3D quads.
|
|
59
|
+
*
|
|
60
|
+
* @param {'x' | 'y' | 'z'} ax The face's axis.
|
|
61
|
+
* @param {number} at Where the face sits.
|
|
62
|
+
* @param {readonly (readonly number[])[]} rects The rectangles.
|
|
63
|
+
* @returns {import('../geometry/types.js').Point3[][]} One quad each.
|
|
64
|
+
*/
|
|
65
|
+
export const quads = (ax, at, rects) => rects.map(([a0, b0, a1, b1]) => onFace(ax, at, [[a0, b0], [a1, b0], [a1, b1], [a0, b1]]));
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A disc of radius r at (a, b) on a face.
|
|
69
|
+
*
|
|
70
|
+
* @param {'x' | 'y' | 'z'} ax The face's axis.
|
|
71
|
+
* @param {number} at Where the face sits.
|
|
72
|
+
* @param {number} a The centre's first coordinate on the face.
|
|
73
|
+
* @param {number} b Its second.
|
|
74
|
+
* @param {number} r The radius.
|
|
75
|
+
* @param {number} [n=72] Sides.
|
|
76
|
+
* @returns {import('../geometry/types.js').Point3[]} The disc's outline.
|
|
77
|
+
*/
|
|
78
|
+
export const discOn = (ax, at, a, b, r, n = 72) => onFace(ax, at, ngonPts(a, b, r, n));
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A 2D shape (drawn about the origin in canvas units, y down) as 3D outlines on an iso face, to cut with isoCut(): on
|
|
82
|
+
* the +y face a runs along x, on the +x face along −y (so it reads left to right either way).
|
|
83
|
+
*
|
|
84
|
+
* @param {'x' | 'y'} ax The face's axis.
|
|
85
|
+
* @param {number} at Where the face sits.
|
|
86
|
+
* @param {import('../geometry/types.js').Shape} shape The shape.
|
|
87
|
+
* @param {number} f Model units per canvas unit.
|
|
88
|
+
* @param {number} ca The shape's middle on the face, first coordinate.
|
|
89
|
+
* @param {number} cb Its second.
|
|
90
|
+
* @returns {import('../geometry/types.js').Point3[][]} The outer ring of each polygon, on the face.
|
|
91
|
+
*/
|
|
92
|
+
export const shapeOn = (ax, at, shape, f, ca, cb) => shape.map((pg) => onFace(ax, at, pg[0].map(([x, y]) => [ax === 'x' ? ca - x * f : ca + x * f, cb - y * f])));
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Draws iso parts, cuts the 3D outlines in `holes` through them, then puts back the outlines in `keep` where something
|
|
96
|
+
* was drawn (the island inside a ring cut: cut a disc, keep a smaller one).
|
|
97
|
+
*
|
|
98
|
+
* @param {readonly any[]} parts The parts.
|
|
99
|
+
* @param {number} g The gap.
|
|
100
|
+
* @param {readonly (readonly number[])[][]} [holes=[]] 3D outlines to cut.
|
|
101
|
+
* @param {readonly (readonly number[])[][]} [keep=[]] 3D outlines to put back.
|
|
102
|
+
* @returns {import('../geometry/types.js').Shape} The mark.
|
|
103
|
+
*/
|
|
104
|
+
export const isoCut = (parts, g, holes = [], keep = []) => {
|
|
105
|
+
const P = fitOf(parts), pr = (q) => poly(q.map((p) => P(...p))), s = scene(parts, g);
|
|
106
|
+
const cut = holes.length ? D(s, ...holes.map(pr)) : s;
|
|
107
|
+
return keep.length ? U(cut, ...keep.map((q) => I(s, pr(q)))) : cut;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/* ---------- solids ---------- */
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A cylinder along x (centre y, z), radius r, from x0 to x1 (the parts' cyl() stands upright).
|
|
114
|
+
*
|
|
115
|
+
* @param {number} cy The axis's y.
|
|
116
|
+
* @param {number} cz Its z.
|
|
117
|
+
* @param {number} r The radius.
|
|
118
|
+
* @param {number} x0 From.
|
|
119
|
+
* @param {number} x1 To.
|
|
120
|
+
* @param {number} [n=72] Sides.
|
|
121
|
+
* @returns {import('../parts/iso.js').Piece} The cylinder.
|
|
122
|
+
*/
|
|
123
|
+
export const cylX = (cy, cz, r, x0, x1, n = 72) => front(ngonPts(cy, cz, r, n), x0, x1, true);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A cylinder along y (centre x, z), radius r, from y0 to y1.
|
|
127
|
+
*
|
|
128
|
+
* @param {number} cx The axis's x.
|
|
129
|
+
* @param {number} cz Its z.
|
|
130
|
+
* @param {number} r The radius.
|
|
131
|
+
* @param {number} y0 From.
|
|
132
|
+
* @param {number} y1 To.
|
|
133
|
+
* @param {number} [n=72] Sides.
|
|
134
|
+
* @returns {import('../parts/iso.js').Piece} The cylinder.
|
|
135
|
+
*/
|
|
136
|
+
export const cylY = (cx, cz, r, y0, y1, n = 72) => side(ngonPts(cx, cz, r, n), y0, y1, true);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* An upright frustum (a cup, a cone, a pot): radius r0 at z0 to r1 at z1, n sides turned deg (n = 4, deg = 45: a square
|
|
140
|
+
* pot). Over 12 sides its side merges into one face.
|
|
141
|
+
*
|
|
142
|
+
* @param {number} cx The centre's x.
|
|
143
|
+
* @param {number} cy Its y.
|
|
144
|
+
* @param {number} r0 The radius at z0.
|
|
145
|
+
* @param {number} r1 The radius at z1.
|
|
146
|
+
* @param {number} z0 The bottom.
|
|
147
|
+
* @param {number} z1 The top.
|
|
148
|
+
* @param {number} [n=72] Sides.
|
|
149
|
+
* @param {number} [deg=0] The turn of the first corner.
|
|
150
|
+
* @returns {import('../parts/iso.js').Piece} The frustum.
|
|
151
|
+
*/
|
|
152
|
+
export const frustum = (cx, cy, r0, r1, z0, z1, n = 72, deg = 0) =>
|
|
153
|
+
loft(ngonPts(cx, cy, r0, n, deg).map(([x, y]) => [x, y, z0]), ngonPts(cx, cy, r1, n, deg).map(([x, y]) => [x, y, z1]), n > 12);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A slab hinged on a line along y: t thick and h long, standing up from (x, z) and leaning back by `deg` (towards −x),
|
|
157
|
+
* from y0 to y1. A laptop's screen, a box's lid.
|
|
158
|
+
*
|
|
159
|
+
* @param {number} x The hinge's x.
|
|
160
|
+
* @param {number} z Its z.
|
|
161
|
+
* @param {number} y0 From.
|
|
162
|
+
* @param {number} y1 To.
|
|
163
|
+
* @param {number} h The slab's length.
|
|
164
|
+
* @param {number} t Its thickness.
|
|
165
|
+
* @param {number} deg How far it leans back.
|
|
166
|
+
* @returns {import('../parts/iso.js').Piece} The slab.
|
|
167
|
+
*/
|
|
168
|
+
export const leaning = (x, z, y0, y1, h, t, deg) => {
|
|
169
|
+
const a = (deg * Math.PI) / 180, ux = -Math.sin(a), uz = Math.cos(a), nx = Math.cos(a), nz = Math.sin(a);
|
|
170
|
+
const base = [[x, z], [x + nx * t, z + nz * t]], top = base.map(([px, pz]) => [px + ux * h, pz + uz * h]);
|
|
171
|
+
return side([base[0], base[1], top[1], top[0]], y0, y1);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A flap hinged on the edge p0–p1 (3D), folded out along the horizontal unit vector `out` by `deg` from upright, L long
|
|
176
|
+
* and t thick: a carton's flaps. Past 45° its inner face looks at the viewer; short of it, it's nearly edge-on and
|
|
177
|
+
* vanishes.
|
|
178
|
+
*
|
|
179
|
+
* @param {readonly number[]} p0 One end of the hinge.
|
|
180
|
+
* @param {readonly number[]} p1 The other.
|
|
181
|
+
* @param {readonly number[]} out The way it folds out (a horizontal unit vector).
|
|
182
|
+
* @param {number} deg The fold, from upright.
|
|
183
|
+
* @param {number} L Its length.
|
|
184
|
+
* @param {number} t Its thickness.
|
|
185
|
+
* @returns {import('../parts/iso.js').Piece} The flap.
|
|
186
|
+
*/
|
|
187
|
+
export const flap = (p0, p1, out, deg, L, t) => {
|
|
188
|
+
const a = (deg * Math.PI) / 180, d = [out[0] * Math.sin(a), out[1] * Math.sin(a), Math.cos(a)];
|
|
189
|
+
const m = [out[0] * Math.cos(a), out[1] * Math.cos(a), -Math.sin(a)], add = (p, v, k) => p.map((c, i) => c + v[i] * k);
|
|
190
|
+
const q = [p0, p1, add(p1, d, L), add(p0, d, L)];
|
|
191
|
+
return hull([...q, ...q.map((p) => add(p, m, t))]);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A bar between two 3D points with a square section w (a tonearm, a strut), as a hull.
|
|
196
|
+
*
|
|
197
|
+
* @param {readonly number[]} a One end.
|
|
198
|
+
* @param {readonly number[]} b The other.
|
|
199
|
+
* @param {number} w The section's side.
|
|
200
|
+
* @returns {import('../parts/iso.js').Piece} The bar.
|
|
201
|
+
*/
|
|
202
|
+
export const rod = (a, b, w) => {
|
|
203
|
+
const h = w / 2, c = [[-h, -h], [h, -h], [h, h], [-h, h]];
|
|
204
|
+
return hull([a, b].flatMap(([x, y, z]) => c.flatMap(([u, v]) => [[x + u, y + v, z - h], [x + u, y + v, z + h]])));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A round bar from a to b (3D points), radius r: two circles square to its axis, lofted smooth (a pestle, a handle at an
|
|
209
|
+
* angle). Unlike rod() it has no facets, so it reads as one clean shape.
|
|
210
|
+
*
|
|
211
|
+
* @param {readonly number[]} a One end.
|
|
212
|
+
* @param {readonly number[]} b The other.
|
|
213
|
+
* @param {number} r The radius.
|
|
214
|
+
* @param {number} [n=48] Sides.
|
|
215
|
+
* @returns {import('../parts/iso.js').Piece} The tube.
|
|
216
|
+
*/
|
|
217
|
+
export const tube = (a, b, r, n = 48) => {
|
|
218
|
+
const ax = b.map((v, i) => v - a[i]), L = Math.hypot(...ax), d = ax.map((v) => v / L);
|
|
219
|
+
const t = Math.abs(d[2]) < 0.9 ? [0, 0, 1] : [1, 0, 0], u = [d[1] * t[2] - d[2] * t[1], d[2] * t[0] - d[0] * t[2], d[0] * t[1] - d[1] * t[0]];
|
|
220
|
+
const ul = Math.hypot(...u), e1 = u.map((v) => v / ul), e2 = [d[1] * e1[2] - d[2] * e1[1], d[2] * e1[0] - d[0] * e1[2], d[0] * e1[1] - d[1] * e1[0]];
|
|
221
|
+
const ring = (c) => [...Array(n)].map((_, i) => { const q = (2 * Math.PI * i) / n; return c.map((v, k) => v + r * (Math.cos(q) * e1[k] + Math.sin(q) * e2[k])); });
|
|
222
|
+
return loft(ring(a), ring(b), true);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A vehicle's wheels on its visible (+y) side: one cylinder along y per x in `xs`, radius r at height z, standing proud
|
|
227
|
+
* of the side at y = W.
|
|
228
|
+
*
|
|
229
|
+
* @param {readonly number[]} xs Each wheel's x.
|
|
230
|
+
* @param {number} W The side's y.
|
|
231
|
+
* @param {number} [r=0.36] The radius.
|
|
232
|
+
* @param {number} [z=0.34] The axle's height.
|
|
233
|
+
* @returns {import('../parts/iso.js').Piece[]} The wheels.
|
|
234
|
+
*/
|
|
235
|
+
export const wheels = (xs, W, r = 0.36, z = 0.34) => xs.map((x) => cylY(x, z, r, W - 0.25, W + 0.06));
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A rectangle w wide from z0 to z1 on the tangent plane at the front of an upright cylinder (the point facing the
|
|
239
|
+
* viewer, along (1, 1)): a postbox's slot, a label on a can.
|
|
240
|
+
*
|
|
241
|
+
* @param {number} cx The cylinder's x.
|
|
242
|
+
* @param {number} cy Its y.
|
|
243
|
+
* @param {number} r Its radius.
|
|
244
|
+
* @param {number} w The rectangle's width.
|
|
245
|
+
* @param {number} z0 Its bottom.
|
|
246
|
+
* @param {number} z1 Its top.
|
|
247
|
+
* @returns {import('../geometry/types.js').Point3[]} The rectangle, as a 3D quad.
|
|
248
|
+
*/
|
|
249
|
+
export const cylFront = (cx, cy, r, w, z0, z1) => {
|
|
250
|
+
const d = Math.SQRT1_2, c = [cx + r * d, cy + r * d], h = (w / 2) * d;
|
|
251
|
+
return [[c[0] + h, c[1] - h, z0], [c[0] - h, c[1] + h, z0], [c[0] - h, c[1] + h, z1], [c[0] + h, c[1] - h, z1]];
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A ball (an apple, a heap of popcorn) as one part: rings of frustums whose sides merge into one face, so it draws as
|
|
256
|
+
* one round solid. `squash` flattens it (z radius = r · squash); it runs from latitude `from` (−90 = the bottom) to
|
|
257
|
+
* `to`. Stop `to` short of 90 (about 60) so the top is a flat disc, not a dot: a dot on a dome reads as a nipple.
|
|
258
|
+
*
|
|
259
|
+
* @param {number} cx The centre's x.
|
|
260
|
+
* @param {number} cy Its y.
|
|
261
|
+
* @param {number} cz Its z.
|
|
262
|
+
* @param {number} r The radius.
|
|
263
|
+
* @param {{ squash?: number, rings?: number, from?: number, to?: number }} [o] The squash (1), the rings (8) and the
|
|
264
|
+
* latitudes it runs between (−90 to 90).
|
|
265
|
+
* @returns {import('../parts/iso.js').Piece[]} The ball, one part.
|
|
266
|
+
*/
|
|
267
|
+
export const ball = (cx, cy, cz, r, { squash = 1, rings = 8, from = -90, to = 90 } = {}) => [...Array(rings)].map((_, i) => {
|
|
268
|
+
const a0 = ((from + ((to - from) * i) / rings) * Math.PI) / 180, a1 = ((from + ((to - from) * (i + 1)) / rings) * Math.PI) / 180;
|
|
269
|
+
return frustum(cx, cy, Math.max(r * Math.cos(a0), 0.02), Math.max(r * Math.cos(a1), 0.02), cz + r * squash * Math.sin(a0), cz + r * squash * Math.sin(a1));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A band along an arc in a side profile (x, z), centre (cx, cz), radii r0–r1 from angle a0 to a1 (degrees), pushed along
|
|
274
|
+
* y from y0 to y1, as convex pieces of one part: a taco shell, a melon's rind.
|
|
275
|
+
*
|
|
276
|
+
* @param {number} cx The arc's centre, x.
|
|
277
|
+
* @param {number} cz Its z.
|
|
278
|
+
* @param {number} r0 The inner radius.
|
|
279
|
+
* @param {number} r1 The outer radius.
|
|
280
|
+
* @param {number} a0 From (degrees).
|
|
281
|
+
* @param {number} a1 To.
|
|
282
|
+
* @param {number} y0 The band's near side.
|
|
283
|
+
* @param {number} y1 Its far side.
|
|
284
|
+
* @param {number} [n=16] Pieces.
|
|
285
|
+
* @returns {import('../parts/iso.js').Piece[]} The band.
|
|
286
|
+
*/
|
|
287
|
+
export const arcSide = (cx, cz, r0, r1, a0, a1, y0, y1, n = 16) => [...Array(n)].map((_, i) => {
|
|
288
|
+
const t0 = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180, t1 = ((a0 + ((a1 - a0) * (i + 1)) / n) * Math.PI) / 180;
|
|
289
|
+
const P = (r, t) => [cx + r * Math.cos(t), cz + r * Math.sin(t)];
|
|
290
|
+
return side([P(r0, t0), P(r1, t0), P(r1, t1), P(r0, t1)], y0, y1);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* arcSide()'s band pushed along x instead (its profile in (y, z)).
|
|
295
|
+
*
|
|
296
|
+
* @param {number} cy The arc's centre, y.
|
|
297
|
+
* @param {number} cz Its z.
|
|
298
|
+
* @param {number} r0 The inner radius.
|
|
299
|
+
* @param {number} r1 The outer radius.
|
|
300
|
+
* @param {number} a0 From (degrees).
|
|
301
|
+
* @param {number} a1 To.
|
|
302
|
+
* @param {number} x0 The band's one side.
|
|
303
|
+
* @param {number} x1 Its other.
|
|
304
|
+
* @param {number} [n=16] Pieces.
|
|
305
|
+
* @returns {import('../parts/iso.js').Piece[]} The band.
|
|
306
|
+
*/
|
|
307
|
+
export const arcFront = (cy, cz, r0, r1, a0, a1, x0, x1, n = 16) => [...Array(n)].map((_, i) => {
|
|
308
|
+
const t0 = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180, t1 = ((a0 + ((a1 - a0) * (i + 1)) / n) * Math.PI) / 180;
|
|
309
|
+
const P = (r, t) => [cy + r * Math.cos(t), cz + r * Math.sin(t)];
|
|
310
|
+
return front([P(r0, t0), P(r1, t0), P(r1, t1), P(r0, t1)], x0, x1);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/* ---------- roofs ---------- */
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* A gable roof from z0 rising by `rise` over the box x0–x1 × y0–y1, its ridge along y (the gable ends face ±y).
|
|
317
|
+
*
|
|
318
|
+
* @param {number} x0 The box's x from.
|
|
319
|
+
* @param {number} x1 Its x to.
|
|
320
|
+
* @param {number} y0 Its y from.
|
|
321
|
+
* @param {number} y1 Its y to.
|
|
322
|
+
* @param {number} z0 The eaves.
|
|
323
|
+
* @param {number} rise The ridge's height above them.
|
|
324
|
+
* @returns {import('../parts/iso.js').Piece} The roof.
|
|
325
|
+
*/
|
|
326
|
+
export const roofY = (x0, x1, y0, y1, z0, rise) => side([[x0, z0], [x1, z0], [(x0 + x1) / 2, z0 + rise]], y0, y1);
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A gable roof with its ridge along x (the gable ends face ±x).
|
|
330
|
+
*
|
|
331
|
+
* @param {number} x0 The box's x from.
|
|
332
|
+
* @param {number} x1 Its x to.
|
|
333
|
+
* @param {number} y0 Its y from.
|
|
334
|
+
* @param {number} y1 Its y to.
|
|
335
|
+
* @param {number} z0 The eaves.
|
|
336
|
+
* @param {number} rise The ridge's height above them.
|
|
337
|
+
* @returns {import('../parts/iso.js').Piece} The roof.
|
|
338
|
+
*/
|
|
339
|
+
export const roofX = (x0, x1, y0, y1, z0, rise) => front([[y0, z0], [y1, z0], [(y0 + y1) / 2, z0 + rise]], x0, x1);
|
|
340
|
+
|
|
341
|
+
/* ---------- shopfronts ---------- */
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A shop: a block W along x, D along y, H tall, and its shopfront cut in the +y face: a window over the left half and a
|
|
345
|
+
* door at the right, both scaled to the block (`win: false` leaves only the door; `door` moves it, as a fraction of W).
|
|
346
|
+
* Jambs, sill and head are at least about 0.4 wide, so no strip of wall is thinner than the gap once it's cut.
|
|
347
|
+
*
|
|
348
|
+
* @param {number} W The block's length along x.
|
|
349
|
+
* @param {number} D Its depth along y.
|
|
350
|
+
* @param {number} H Its height.
|
|
351
|
+
* @param {{ win?: boolean, door?: number | null, dh?: number }} [o] The window (true), the door's place (null: at the
|
|
352
|
+
* right) and its height as a fraction of H (0.62).
|
|
353
|
+
* @returns {{ parts: import('../parts/iso.js').Piece[], cuts: import('../geometry/types.js').Point3[][], c: import('../geometry/types.js').Point3 }}
|
|
354
|
+
* The block, its shopfront cuts and the roof's centre (where the trade feature goes).
|
|
355
|
+
*/
|
|
356
|
+
export const shop = (W, D, H, { win = true, door = null, dh = 0.62 } = {}) => {
|
|
357
|
+
// jambs, sill and head at least ~0.4 wide, so no strip of wall is thinner than the gap once it's cut
|
|
358
|
+
const dw = Math.min(0.5, W * 0.2), d0 = door == null ? W - dw - 0.42 : W * door, top = Math.min(H * dh, H - 0.42, 1.05), sill = Math.max(0.36, H * 0.22);
|
|
359
|
+
const cuts = quads('y', D, [...(win && d0 - 0.4 - 0.38 > 0.45 && top - sill > 0.35 ? [[0.38, sill, d0 - 0.4, top]] : []), [d0, 0, d0 + dw, top]]);
|
|
360
|
+
return { parts: [box([0, 0, 0], [W, D, H])], cuts, c: [W / 2, D / 2, H] };
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* A flat profile [[x, z]…] (convex) standing on the roof facing +y, t thick, centred on y = yc.
|
|
365
|
+
*
|
|
366
|
+
* @param {readonly (readonly number[])[]} pts The profile.
|
|
367
|
+
* @param {number} yc Its middle along y.
|
|
368
|
+
* @param {number} [t=0.42] Its thickness.
|
|
369
|
+
* @returns {import('../parts/iso.js').Piece} The sign.
|
|
370
|
+
*/
|
|
371
|
+
export const sign = (pts, yc, t = 0.42) => side(pts, yc - t / 2, yc + t / 2);
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* A disc of radius r standing on edge at (x, z) facing +y, t deep, centred on y = yc.
|
|
375
|
+
*
|
|
376
|
+
* @param {number} x The centre's x.
|
|
377
|
+
* @param {number} z Its z.
|
|
378
|
+
* @param {number} r The radius.
|
|
379
|
+
* @param {number} yc Its middle along y.
|
|
380
|
+
* @param {number} [t=0.42] Its depth.
|
|
381
|
+
* @returns {import('../parts/iso.js').Piece} The disc.
|
|
382
|
+
*/
|
|
383
|
+
export const discY = (x, z, r, yc, t = 0.42) => cylY(x, z, r, yc - t / 2, yc + t / 2);
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* A disc of radius r standing on edge at (y, z) facing +x, t deep, centred on x = xc.
|
|
387
|
+
*
|
|
388
|
+
* @param {number} y The centre's y.
|
|
389
|
+
* @param {number} z Its z.
|
|
390
|
+
* @param {number} r The radius.
|
|
391
|
+
* @param {number} xc Its middle along x.
|
|
392
|
+
* @param {number} [t=0.42] Its depth.
|
|
393
|
+
* @returns {import('../parts/iso.js').Piece} The disc.
|
|
394
|
+
*/
|
|
395
|
+
export const discX = (y, z, r, xc, t = 0.42) => cylX(y, z, r, xc - t / 2, xc + t / 2);
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Scales a 3D point list about o by k.
|
|
399
|
+
*
|
|
400
|
+
* @param {readonly (readonly number[])[]} pts The points.
|
|
401
|
+
* @param {readonly number[]} o The centre.
|
|
402
|
+
* @param {number} k The factor.
|
|
403
|
+
* @returns {number[][]} The scaled points.
|
|
404
|
+
*/
|
|
405
|
+
export const sc3 = (pts, o, k) => pts.map((p) => p.map((v, i) => o[i] + (v - o[i]) * k));
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* A part (a piece or a list of pieces) scaled about o by k: a roof feature drawn bigger, so it holds at 32 px.
|
|
409
|
+
*
|
|
410
|
+
* @param {any} part The part.
|
|
411
|
+
* @param {readonly number[]} o The centre.
|
|
412
|
+
* @param {number} k The factor.
|
|
413
|
+
* @returns {any} The part, scaled (a piece, or a list of pieces, as it came).
|
|
414
|
+
*/
|
|
415
|
+
export const scalePart = (part, o, k) => (Array.isArray(part) ? part.map((q) => scalePart(q, o, k)) : piece(part.faces.map((f) => sc3(f.pts, o, k)), part.faces.map((f) => f.tag)));
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* A shop with its trade feature, as a build `(g) => shape`: `feat(c, W, D, H, k)` returns [parts, cuts, keeps, flats] (k
|
|
419
|
+
* is the gap in model units; flats are flat() cut-outs, drawn by isoFlat()); the shop's own shopfront cuts are added.
|
|
420
|
+
* `o` takes shop()'s options plus `big`, which scales the feature's parts, cuts and flats about the roof's centre.
|
|
421
|
+
*
|
|
422
|
+
* @param {readonly number[]} size The shop's [W, D, H].
|
|
423
|
+
* @param {(c: number[], W: number, D: number, H: number, k: number) => any[]} feat The trade feature.
|
|
424
|
+
* @param {{ win?: boolean, door?: number | null, dh?: number, big?: number }} [o] shop()'s options and the feature's scale.
|
|
425
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
426
|
+
*/
|
|
427
|
+
export const shopMark = ([W, D, H], feat, o = {}) => (g) => {
|
|
428
|
+
const sh = shop(W, D, H, o), big = o.big || 1, fe = (k) => {
|
|
429
|
+
const [p, c = [], kp = []] = feat(sh.c, W, D, H, k);
|
|
430
|
+
return big === 1 ? [p, c, kp] : [p.map((q) => scalePart(q, sh.c, big)), c.map((q) => sc3(q, sh.c, big)), kp.map((q) => sc3(q, sh.c, big))];
|
|
431
|
+
};
|
|
432
|
+
const k = gapIn([...sh.parts, ...fe(0)[0]], g), [parts, cuts, keep] = fe(k / big), fl = (feat(sh.c, W, D, H, k / big)[3] || []).map((f) => scaleFlat(f, sh.c, big));
|
|
433
|
+
return fl.length ? isoFlat([...sh.parts, ...parts], g, fl, [...sh.cuts, ...cuts], keep) : isoCut([...sh.parts, ...parts], g, [...sh.cuts, ...cuts], keep);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
/* ---------- open and tapered boxes ---------- */
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* A box tapering from a w × d footprint at z0 to W × D at z1, centred on (cx, cy): a bin, a hopper, a jar, a lectern.
|
|
440
|
+
*
|
|
441
|
+
* @param {number} cx The centre's x.
|
|
442
|
+
* @param {number} cy Its y.
|
|
443
|
+
* @param {number} w The width (along x) at z0.
|
|
444
|
+
* @param {number} d The depth (along y) at z0.
|
|
445
|
+
* @param {number} W The width at z1.
|
|
446
|
+
* @param {number} D The depth at z1.
|
|
447
|
+
* @param {number} z0 The bottom.
|
|
448
|
+
* @param {number} z1 The top.
|
|
449
|
+
* @returns {import('../parts/iso.js').Piece} The box.
|
|
450
|
+
*/
|
|
451
|
+
export const taper = (cx, cy, w, d, W, D, z0, z1) => loft([[-w, -d], [w, -d], [w, d], [-w, d]].map(([x, y]) => [cx + x / 2, cy + y / 2, z0]), [[-W, -D], [W, -D], [W, D], [-W, D]].map(([x, y]) => [cx + x / 2, cy + y / 2, z1]));
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* An open box's walls (one part): floor and four walls t thick, so its inside shows: a tray, a crib, a planter.
|
|
455
|
+
*
|
|
456
|
+
* @param {number} x0 The box's x from.
|
|
457
|
+
* @param {number} y0 Its y from.
|
|
458
|
+
* @param {number} x1 Its x to.
|
|
459
|
+
* @param {number} y1 Its y to.
|
|
460
|
+
* @param {number} h The walls' height.
|
|
461
|
+
* @param {number} t Their thickness (and the floor's).
|
|
462
|
+
* @param {number} [z0=0] The floor's bottom.
|
|
463
|
+
* @returns {import('../parts/iso.js').Piece[]} The floor and walls.
|
|
464
|
+
*/
|
|
465
|
+
export const tray = (x0, y0, x1, y1, h, t, z0 = 0) => [box([x0, y0, z0], [x1, y1, z0 + t]), box([x0, y0, z0], [x0 + t, y1, z0 + h]), box([x0, y0, z0], [x1, y0 + t, z0 + h]), box([x1 - t, y0, z0], [x1, y1, z0 + h]), box([x0, y1 - t, z0], [x1, y1, z0 + h])];
|
|
466
|
+
|
|
467
|
+
/* ---------- the plane facing the viewer, and lathes ---------- */
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The upright plane's right-hand axis: it faces the viewer square on and runs to the right on the canvas (along
|
|
471
|
+
* (1, −1, 0)). A profile drawn in it keeps its true shape: no iso shear, unlike one on the +x or +y face.
|
|
472
|
+
*/
|
|
473
|
+
export const FU = [Math.SQRT1_2, -Math.SQRT1_2, 0];
|
|
474
|
+
|
|
475
|
+
/** The facing plane's normal: it points out at the viewer (along (1, 1, 0)). */
|
|
476
|
+
export const FN = [Math.SQRT1_2, Math.SQRT1_2, 0];
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 2D points [a, b] (a to the right, b up) on the facing plane through o, pushed s towards the viewer, as 3D points.
|
|
480
|
+
*
|
|
481
|
+
* @param {readonly number[]} o A point on the plane.
|
|
482
|
+
* @param {readonly (readonly number[])[]} pts The 2D points.
|
|
483
|
+
* @param {number} [s=0] How far towards the viewer.
|
|
484
|
+
* @returns {import('../geometry/types.js').Point3[]} The 3D points.
|
|
485
|
+
*/
|
|
486
|
+
export const onFacing = (o, pts, s = 0) => onPlane([o[0] + s * FN[0], o[1] + s * FN[1], o[2]], FU, [0, 0, 1], pts);
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* A convex profile on the facing plane through o, t deep (half each side of o): a cut-out solid whose face keeps its
|
|
490
|
+
* shape and whose upward edges show below it as its depth (a wooden toy, a donut on edge). `smooth` merges its sides.
|
|
491
|
+
*
|
|
492
|
+
* @param {readonly number[]} o A point on the plane.
|
|
493
|
+
* @param {readonly (readonly number[])[]} pts The profile (convex, a to the right, b up).
|
|
494
|
+
* @param {number} t The depth.
|
|
495
|
+
* @param {boolean} [smooth=false] Merge the sides into one face (an arc).
|
|
496
|
+
* @returns {import('../parts/iso.js').Piece} The solid.
|
|
497
|
+
*/
|
|
498
|
+
export const facing = (o, pts, t, smooth = false) => loft(onFacing(o, pts, -t / 2), onFacing(o, pts, t / 2), smooth);
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* A band between radii r0 and r1 round (a, b) on the facing plane, from angle a0 to a1 (degrees), t deep, as convex
|
|
502
|
+
* pieces of one part. scene() can't order pieces whose boxes overlap, so a full ring draws side faces over its front:
|
|
503
|
+
* for a ring or any outline that isn't convex, use flat() instead.
|
|
504
|
+
*
|
|
505
|
+
* @param {readonly number[]} o A point on the plane.
|
|
506
|
+
* @param {readonly number[]} c The band's centre [a, b] on the plane.
|
|
507
|
+
* @param {number} r0 The inner radius.
|
|
508
|
+
* @param {number} r1 The outer radius.
|
|
509
|
+
* @param {number} a0 From (degrees).
|
|
510
|
+
* @param {number} a1 To.
|
|
511
|
+
* @param {number} t The depth.
|
|
512
|
+
* @param {number} [n=48] Pieces.
|
|
513
|
+
* @returns {import('../parts/iso.js').Piece[]} The band.
|
|
514
|
+
*/
|
|
515
|
+
export const facingArc = (o, [a, b], r0, r1, a0, a1, t, n = 48) => [...Array(n)].map((_, i) => {
|
|
516
|
+
const t0 = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180, t1 = ((a0 + ((a1 - a0) * (i + 1)) / n) * Math.PI) / 180;
|
|
517
|
+
const P = (r, u) => [a + r * Math.cos(u), b + r * Math.sin(u)];
|
|
518
|
+
return facing(o, [P(r0, t0), P(r1, t0), P(r1, t1), P(r0, t1)], t, true);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* A lathe round (cx, cy): stacked frustums through the profile [[r, z]…], bottom to top, their sides merged into one
|
|
523
|
+
* face (a bowling pin, a vase).
|
|
524
|
+
*
|
|
525
|
+
* @param {number} cx The axis's x.
|
|
526
|
+
* @param {number} cy Its y.
|
|
527
|
+
* @param {readonly (readonly number[])[]} prof The profile, [radius, height] bottom to top.
|
|
528
|
+
* @param {number} [n=72] Sides.
|
|
529
|
+
* @returns {import('../parts/iso.js').Piece[]} The lathe, one part.
|
|
530
|
+
*/
|
|
531
|
+
export const lathe = (cx, cy, prof, n = 72) => prof.slice(1).map(([r, z], i) => frustum(cx, cy, prof[i][0], r, prof[i][1], z, n));
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* A cut-out square to the viewer: its outline the convex 2D polys in `outer` less the polys in `holes` (a, b on the
|
|
535
|
+
* facing plane through o, as onFacing()), t deep. Drawn by isoFlat() as one face and the band of its depth above it, so
|
|
536
|
+
* a profile that isn't convex (a rocking horse, a donut) keeps its true shape without scene() having to order its
|
|
537
|
+
* pieces. flat() subtracts its holes by orientation: pass outlines anticlockwise (ccw(), flatCcw()).
|
|
538
|
+
*
|
|
539
|
+
* @param {readonly number[]} o A point on the plane.
|
|
540
|
+
* @param {readonly (readonly (readonly number[])[])[]} outer Convex 2D outlines, their union the cut-out.
|
|
541
|
+
* @param {number} t The depth.
|
|
542
|
+
* @param {readonly (readonly (readonly number[])[])[]} [holes=[]] 2D outlines to take out of it.
|
|
543
|
+
* @returns {import('./objects.js').Flat} The cut-out.
|
|
544
|
+
*/
|
|
545
|
+
export const flat = (o, outer, t, holes = []) => ({ outer: outer.map((q) => onFacing(o, q, t / 2)), holes: holes.map((q) => onFacing(o, q, t / 2)), back: FN.map((v) => -v * t) });
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* A 2D outline turned anticlockwise (a, b with b up): flat() subtracts its holes by orientation, and slot2()'s quads run
|
|
549
|
+
* clockwise, so a slot passed as a hole (or an outline) must be turned first.
|
|
550
|
+
*
|
|
551
|
+
* @param {readonly (readonly number[])[]} pts The outline.
|
|
552
|
+
* @returns {number[][]} It, anticlockwise.
|
|
553
|
+
*/
|
|
554
|
+
export const ccw = (pts) => {
|
|
555
|
+
let a = 0;
|
|
556
|
+
pts.forEach(([x0, y0], i) => { const [x1, y1] = pts[(i + 1) % pts.length]; a += x0 * y1 - x1 * y0; });
|
|
557
|
+
return a < 0 ? pts.slice().reverse() : pts;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* flat() with every outline and hole turned anticlockwise first.
|
|
562
|
+
*
|
|
563
|
+
* @param {readonly number[]} o A point on the plane.
|
|
564
|
+
* @param {readonly (readonly (readonly number[])[])[]} outer Convex 2D outlines.
|
|
565
|
+
* @param {number} t The depth.
|
|
566
|
+
* @param {readonly (readonly (readonly number[])[])[]} [holes=[]] 2D outlines to take out.
|
|
567
|
+
* @returns {import('./objects.js').Flat} The cut-out.
|
|
568
|
+
*/
|
|
569
|
+
export const flatCcw = (o, outer, t, holes = []) => flat(o, outer.map(ccw), t, holes.map(ccw));
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* A flat() scaled about o by k (a roof feature drawn bigger).
|
|
573
|
+
*
|
|
574
|
+
* @param {import('./objects.js').Flat} f The cut-out.
|
|
575
|
+
* @param {readonly number[]} o The centre.
|
|
576
|
+
* @param {number} k The factor.
|
|
577
|
+
* @returns {import('./objects.js').Flat} The scaled cut-out.
|
|
578
|
+
*/
|
|
579
|
+
export const scaleFlat = (f, o, k) => {
|
|
580
|
+
const sc = (q) => q.map((p) => p.map((v, i) => o[i] + (v - o[i]) * k));
|
|
581
|
+
return { outer: f.outer.map(sc), holes: f.holes.map(sc), back: f.back.map((v) => v * k) };
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* A 2D shape swept along the screen vector v: the shape, its copy moved by v, and every edge's sweep between them (a
|
|
586
|
+
* cut-out's depth; an oblique solid).
|
|
587
|
+
*
|
|
588
|
+
* @param {import('../geometry/types.js').Shape} sh The shape.
|
|
589
|
+
* @param {readonly number[]} v The sweep.
|
|
590
|
+
* @returns {import('../geometry/types.js').Shape} The swept shape.
|
|
591
|
+
*/
|
|
592
|
+
export const sweep = (sh, v) => U(sh, mapPts(sh, ([x, y]) => [x + v[0], y + v[1]]), ...sh.flatMap((pg) => pg.flatMap((r) => r.map((p, i) => { const q = r[(i + 1) % r.length]; return poly([p, q, [q[0] + v[0], q[1] + v[1]], [p[0] + v[0], p[1] + v[1]]]); }))));
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* isoCut() with flat() cut-outs in front of the parts. Each is put in the scene as its convex pieces (so the parts
|
|
596
|
+
* behind it are hidden and parted from it by the gap), then that region is redrawn: the front face F, and the band of
|
|
597
|
+
* its depth showing above it (F swept back along its depth, less F), each shrunk by g/2 so they sit a gap apart. Holes
|
|
598
|
+
* show what's behind the band; cut-outs later in the list stand in front of earlier ones, a gap clear of them.
|
|
599
|
+
*
|
|
600
|
+
* @param {readonly any[]} parts The parts.
|
|
601
|
+
* @param {number} g The gap.
|
|
602
|
+
* @param {readonly import('./objects.js').Flat[]} flats The cut-outs, back to front.
|
|
603
|
+
* @param {readonly (readonly number[])[][]} [holes=[]] 3D outlines to cut.
|
|
604
|
+
* @param {readonly (readonly number[])[][]} [keep=[]] 3D outlines to put back.
|
|
605
|
+
* @returns {import('../geometry/types.js').Shape} The mark.
|
|
606
|
+
*/
|
|
607
|
+
export const isoFlat = (parts, g, flats, holes = [], keep = []) => {
|
|
608
|
+
const add = (q, v) => q.map((p) => p.map((c, i) => c + v[i]));
|
|
609
|
+
const all = [...parts, ...flats.flatMap((f) => f.outer.map((q) => loft(add(q, f.back), q)))], P = fitOf(all), pr = (q) => poly(q.map((p) => P(...p)));
|
|
610
|
+
let base = scene(all, g), ink = []; // flats later in the list stand in front of earlier ones, a gap clear of them
|
|
611
|
+
for (const f of flats) {
|
|
612
|
+
const o = P(0, 0, 0), b = P(...f.back), v = [b[0] - o[0], b[1] - o[1]], full = U(...f.outer.map(pr)), F = f.holes.length ? D(full, ...f.holes.map(pr)) : full, E = sweep(full, v);
|
|
613
|
+
base = D(base, E); ink = U(D(ink, grow(E, g / 2)), grow(F, -g / 2), grow(D(sweep(F, v), F), -g / 2));
|
|
614
|
+
}
|
|
615
|
+
const s = U(base, ink), cut = holes.length ? D(s, ...holes.map(pr)) : s;
|
|
616
|
+
return keep.length ? U(cut, ...keep.map((q) => I(s, pr(q)))) : cut;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
/* ---------- 2D outlines and turned solids ---------- */
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Points round an ellipse (a, b), radii ra and rb, from d0 to d1 degrees (2D, both ends included).
|
|
623
|
+
*
|
|
624
|
+
* @param {number} a The centre's first coordinate.
|
|
625
|
+
* @param {number} b Its second.
|
|
626
|
+
* @param {number} ra The radius along a.
|
|
627
|
+
* @param {number} rb The radius along b.
|
|
628
|
+
* @param {number} d0 From (degrees).
|
|
629
|
+
* @param {number} d1 To.
|
|
630
|
+
* @param {number} [n=24] Steps.
|
|
631
|
+
* @returns {import('../geometry/types.js').Point[]} n + 1 points.
|
|
632
|
+
*/
|
|
633
|
+
export const arcPts = (a, b, ra, rb, d0, d1, n = 24) => [...Array(n + 1)].map((_, i) => { const t = ((d0 + ((d1 - d0) * i) / n) * Math.PI) / 180; return [a + ra * Math.cos(t), b + rb * Math.sin(t)]; });
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* A slot w wide along the segment p–q (2D), as a quad.
|
|
637
|
+
*
|
|
638
|
+
* @param {readonly number[]} p One end.
|
|
639
|
+
* @param {readonly number[]} q The other.
|
|
640
|
+
* @param {number} w The width.
|
|
641
|
+
* @returns {import('../geometry/types.js').Point[]} The quad.
|
|
642
|
+
*/
|
|
643
|
+
export const slot2 = ([x0, y0], [x1, y1], w) => {
|
|
644
|
+
const L = Math.hypot(x1 - x0, y1 - y0), nx = (-(y1 - y0) / L) * (w / 2), ny = ((x1 - x0) / L) * (w / 2);
|
|
645
|
+
return [[x0 + nx, y0 + ny], [x1 + nx, y1 + ny], [x1 - nx, y1 - ny], [x0 - nx, y0 - ny]];
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* A ring of gap w wide at radius r round (a, b) on a face, as small quads (a ring is no simple outline, so it's cut in
|
|
650
|
+
* pieces).
|
|
651
|
+
*
|
|
652
|
+
* @param {number} a The centre's first coordinate.
|
|
653
|
+
* @param {number} b Its second.
|
|
654
|
+
* @param {number} r The radius.
|
|
655
|
+
* @param {number} w The width.
|
|
656
|
+
* @param {number} [n=48] Quads.
|
|
657
|
+
* @returns {import('../geometry/types.js').Point[][]} The quads.
|
|
658
|
+
*/
|
|
659
|
+
export const ring2 = (a, b, r, w, n = 48) => [...Array(n)].map((_, i) => {
|
|
660
|
+
const t0 = (2 * Math.PI * i) / n, t1 = (2 * Math.PI * (i + 1)) / n, P = (rr, t) => [a + rr * Math.cos(t), b + rr * Math.sin(t)];
|
|
661
|
+
return [P(r - w / 2, t0), P(r + w / 2, t0), P(r + w / 2, t1), P(r - w / 2, t1)];
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* A part re-aimed and moved: each point p goes to T + p.x·e1 + p.y·e2 + p.z·e3, so a lathe built up z round the origin
|
|
666
|
+
* lies along e3 from T.
|
|
667
|
+
*
|
|
668
|
+
* @param {any} part The part (a piece or a list of pieces).
|
|
669
|
+
* @param {readonly number[]} T Where the origin goes.
|
|
670
|
+
* @param {readonly number[]} e1 Where x points.
|
|
671
|
+
* @param {readonly number[]} e2 Where y points.
|
|
672
|
+
* @param {readonly number[]} e3 Where z points.
|
|
673
|
+
* @returns {any} The part, moved.
|
|
674
|
+
*/
|
|
675
|
+
export const turned = (part, T, e1, e2, e3) => (Array.isArray(part) ? part.map((q) => turned(q, T, e1, e2, e3)) :
|
|
676
|
+
piece(part.faces.map((f) => f.pts.map((p) => [0, 1, 2].map((i) => T[i] + p[0] * e1[i] + p[1] * e2[i] + p[2] * e3[i]))), part.faces.map((f) => f.tag)));
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* A capsule's lathe profile [[r, z]…] up z from 0: a round end of radius r, a straight run L, and a round end again if
|
|
680
|
+
* `both`.
|
|
681
|
+
*
|
|
682
|
+
* @param {number} r The radius.
|
|
683
|
+
* @param {number} L The straight run.
|
|
684
|
+
* @param {boolean} [both=true] Round the top end too.
|
|
685
|
+
* @returns {number[][]} The profile, for lathe().
|
|
686
|
+
*/
|
|
687
|
+
export const capsule = (r, L, both = true) => {
|
|
688
|
+
const cap = (d) => [r * Math.sin((d * Math.PI) / 180), r * (1 - Math.cos((d * Math.PI) / 180))], ds = [12, 30, 48, 66, 90];
|
|
689
|
+
return [...ds.map(cap), ...(both ? ds.slice().reverse().map((d) => { const [rr, h] = cap(d); return [rr, 2 * r + L - h]; }) : [[r, r + L]])];
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* A part built up z from the origin (a lathe, a cylinder) turned so its z axis runs from a towards b: a pole, a tilted
|
|
694
|
+
* mixer drum.
|
|
695
|
+
*
|
|
696
|
+
* @param {any} part The part.
|
|
697
|
+
* @param {readonly number[]} a Where its foot goes.
|
|
698
|
+
* @param {readonly number[]} b The way its z axis points.
|
|
699
|
+
* @returns {any} The part, aimed.
|
|
700
|
+
*/
|
|
701
|
+
export const aimed = (part, a, b) => {
|
|
702
|
+
const d = b.map((v, i) => v - a[i]), L = Math.hypot(...d), e3 = d.map((v) => v / L), h = Math.abs(e3[2]) < 0.9 ? [0, 0, 1] : [1, 0, 0];
|
|
703
|
+
const c = [e3[1] * h[2] - e3[2] * h[1], e3[2] * h[0] - e3[0] * h[2], e3[0] * h[1] - e3[1] * h[0]], n = Math.hypot(...c), e1 = c.map((v) => v / n), e2 = [e3[1] * e1[2] - e3[2] * e1[1], e3[2] * e1[0] - e3[0] * e1[2], e3[0] * e1[1] - e3[1] * e1[0]];
|
|
704
|
+
return turned(part, a, e1, e2, e3);
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* A round pole of radius r from a to b (a straw, a chopstick): a cylinder turned along a–b, its side one face.
|
|
709
|
+
*
|
|
710
|
+
* @param {readonly number[]} a One end.
|
|
711
|
+
* @param {readonly number[]} b The other.
|
|
712
|
+
* @param {number} r The radius.
|
|
713
|
+
* @returns {import('../parts/iso.js').Piece} The pole.
|
|
714
|
+
*/
|
|
715
|
+
export const pole = (a, b, r) => aimed(cyl(0, 0, r, 0, Math.hypot(...b.map((v, i) => v - a[i])), 32), a, b);
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Lies a lathe (built round the origin) along the horizontal unit vector u, its middle at c.
|
|
719
|
+
*
|
|
720
|
+
* @param {any} part The lathe.
|
|
721
|
+
* @param {readonly number[]} c Where its middle goes.
|
|
722
|
+
* @param {readonly number[]} u The way it lies (horizontal, unit).
|
|
723
|
+
* @param {number} len Its length.
|
|
724
|
+
* @returns {any} The part, lying.
|
|
725
|
+
*/
|
|
726
|
+
export const lying = (part, c, u, len) => turned(part, [c[0] - (u[0] * len) / 2, c[1] - (u[1] * len) / 2, c[2]], [0, 0, 1], [-u[1], u[0], 0], [u[0], u[1], 0]);
|
|
727
|
+
|
|
728
|
+
/* ---------- helpers for flat() cut-outs ---------- */
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* The gap in model units for parts plus flat() cut-outs (their depth counts towards the fit).
|
|
732
|
+
*
|
|
733
|
+
* @param {readonly any[]} parts The parts.
|
|
734
|
+
* @param {readonly import('./objects.js').Flat[]} flats The cut-outs.
|
|
735
|
+
* @param {number} g The gap on the canvas.
|
|
736
|
+
* @returns {number} The gap in model units.
|
|
737
|
+
*/
|
|
738
|
+
export const gapWith = (parts, flats, g) => gapIn([...parts, ...flats.flatMap((f) => f.outer.map((q) => loft(q.map((p) => p.map((c, i) => c + f.back[i])), q)))], g);
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* A circle as 2D points (a, b), radius r, n sides.
|
|
742
|
+
*
|
|
743
|
+
* @param {number} a The centre's first coordinate.
|
|
744
|
+
* @param {number} b Its second.
|
|
745
|
+
* @param {number} r The radius.
|
|
746
|
+
* @param {number} [n=48] Sides.
|
|
747
|
+
* @returns {import('../geometry/types.js').Point[]} n points.
|
|
748
|
+
*/
|
|
749
|
+
export const circ = (a, b, r, n = 48) => arcPts(a, b, r, r, 0, 360, n).slice(0, n);
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* A quad along an arm from (cx, cy): angle d (degrees), from radius r0 to r1, w wide (centred on the arm), w1 wide at r1.
|
|
753
|
+
*
|
|
754
|
+
* @param {number} cx The centre's first coordinate.
|
|
755
|
+
* @param {number} cy Its second.
|
|
756
|
+
* @param {number} d The arm's angle (degrees).
|
|
757
|
+
* @param {number} r0 Where it starts.
|
|
758
|
+
* @param {number} r1 Where it ends.
|
|
759
|
+
* @param {number} w Its width at r0.
|
|
760
|
+
* @param {number} [w1=w] Its width at r1.
|
|
761
|
+
* @returns {import('../geometry/types.js').Point[]} The quad.
|
|
762
|
+
*/
|
|
763
|
+
export const arm = (cx, cy, d, r0, r1, w, w1 = w) => {
|
|
764
|
+
const t = (d * Math.PI) / 180, u = [Math.cos(t), Math.sin(t)], n = [-u[1], u[0]];
|
|
765
|
+
return [[cx + u[0] * r0 + (n[0] * w) / 2, cy + u[1] * r0 + (n[1] * w) / 2], [cx + u[0] * r1 + (n[0] * w1) / 2, cy + u[1] * r1 + (n[1] * w1) / 2],
|
|
766
|
+
[cx + u[0] * r1 - (n[0] * w1) / 2, cy + u[1] * r1 - (n[1] * w1) / 2], [cx + u[0] * r0 - (n[0] * w) / 2, cy + u[1] * r0 - (n[1] * w) / 2]];
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
/* ---------- 2D outlines for flat() cut-outs ---------- */
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* 2D points turned deg (anticlockwise, b up) about (ca, cb).
|
|
773
|
+
*
|
|
774
|
+
* @param {readonly (readonly number[])[]} pts The points.
|
|
775
|
+
* @param {number} deg The turn.
|
|
776
|
+
* @param {readonly number[]} [c=[0, 0]] The centre.
|
|
777
|
+
* @returns {import('../geometry/types.js').Point[]} The turned points.
|
|
778
|
+
*/
|
|
779
|
+
export const rot = (pts, deg, [ca, cb] = [0, 0]) => { const t = (deg * Math.PI) / 180, c = Math.cos(t), s = Math.sin(t); return pts.map(([u, v]) => [ca + (u - ca) * c - (v - cb) * s, cb + (u - ca) * s + (v - cb) * c]); };
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* An ellipse round (a, b), radii ra and rb, turned deg (2D points).
|
|
783
|
+
*
|
|
784
|
+
* @param {number} a The centre's first coordinate.
|
|
785
|
+
* @param {number} b Its second.
|
|
786
|
+
* @param {number} ra The radius along a.
|
|
787
|
+
* @param {number} rb The radius along b.
|
|
788
|
+
* @param {number} [deg=0] The turn.
|
|
789
|
+
* @param {number} [n=48] Sides.
|
|
790
|
+
* @returns {import('../geometry/types.js').Point[]} n points.
|
|
791
|
+
*/
|
|
792
|
+
export const ell = (a, b, ra, rb, deg = 0, n = 48) => rot(arcPts(0, 0, ra, rb, 0, 360, n).slice(0, n), deg).map(([u, v]) => [a + u, b + v]);
|
|
793
|
+
|
|
794
|
+
/** The facing plane's vertical foreshortening: a circle drawn with rb = r / SQ projects round. */
|
|
795
|
+
export const SQ = Math.sqrt(2 / 3);
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* A circle on the facing plane that projects round (2D points, for flat()).
|
|
799
|
+
*
|
|
800
|
+
* @param {number} a The centre's first coordinate.
|
|
801
|
+
* @param {number} b Its second.
|
|
802
|
+
* @param {number} r The radius as it shows.
|
|
803
|
+
* @param {number} [n=48] Sides.
|
|
804
|
+
* @returns {import('../geometry/types.js').Point[]} n points.
|
|
805
|
+
*/
|
|
806
|
+
export const rnd = (a, b, r, n = 48) => ell(a, b, r, r / SQ, 0, n);
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* A disc's outline (a full ellipse with ra = rb, 2D points).
|
|
810
|
+
*
|
|
811
|
+
* @param {number} a The centre's first coordinate.
|
|
812
|
+
* @param {number} b Its second.
|
|
813
|
+
* @param {number} r The radius.
|
|
814
|
+
* @param {number} [n=48] Sides.
|
|
815
|
+
* @returns {import('../geometry/types.js').Point[]} n points.
|
|
816
|
+
*/
|
|
817
|
+
export const disc = (a, b, r, n = 48) => ell(a, b, r, r, 0, n);
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* A rectangle a0–a1 × b0–b1 with its corners rounded by r (2D points).
|
|
821
|
+
*
|
|
822
|
+
* @param {number} a0 From, first coordinate.
|
|
823
|
+
* @param {number} b0 From, second.
|
|
824
|
+
* @param {number} a1 To, first coordinate.
|
|
825
|
+
* @param {number} b1 To, second.
|
|
826
|
+
* @param {number} r The corner radius.
|
|
827
|
+
* @param {number} [n=6] Steps a corner.
|
|
828
|
+
* @returns {import('../geometry/types.js').Point[]} The outline.
|
|
829
|
+
*/
|
|
830
|
+
export const rr = (a0, b0, a1, b1, r, n = 6) => [[a1 - r, b0 + r, -90], [a1 - r, b1 - r, 0], [a0 + r, b1 - r, 90], [a0 + r, b0 + r, 180]].flatMap(([a, b, d]) => arcPts(a, b, r, r, d, d + 90, n));
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* An almond from p to q, w wide at its middle (a leaf, a blade; 2D points).
|
|
834
|
+
*
|
|
835
|
+
* @param {readonly number[]} p One tip.
|
|
836
|
+
* @param {readonly number[]} q The other.
|
|
837
|
+
* @param {number} w The width at the middle.
|
|
838
|
+
* @param {number} [n=14] Steps a side.
|
|
839
|
+
* @returns {import('../geometry/types.js').Point[]} The outline.
|
|
840
|
+
*/
|
|
841
|
+
export const lens = ([a0, b0], [a1, b1], w, n = 14) => {
|
|
842
|
+
const L = Math.hypot(a1 - a0, b1 - b0), u = [(a1 - a0) / L, (b1 - b0) / L], nn = [-u[1], u[0]];
|
|
843
|
+
const side = (s) => [...Array(n + 1)].map((_, i) => { const t = i / n, h = s * (w / 2) * Math.sin(Math.PI * t); return [a0 + u[0] * L * t + nn[0] * h, b0 + u[1] * L * t + nn[1] * h]; });
|
|
844
|
+
return [...side(1), ...side(-1).reverse().slice(1, -1)];
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* A slot of gap w wide along a polyline: one slot2() per segment.
|
|
849
|
+
*
|
|
850
|
+
* @param {readonly (readonly number[])[]} pts The polyline.
|
|
851
|
+
* @param {number} w The width.
|
|
852
|
+
* @returns {import('../geometry/types.js').Point[][]} The quads.
|
|
853
|
+
*/
|
|
854
|
+
export const path2 = (pts, w) => pts.slice(0, -1).map((p, i) => slot2(p, pts[i + 1], w));
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* 2D outlines as cuts on the front face of a flat() t deep through o.
|
|
858
|
+
*
|
|
859
|
+
* @param {readonly number[]} o The flat's point on the plane.
|
|
860
|
+
* @param {number} t Its depth.
|
|
861
|
+
* @param {readonly (readonly (readonly number[])[])[]} list The outlines.
|
|
862
|
+
* @returns {import('../geometry/types.js').Point3[][]} The cuts, in 3D.
|
|
863
|
+
*/
|
|
864
|
+
export const flatCuts = (o, t, list) => list.map((q) => onFacing(o, q, t / 2));
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* A roof feature made only of flat() cut-outs (back to front) and cuts on them, as the feat shopMark() takes: fn(o, k)
|
|
868
|
+
* returns { flats, cuts, keep }, o the roof's centre lifted a hair, k the gap in model units.
|
|
869
|
+
*
|
|
870
|
+
* @param {(o: number[], k: number) => { flats: import('./objects.js').Flat[], cuts?: any[], keep?: any[] }} fn The feature.
|
|
871
|
+
* @returns {(c: number[], W: number, D: number, H: number, k: number) => any[]} The feat for shopMark().
|
|
872
|
+
*/
|
|
873
|
+
export const roofFlats = (fn) => (c, W, D, H, k) => { const o = [c[0], c[1], c[2] + 0.04], { flats, cuts = [], keep = [] } = fn(o, k); return [[], cuts, keep, flats]; };
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* A spiral slot w wide on a face: from radius r0 to r1 over `turns`, round (a, b), as 2D quads.
|
|
877
|
+
*
|
|
878
|
+
* @param {number} a The centre's first coordinate.
|
|
879
|
+
* @param {number} b Its second.
|
|
880
|
+
* @param {number} r0 The starting radius.
|
|
881
|
+
* @param {number} r1 The end radius.
|
|
882
|
+
* @param {number} turns How many turns.
|
|
883
|
+
* @param {number} w The slot's width.
|
|
884
|
+
* @param {number} [n=80] Quads.
|
|
885
|
+
* @returns {import('../geometry/types.js').Point[][]} The quads.
|
|
886
|
+
*/
|
|
887
|
+
export const spiral = (a, b, r0, r1, turns, w, n = 80) => {
|
|
888
|
+
const P = (i) => { const t = i / n, ang = t * turns * 2 * Math.PI, r = r0 + (r1 - r0) * t; return [a + r * Math.cos(ang), b + r * Math.sin(ang)]; };
|
|
889
|
+
return [...Array(n)].map((_, i) => slot2(P(i), P(i + 1), w));
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Pieces whose side faces merge into one surface (a band built from arcFront()/arcSide() quads): every face but the two
|
|
894
|
+
* end caps is tagged 'side'.
|
|
895
|
+
*
|
|
896
|
+
* @param {readonly import('../parts/iso.js').Piece[]} ps The pieces.
|
|
897
|
+
* @returns {import('../parts/iso.js').Piece[]} The pieces, retagged.
|
|
898
|
+
*/
|
|
899
|
+
export const smooth = (ps) => ps.map((q) => piece(q.faces.map((f) => f.pts), q.faces.map((f, i) => (i < 2 ? 0 : 'side'))));
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* A flame (2D points, about its foot at (a, b), h tall, b up): a teardrop with a second tongue off its side. Not a
|
|
903
|
+
* curled flame.
|
|
904
|
+
*
|
|
905
|
+
* @param {number} a The foot's first coordinate.
|
|
906
|
+
* @param {number} b Its second.
|
|
907
|
+
* @param {number} h The height.
|
|
908
|
+
* @returns {import('../geometry/types.js').Point[]} The outline.
|
|
909
|
+
*/
|
|
910
|
+
export const flame = (a, b, h) => {
|
|
911
|
+
const w = h * 0.36;
|
|
912
|
+
return [[a, b], ...arcPts(a, b + w, w, w, -90, 90, 10), [a + w * 0.25, b + h * 0.72], [a + w * 0.05, b + h * 0.55], [a - w * 0.1, b + h], [a - w * 0.85, b + h * 0.48], ...arcPts(a, b + w, w, w, 180, 270, 6)];
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
/* ---------- 2D stroke and band helpers for flat() cut-outs ---------- */
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* A thick stroke along a 2D polyline, w wide: a quad per segment and a disc at every joint (all convex, for flat()).
|
|
919
|
+
*
|
|
920
|
+
* @param {readonly (readonly number[])[]} pts The polyline.
|
|
921
|
+
* @param {number} w The width.
|
|
922
|
+
* @returns {import('../geometry/types.js').Point[][]} The convex pieces.
|
|
923
|
+
*/
|
|
924
|
+
export const stroke = (pts, w) => [...pts.slice(0, -1).map((p, i) => slot2(p, pts[i + 1], w)), ...pts.map(([a, b]) => circ(a, b, w / 2, 16))];
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* A band of an annulus (2D) round (a, b), radii r0–r1, from d0 to d1 degrees, as convex quads (a handle, a hook).
|
|
928
|
+
*
|
|
929
|
+
* @param {number} a The centre's first coordinate.
|
|
930
|
+
* @param {number} b Its second.
|
|
931
|
+
* @param {number} r0 The inner radius.
|
|
932
|
+
* @param {number} r1 The outer radius.
|
|
933
|
+
* @param {number} d0 From (degrees).
|
|
934
|
+
* @param {number} d1 To.
|
|
935
|
+
* @param {number} [n=16] Quads.
|
|
936
|
+
* @returns {import('../geometry/types.js').Point[][]} The quads.
|
|
937
|
+
*/
|
|
938
|
+
export const band = (a, b, r0, r1, d0, d1, n = 16) => [...Array(n)].map((_, i) => {
|
|
939
|
+
const t0 = ((d0 + ((d1 - d0) * i) / n) * Math.PI) / 180, t1 = ((d0 + ((d1 - d0) * (i + 1)) / n) * Math.PI) / 180;
|
|
940
|
+
const P = (r, t) => [a + r * Math.cos(t), b + r * Math.sin(t)];
|
|
941
|
+
return [P(r0, t0), P(r1, t0), P(r1, t1), P(r0, t1)];
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* 2D points moved by (da, db).
|
|
946
|
+
*
|
|
947
|
+
* @param {readonly (readonly number[])[]} pts The points.
|
|
948
|
+
* @param {number} da Along the first coordinate.
|
|
949
|
+
* @param {number} db Along the second.
|
|
950
|
+
* @returns {import('../geometry/types.js').Point[]} The moved points.
|
|
951
|
+
*/
|
|
952
|
+
export const mv = (pts, da, db) => pts.map(([a, b]) => [a + da, b + db]);
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* A rectangle a0–a1 × b0–b1 (2D points; the geometry kit's rect() is a polygon from a corner and a size).
|
|
956
|
+
*
|
|
957
|
+
* @param {number} a0 From, first coordinate.
|
|
958
|
+
* @param {number} b0 From, second.
|
|
959
|
+
* @param {number} a1 To, first coordinate.
|
|
960
|
+
* @param {number} b1 To, second.
|
|
961
|
+
* @returns {import('../geometry/types.js').Point[]} Its four corners.
|
|
962
|
+
*/
|
|
963
|
+
export const rect = (a0, b0, a1, b1) => [[a0, b0], [a1, b0], [a1, b1], [a0, b1]];
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* A kit glyph h tall on a plane o + a·u + b·v (centred on o, upright along v), its strokes T of 400 thick: its outlines
|
|
967
|
+
* to cut and its counters to keep.
|
|
968
|
+
*
|
|
969
|
+
* @param {string} k The glyph (the parts' glyph() key).
|
|
970
|
+
* @param {readonly number[]} o The plane's origin (the glyph's centre).
|
|
971
|
+
* @param {readonly number[]} u The plane's first axis.
|
|
972
|
+
* @param {readonly number[]} v Its second (up the glyph).
|
|
973
|
+
* @param {number} h The glyph's height.
|
|
974
|
+
* @param {number} [T=96] Its stroke, of 400.
|
|
975
|
+
* @returns {{ cut: import('../geometry/types.js').Point3[][], keep: import('../geometry/types.js').Point3[][] }} The
|
|
976
|
+
* outlines to cut and the counters to put back.
|
|
977
|
+
*/
|
|
978
|
+
export const glyphOn = (k, o, u, v, h, T = 96) => {
|
|
979
|
+
const s = glyph(k, 400, T), ys = s.flat(2).map((p) => p[1]), sc = h / (Math.max(...ys) - Math.min(...ys));
|
|
980
|
+
const to = (ring) => onPlane(o, u, v, ring.map(([x, y]) => [x * sc, -y * sc]));
|
|
981
|
+
return { cut: s.map((pg) => to(pg[0])), keep: s.flatMap((pg) => pg.slice(1).map(to)) };
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
/* ---------- tipped slabs ---------- */
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* A 3D point p moved by k times the vector v.
|
|
988
|
+
*
|
|
989
|
+
* @param {readonly number[]} p The point.
|
|
990
|
+
* @param {readonly number[]} v The vector.
|
|
991
|
+
* @param {number} [k=1] How many times.
|
|
992
|
+
* @returns {number[]} The moved point.
|
|
993
|
+
*/
|
|
994
|
+
export const add3 = (p, v, k = 1) => p.map((c, i) => c + v[i] * k);
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* A ring of gap round the 3D outline q, measured on the canvas (an inset in model units on a tipped or sloped face
|
|
998
|
+
* projects wider one way than the other): cuts q from the drawn mark s and puts back its inside g in.
|
|
999
|
+
*
|
|
1000
|
+
* @param {readonly any[]} parts The parts s was drawn from (for the projection).
|
|
1001
|
+
* @param {import('../geometry/types.js').Shape} s The drawn mark.
|
|
1002
|
+
* @param {readonly (readonly number[])[]} q The 3D outline.
|
|
1003
|
+
* @param {number} g The gap.
|
|
1004
|
+
* @returns {import('../geometry/types.js').Shape} The mark with the ring cut.
|
|
1005
|
+
*/
|
|
1006
|
+
export const ringIn = (parts, s, q, g) => { const P = fitOf(parts), h = poly(q.map((p) => P(...p))); return U(D(s, h), I(s, grow(h, -g))); };
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Keys tiling the 3D outline `area`, cut as islands with the gap between them measured on the canvas.
|
|
1010
|
+
*
|
|
1011
|
+
* @param {readonly any[]} parts The parts s was drawn from.
|
|
1012
|
+
* @param {import('../geometry/types.js').Shape} s The drawn mark.
|
|
1013
|
+
* @param {readonly (readonly number[])[]} area The keypad's 3D outline.
|
|
1014
|
+
* @param {readonly (readonly (readonly number[])[])[]} keys Each key's 3D outline.
|
|
1015
|
+
* @param {number} g The gap.
|
|
1016
|
+
* @returns {import('../geometry/types.js').Shape} The mark with the keys cut.
|
|
1017
|
+
*/
|
|
1018
|
+
export const keysIn = (parts, s, area, keys, g) => {
|
|
1019
|
+
const P = fitOf(parts), pr = (q) => poly(q.map((p) => P(...p))), A = pr(area), inner = grow(A, -g);
|
|
1020
|
+
return U(D(s, A), ...keys.map((q) => I(s, grow(pr(q), -g / 2), inner)));
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* A slab tipped back by `deg` from upright, its foot edge's middle at o. `dir` 'y': its width runs along −y (up and
|
|
1025
|
+
* right on the canvas) and it tips back towards −x; 'x': its width runs along +x (down and right) and it tips back
|
|
1026
|
+
* towards −y. At 90° it lies flat. `outline` is a 2D (a, b) shape on its face (a across, b up the face), `t` its
|
|
1027
|
+
* thickness behind the face; `smooth` merges its edge band into one face.
|
|
1028
|
+
*
|
|
1029
|
+
* @param {readonly number[]} o The foot edge's middle.
|
|
1030
|
+
* @param {number} deg How far it tips back.
|
|
1031
|
+
* @param {readonly (readonly number[])[]} outline The face's outline (2D).
|
|
1032
|
+
* @param {number} t The thickness.
|
|
1033
|
+
* @param {{ dir?: 'x' | 'y', smooth?: boolean }} [o] The way its width runs ('y') and whether its edge merges (true).
|
|
1034
|
+
* @returns {{ part: import('../parts/iso.js').Piece, on: (pts: readonly (readonly number[])[]) => number[][], n: import('../geometry/types.js').Point3 }}
|
|
1035
|
+
* The slab, a function putting 2D outlines on its face, and the face's normal.
|
|
1036
|
+
*/
|
|
1037
|
+
export const slab = (o, deg, outline, t, { dir = 'y', smooth = true } = {}) => {
|
|
1038
|
+
const r = (deg * Math.PI) / 180, s = Math.sin(r), c = Math.cos(r);
|
|
1039
|
+
const u = dir === 'y' ? [0, -1, 0] : [1, 0, 0], v = dir === 'y' ? [-s, 0, c] : [0, -s, c], n = cross3(v, u);
|
|
1040
|
+
const on = (pts) => pts.map(([a, b]) => [0, 1, 2].map((i) => o[i] + a * u[i] + b * v[i])), face = on(outline);
|
|
1041
|
+
return { part: loft(face.map((p) => add3(p, n, -t)), face, smooth), on, n };
|
|
1042
|
+
};
|