cadcode 0.0.0-stage → 0.1.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/README.md +214 -2
- package/client/index.html +51 -0
- package/client/viewer.css +216 -0
- package/client/viewer.js +308 -0
- package/package.json +34 -4
- package/src/cli.js +96 -0
- package/src/core/mesh.js +69 -0
- package/src/core/program.js +118 -0
- package/src/dev/server.js +172 -0
- package/src/dev/worker.js +39 -0
- package/src/export/png.js +52 -0
- package/src/export/raster.js +207 -0
- package/src/export/stl.js +28 -0
- package/src/lib/colors.js +56 -0
- package/src/lib/index.js +25 -0
- package/src/lib/shapes.js +489 -0
- package/src/lib/util.js +24 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import Module from 'manifold-3d';
|
|
2
|
+
import { parseColor } from './colors.js';
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('./colors.js').ColorInput} ColorInput */
|
|
5
|
+
/** @typedef {import('./colors.js').RGBA} RGBA */
|
|
6
|
+
/** @typedef {[number, number]} Vec2 */
|
|
7
|
+
/** @typedef {[number, number, number]} Vec3 */
|
|
8
|
+
/**
|
|
9
|
+
* Nested arrays of shapes, as produced by loops. Falsy entries are skipped, so `cond && shape` works.
|
|
10
|
+
* @template T
|
|
11
|
+
* @typedef {T | null | undefined | false | Items<T>[]} Items
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const wasm = await Module();
|
|
15
|
+
wasm.setup();
|
|
16
|
+
const { Manifold, CrossSection, Mesh } = wasm;
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Resolution of curved primitives — the same rules as OpenSCAD's $fn/$fa/$fs.
|
|
20
|
+
|
|
21
|
+
const resolution = { fn: 0, fa: 12, fs: 2 };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sets the default number of segments for circles, cylinders and spheres, like OpenSCAD's `$fn`, `$fa` and `$fs`.
|
|
25
|
+
* @param {{ fn?: number, fa?: number, fs?: number }} options
|
|
26
|
+
*/
|
|
27
|
+
export function setResolution(options) {
|
|
28
|
+
Object.assign(resolution, options);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function segmentsFor(r, fn) {
|
|
32
|
+
const f = fn ?? resolution.fn;
|
|
33
|
+
if (r < 1e-9) return 3;
|
|
34
|
+
if (f > 0) return Math.max(Math.round(f), 3);
|
|
35
|
+
return Math.ceil(Math.max(Math.min(360 / resolution.fa, (r * 2 * Math.PI) / resolution.fs), 5));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Colors travel with the geometry: color() stamps a fresh mesh ID on the solid,
|
|
40
|
+
// and manifold keeps per-triangle IDs through booleans, so a union of colored
|
|
41
|
+
// parts still knows which faces came from where.
|
|
42
|
+
|
|
43
|
+
/** @type {Map<number, RGBA>} */
|
|
44
|
+
const colorById = new Map();
|
|
45
|
+
|
|
46
|
+
/** @internal Looks up the color assigned to a manifold original ID. */
|
|
47
|
+
export function colorOfId(id) {
|
|
48
|
+
return colorById.get(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/** @template T @param {Items<T>[]} items @returns {T[]} */
|
|
54
|
+
function flatten(items) {
|
|
55
|
+
const out = [];
|
|
56
|
+
const walk = (item) => {
|
|
57
|
+
if (Array.isArray(item)) item.forEach(walk);
|
|
58
|
+
else if (item) out.push(item);
|
|
59
|
+
};
|
|
60
|
+
items.forEach(walk);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {number[]} v @returns {Vec3} */
|
|
65
|
+
function toVec3(v) {
|
|
66
|
+
return [v[0] ?? 0, v[1] ?? 0, v[2] ?? 0];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Rotation by `degrees` around an arbitrary `axis`, as a column-major 4x4 matrix. */
|
|
70
|
+
function axisRotation(degrees, axis) {
|
|
71
|
+
const [ax, ay, az] = toVec3(axis);
|
|
72
|
+
const len = Math.hypot(ax, ay, az);
|
|
73
|
+
const [x, y, z] = [ax / len, ay / len, az / len];
|
|
74
|
+
const a = (degrees * Math.PI) / 180;
|
|
75
|
+
const c = Math.cos(a);
|
|
76
|
+
const s = Math.sin(a);
|
|
77
|
+
const t = 1 - c;
|
|
78
|
+
return [
|
|
79
|
+
t * x * x + c, t * x * y + s * z, t * x * z - s * y, 0,
|
|
80
|
+
t * x * y - s * z, t * y * y + c, t * y * z + s * x, 0,
|
|
81
|
+
t * x * z + s * y, t * y * z - s * x, t * z * z + c, 0,
|
|
82
|
+
0, 0, 0, 1,
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A 3D solid. Every method returns a new solid; solids are never modified in place. */
|
|
87
|
+
export class Solid {
|
|
88
|
+
/** @internal */
|
|
89
|
+
constructor(manifold) {
|
|
90
|
+
this.manifold = manifold;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @param {number[]} v */
|
|
94
|
+
translate(v) {
|
|
95
|
+
return new Solid(this.manifold.translate(toVec3(v)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Rotates in degrees. `rotate([x, y, z])` turns around X, then Y, then Z, like OpenSCAD.
|
|
100
|
+
* `rotate(a)` turns around Z; `rotate(a, axis)` turns around an arbitrary axis.
|
|
101
|
+
* @param {number[] | number} angles
|
|
102
|
+
* @param {number[]} [axis]
|
|
103
|
+
*/
|
|
104
|
+
rotate(angles, axis) {
|
|
105
|
+
if (axis) return new Solid(this.manifold.transform(axisRotation(angles, axis)));
|
|
106
|
+
return new Solid(this.manifold.rotate(typeof angles === 'number' ? [0, 0, angles] : toVec3(angles)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @param {number[] | number} v */
|
|
110
|
+
scale(v) {
|
|
111
|
+
return new Solid(this.manifold.scale(typeof v === 'number' ? v : [v[0] ?? 1, v[1] ?? 1, v[2] ?? 1]));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Mirrors across the plane through the origin with the given normal. @param {number[]} normal */
|
|
115
|
+
mirror(normal) {
|
|
116
|
+
return new Solid(this.manifold.mirror(toVec3(normal)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Applies a column-major 4x4 matrix. @param {number[]} matrix */
|
|
120
|
+
transform(matrix) {
|
|
121
|
+
return new Solid(this.manifold.transform(matrix));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Colors the whole solid. Accepts CSS names, hex strings or [r, g, b, a] in 0..1.
|
|
126
|
+
* @param {ColorInput} color
|
|
127
|
+
* @param {number} [alpha]
|
|
128
|
+
*/
|
|
129
|
+
color(color, alpha) {
|
|
130
|
+
const m = this.manifold.asOriginal();
|
|
131
|
+
colorById.set(m.originalID(), parseColor(color, alpha));
|
|
132
|
+
return new Solid(m);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @param {...Items<Solid>} others */
|
|
136
|
+
union(...others) {
|
|
137
|
+
return new Solid(Manifold.union([this.manifold, ...flatten(others).map((s) => s.manifold)]));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @param {...Items<Solid>} others */
|
|
141
|
+
subtract(...others) {
|
|
142
|
+
const cutters = flatten(others);
|
|
143
|
+
if (cutters.length === 0) return this;
|
|
144
|
+
return new Solid(this.manifold.subtract(Manifold.union(cutters.map((s) => s.manifold))));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @param {...Items<Solid>} others */
|
|
148
|
+
intersect(...others) {
|
|
149
|
+
return new Solid(Manifold.intersection([this.manifold, ...flatten(others).map((s) => s.manifold)]));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Convex hull of this solid together with any others. @param {...Items<Solid>} others */
|
|
153
|
+
hull(...others) {
|
|
154
|
+
return new Solid(Manifold.hull([this.manifold, ...flatten(others).map((s) => s.manifold)]));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The cross-section at height `z`. */
|
|
158
|
+
slice(z = 0) {
|
|
159
|
+
return new Shape2(this.manifold.slice(z));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The outline of the solid projected onto the XY plane. */
|
|
163
|
+
project() {
|
|
164
|
+
return new Shape2(this.manifold.project());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
volume() {
|
|
168
|
+
return this.manifold.volume();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
surfaceArea() {
|
|
172
|
+
return this.manifold.surfaceArea();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @returns {{ min: Vec3, max: Vec3 }} */
|
|
176
|
+
bounds() {
|
|
177
|
+
const { min, max } = this.manifold.boundingBox();
|
|
178
|
+
return { min: [...min], max: [...max] };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
isEmpty() {
|
|
182
|
+
return this.manifold.isEmpty();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @typedef {object} ExtrudeOptions
|
|
188
|
+
* @property {boolean} [center]
|
|
189
|
+
* @property {number} [twist] degrees the top is rotated relative to the bottom
|
|
190
|
+
* @property {number | Vec2} [scale] scale of the top relative to the bottom
|
|
191
|
+
* @property {number} [divisions] intermediate slices; useful with twist
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/** @typedef {{ r: number, fn?: number } | { delta: number, chamfer?: boolean }} OffsetOptions */
|
|
195
|
+
|
|
196
|
+
/** A 2D shape in the XY plane. Extrude or revolve it to get a solid. */
|
|
197
|
+
export class Shape2 {
|
|
198
|
+
/** @internal */
|
|
199
|
+
constructor(section) {
|
|
200
|
+
this.section = section;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @param {number[]} v */
|
|
204
|
+
translate(v) {
|
|
205
|
+
return new Shape2(this.section.translate([v[0] ?? 0, v[1] ?? 0]));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** @param {number} degrees */
|
|
209
|
+
rotate(degrees) {
|
|
210
|
+
return new Shape2(this.section.rotate(degrees));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @param {number[] | number} v */
|
|
214
|
+
scale(v) {
|
|
215
|
+
return new Shape2(this.section.scale(typeof v === 'number' ? v : [v[0] ?? 1, v[1] ?? 1]));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @param {number[]} axis */
|
|
219
|
+
mirror(axis) {
|
|
220
|
+
return new Shape2(this.section.mirror([axis[0] ?? 0, axis[1] ?? 0]));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Grows (positive) or shrinks (negative) the outline, like OpenSCAD's offset().
|
|
225
|
+
* A number means `{ r }`: rounded corners. `{ delta }` keeps corners sharp, `{ delta, chamfer: true }` cuts them.
|
|
226
|
+
* @param {number | OffsetOptions} options
|
|
227
|
+
*/
|
|
228
|
+
offset(options) {
|
|
229
|
+
const o = typeof options === 'number' ? { r: options } : options;
|
|
230
|
+
if ('r' in o) return new Shape2(this.section.offset(o.r, 'Round', 2, segmentsFor(Math.abs(o.r), o.fn)));
|
|
231
|
+
return new Shape2(this.section.offset(o.delta, o.chamfer ? 'Square' : 'Miter', 1e6));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** @param {...Items<Shape2>} others */
|
|
235
|
+
union(...others) {
|
|
236
|
+
return new Shape2(CrossSection.union([this.section, ...flatten(others).map((s) => s.section)]));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** @param {...Items<Shape2>} others */
|
|
240
|
+
subtract(...others) {
|
|
241
|
+
const cutters = flatten(others);
|
|
242
|
+
if (cutters.length === 0) return this;
|
|
243
|
+
return new Shape2(this.section.subtract(CrossSection.union(cutters.map((s) => s.section))));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** @param {...Items<Shape2>} others */
|
|
247
|
+
intersect(...others) {
|
|
248
|
+
return new Shape2(CrossSection.intersection([this.section, ...flatten(others).map((s) => s.section)]));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** @param {...Items<Shape2>} others */
|
|
252
|
+
hull(...others) {
|
|
253
|
+
return new Shape2(CrossSection.hull([this.section, ...flatten(others).map((s) => s.section)]));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extrudes along +Z, like OpenSCAD's linear_extrude().
|
|
258
|
+
* @param {number} height
|
|
259
|
+
* @param {ExtrudeOptions} [options]
|
|
260
|
+
*/
|
|
261
|
+
extrude(height, options = {}) {
|
|
262
|
+
const { center = false, twist = 0, scale = 1, divisions = 0 } = options;
|
|
263
|
+
// manifold-3d misreads a scalar scaleTop, so always pass a vector.
|
|
264
|
+
const scaleTop = typeof scale === 'number' ? [scale, scale] : scale;
|
|
265
|
+
return new Solid(this.section.extrude(height, divisions, twist, scaleTop, center));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Revolves around the Y axis into a solid around Z, like OpenSCAD's rotate_extrude().
|
|
270
|
+
* @param {{ angle?: number, fn?: number }} [options]
|
|
271
|
+
*/
|
|
272
|
+
revolve(options = {}) {
|
|
273
|
+
const { min, max } = this.section.bounds();
|
|
274
|
+
const radius = Math.max(Math.abs(min[0]), Math.abs(max[0]));
|
|
275
|
+
return new Solid(this.section.revolve(segmentsFor(radius, options.fn), options.angle ?? 360));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
area() {
|
|
279
|
+
return this.section.area();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** @returns {{ min: Vec2, max: Vec2 }} */
|
|
283
|
+
bounds() {
|
|
284
|
+
const { min, max } = this.section.bounds();
|
|
285
|
+
return { min: [...min], max: [...max] };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
isEmpty() {
|
|
289
|
+
return this.section.isEmpty();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The outline as arrays of points. @returns {Vec2[][]} */
|
|
293
|
+
toPolygons() {
|
|
294
|
+
return this.section.toPolygons().map((poly) => poly.map((p) => [p[0], p[1]]));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// 3D primitives
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* A box with one corner at the origin, or centered with `{ center: true }`.
|
|
303
|
+
* @param {number | number[]} [size]
|
|
304
|
+
* @param {{ center?: boolean }} [options]
|
|
305
|
+
*/
|
|
306
|
+
export function cube(size = 1, options = {}) {
|
|
307
|
+
const s = typeof size === 'number' ? [size, size, size] : toVec3(size);
|
|
308
|
+
return new Solid(Manifold.cube(s, options.center ?? false));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @typedef {object} CylinderOptions
|
|
313
|
+
* @property {number} h
|
|
314
|
+
* @property {number} [r]
|
|
315
|
+
* @property {number} [d]
|
|
316
|
+
* @property {number} [r1] bottom radius, for cones
|
|
317
|
+
* @property {number} [r2] top radius, for cones
|
|
318
|
+
* @property {number} [d1]
|
|
319
|
+
* @property {number} [d2]
|
|
320
|
+
* @property {boolean} [center]
|
|
321
|
+
* @property {number} [fn]
|
|
322
|
+
*/
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* A cylinder or cone standing on the XY plane along +Z, like OpenSCAD's cylinder().
|
|
326
|
+
* @param {CylinderOptions} options
|
|
327
|
+
*/
|
|
328
|
+
export function cylinder(options) {
|
|
329
|
+
const { h, r, d, r1, r2, d1, d2, center = false, fn } = options;
|
|
330
|
+
const base = r ?? (d !== undefined ? d / 2 : 1);
|
|
331
|
+
const bottom = r1 ?? (d1 !== undefined ? d1 / 2 : base);
|
|
332
|
+
const top = r2 ?? (d2 !== undefined ? d2 / 2 : base);
|
|
333
|
+
return new Solid(Manifold.cylinder(h, bottom, top, segmentsFor(Math.max(bottom, top), fn), center));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* A sphere centered on the origin. `sphere(5)` is `sphere({ r: 5 })`.
|
|
338
|
+
* @param {number | { r?: number, d?: number, fn?: number }} options
|
|
339
|
+
*/
|
|
340
|
+
export function sphere(options) {
|
|
341
|
+
const o = typeof options === 'number' ? { r: options } : options;
|
|
342
|
+
const r = o.r ?? (o.d !== undefined ? o.d / 2 : 1);
|
|
343
|
+
return new Solid(Manifold.sphere(r, segmentsFor(r, o.fn)));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* A solid from explicit vertices and faces, like OpenSCAD's polyhedron():
|
|
348
|
+
* each face lists point indices clockwise when seen from outside.
|
|
349
|
+
* @param {number[][]} points
|
|
350
|
+
* @param {number[][]} faces
|
|
351
|
+
*/
|
|
352
|
+
export function polyhedron(points, faces) {
|
|
353
|
+
const triVerts = [];
|
|
354
|
+
for (const face of faces) {
|
|
355
|
+
for (let i = 1; i + 1 < face.length; i++) triVerts.push(face[0], face[i + 1], face[i]);
|
|
356
|
+
}
|
|
357
|
+
const mesh = new Mesh({
|
|
358
|
+
numProp: 3,
|
|
359
|
+
vertProperties: new Float32Array(points.flatMap(toVec3)),
|
|
360
|
+
triVerts: new Uint32Array(triVerts),
|
|
361
|
+
});
|
|
362
|
+
mesh.merge();
|
|
363
|
+
const manifold = new Manifold(mesh);
|
|
364
|
+
if (manifold.status() !== 'NoError') throw new Error(`polyhedron() is not a closed solid: ${manifold.status()}`);
|
|
365
|
+
return new Solid(manifold);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// 2D primitives
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* @param {number | number[]} [size]
|
|
373
|
+
* @param {{ center?: boolean }} [options]
|
|
374
|
+
*/
|
|
375
|
+
export function square(size = 1, options = {}) {
|
|
376
|
+
const s = typeof size === 'number' ? [size, size] : [size[0], size[1]];
|
|
377
|
+
return new Shape2(CrossSection.square(s, options.center ?? false));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* A circle centered on the origin. `circle(5)` is `circle({ r: 5 })`.
|
|
382
|
+
* @param {number | { r?: number, d?: number, fn?: number }} options
|
|
383
|
+
*/
|
|
384
|
+
export function circle(options) {
|
|
385
|
+
const o = typeof options === 'number' ? { r: options } : options;
|
|
386
|
+
const r = o.r ?? (o.d !== undefined ? o.d / 2 : 1);
|
|
387
|
+
return new Shape2(CrossSection.circle(r, segmentsFor(r, o.fn)));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* A polygon from points. With `paths`, each path is a list of point indices;
|
|
392
|
+
* paths inside others become holes (even-odd fill), like OpenSCAD's polygon().
|
|
393
|
+
* @param {number[][]} points
|
|
394
|
+
* @param {number[][]} [paths]
|
|
395
|
+
*/
|
|
396
|
+
export function polygon(points, paths) {
|
|
397
|
+
const pts = points.map((p) => [p[0], p[1]]);
|
|
398
|
+
const contours = paths ? paths.map((path) => path.map((i) => pts[i])) : [pts];
|
|
399
|
+
return new Shape2(new CrossSection(contours, 'EvenOdd'));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// Functional forms. They take any mix of shapes and nested arrays (from loops),
|
|
404
|
+
// so OpenSCAD's `translate(v) for (...) child();` becomes `translate(v, xs.map(...))`.
|
|
405
|
+
|
|
406
|
+
function dimension(items, op) {
|
|
407
|
+
if (items.length === 0) return 'empty';
|
|
408
|
+
const solids = items.filter((s) => s instanceof Solid).length;
|
|
409
|
+
if (solids === items.length) return 'solid';
|
|
410
|
+
if (solids === 0) return 'shape2';
|
|
411
|
+
throw new Error(`${op}() cannot mix 2D shapes and 3D solids`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Applies a 3D or 2D operation to all items, depending on what they are. */
|
|
415
|
+
function combine(items, op, solidOp, shapeOp) {
|
|
416
|
+
const flat = flatten(items);
|
|
417
|
+
switch (dimension(flat, op)) {
|
|
418
|
+
case 'empty':
|
|
419
|
+
return new Solid(Manifold.compose([]));
|
|
420
|
+
case 'solid':
|
|
421
|
+
return new Solid(solidOp(flat.map((s) => s.manifold)));
|
|
422
|
+
case 'shape2':
|
|
423
|
+
return new Shape2(shapeOp(flat.map((s) => s.section)));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** @param {...Items<Solid | Shape2>} items */
|
|
428
|
+
export function union(...items) {
|
|
429
|
+
return combine(items, 'union', (ms) => Manifold.union(ms), (cs) => CrossSection.union(cs));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Subtracts every following shape from the first one.
|
|
434
|
+
* @param {Solid | Shape2} base
|
|
435
|
+
* @param {...Items<Solid | Shape2>} cutters
|
|
436
|
+
*/
|
|
437
|
+
export function difference(base, ...cutters) {
|
|
438
|
+
return base.subtract(...cutters);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** @param {...Items<Solid | Shape2>} items */
|
|
442
|
+
export function intersection(...items) {
|
|
443
|
+
return combine(items, 'intersection', (ms) => Manifold.intersection(ms), (cs) => CrossSection.intersection(cs));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Convex hull of all given shapes, like OpenSCAD's hull(). @param {...Items<Solid | Shape2>} items */
|
|
447
|
+
export function hull(...items) {
|
|
448
|
+
return combine(items, 'hull', (ms) => Manifold.hull(ms), (cs) => CrossSection.hull(cs));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** @param {number[]} v @param {...Items<Solid | Shape2>} items */
|
|
452
|
+
export function translate(v, ...items) {
|
|
453
|
+
return union(...items).translate(v);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Same arguments as {@link Solid#rotate}; 2D shapes take a single angle. @param {number[] | number} angles @param {...Items<Solid | Shape2>} items */
|
|
457
|
+
export function rotate(angles, ...items) {
|
|
458
|
+
return union(...items).rotate(angles);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** @param {number[] | number} v @param {...Items<Solid | Shape2>} items */
|
|
462
|
+
export function scale(v, ...items) {
|
|
463
|
+
return union(...items).scale(v);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** @param {number[]} normal @param {...Items<Solid | Shape2>} items */
|
|
467
|
+
export function mirror(normal, ...items) {
|
|
468
|
+
return union(...items).mirror(normal);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* `color('orange', lid)` or `color('orange', 0.35, lid)`.
|
|
473
|
+
* @param {ColorInput} c
|
|
474
|
+
* @param {...(number | Items<Solid>)} rest
|
|
475
|
+
*/
|
|
476
|
+
export function color(c, ...rest) {
|
|
477
|
+
const alpha = typeof rest[0] === 'number' ? rest.shift() : undefined;
|
|
478
|
+
return union(...rest).color(c, alpha);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** @param {number | OffsetOptions} options @param {...Items<Shape2>} items */
|
|
482
|
+
export function offset(options, ...items) {
|
|
483
|
+
return union(...items).offset(options);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** @param {number} height @param {ExtrudeOptions} options @param {...Items<Shape2>} items */
|
|
487
|
+
export function linearExtrude(height, options, ...items) {
|
|
488
|
+
return union(...items).extrude(height, options);
|
|
489
|
+
}
|
package/src/lib/util.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numbers from `start` to `end` inclusive, like OpenSCAD's `[start : step : end]`.
|
|
3
|
+
* @param {number} start
|
|
4
|
+
* @param {number} end
|
|
5
|
+
* @param {number} [step]
|
|
6
|
+
* @returns {number[]}
|
|
7
|
+
*/
|
|
8
|
+
export function range(start, end, step = 1) {
|
|
9
|
+
if (step === 0) throw new Error('range() step must not be 0');
|
|
10
|
+
const out = [];
|
|
11
|
+
const count = Math.floor((end - start) / step + 1e-9);
|
|
12
|
+
for (let i = 0; i <= count; i++) out.push(start + i * step);
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Every combination of the given lists, like OpenSCAD's `for (x = xs, y = ys)`:
|
|
18
|
+
* `product([1, 2], [3, 4])` is `[[1, 3], [1, 4], [2, 3], [2, 4]]`.
|
|
19
|
+
* @param {...any[]} lists
|
|
20
|
+
* @returns {any[][]}
|
|
21
|
+
*/
|
|
22
|
+
export function product(...lists) {
|
|
23
|
+
return lists.reduce((acc, list) => acc.flatMap((combo) => list.map((v) => [...combo, v])), [[]]);
|
|
24
|
+
}
|