@mylasertools/kerfscript 0.3.0 → 0.4.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/dist/cli.js CHANGED
@@ -12302,6 +12302,273 @@ var require_opentype = __commonJS({
12302
12302
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
12303
12303
  import { basename, dirname, extname, join, resolve } from "node:path";
12304
12304
 
12305
+ // src/assembly.ts
12306
+ var RAD = Math.PI / 180;
12307
+ function multiply(a, b) {
12308
+ const out = new Array(16).fill(0);
12309
+ for (let r = 0; r < 4; r++)
12310
+ for (let c = 0; c < 4; c++)
12311
+ for (let k = 0; k < 4; k++) out[r * 4 + c] += a[r * 4 + k] * b[k * 4 + c];
12312
+ return out;
12313
+ }
12314
+ function rotation(axis, degrees) {
12315
+ const c = Math.cos(degrees * RAD);
12316
+ const s = Math.sin(degrees * RAD);
12317
+ if (axis === 0) return [1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1];
12318
+ if (axis === 1) return [c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1];
12319
+ return [c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
12320
+ }
12321
+ var clean = (n2) => Math.abs(n2) < 1e-12 ? 0 : n2;
12322
+ function placementMatrix(p) {
12323
+ const [ax, ay, az] = p.rotate;
12324
+ const r = multiply(rotation(2, az), multiply(rotation(1, ay), rotation(0, ax)));
12325
+ r[3] = p.at[0];
12326
+ r[7] = p.at[1];
12327
+ r[11] = p.at[2];
12328
+ return r.map(clean);
12329
+ }
12330
+ function apply(m, x, y, z) {
12331
+ return [
12332
+ m[0] * x + m[1] * y + m[2] * z + m[3],
12333
+ m[4] * x + m[5] * y + m[6] * z + m[7],
12334
+ m[8] * x + m[9] * y + m[10] * z + m[11]
12335
+ ];
12336
+ }
12337
+ function invertRigid(m) {
12338
+ const [tx, ty, tz] = [m[3], m[7], m[11]];
12339
+ const r = [m[0], m[4], m[8], m[1], m[5], m[9], m[2], m[6], m[10]];
12340
+ return [
12341
+ r[0],
12342
+ r[1],
12343
+ r[2],
12344
+ -(r[0] * tx + r[1] * ty + r[2] * tz),
12345
+ r[3],
12346
+ r[4],
12347
+ r[5],
12348
+ -(r[3] * tx + r[4] * ty + r[5] * tz),
12349
+ r[6],
12350
+ r[7],
12351
+ r[8],
12352
+ -(r[6] * tx + r[7] * ty + r[8] * tz),
12353
+ 0,
12354
+ 0,
12355
+ 0,
12356
+ 1
12357
+ ];
12358
+ }
12359
+ function bounds2(regions) {
12360
+ let [x0, y0, x1, y1] = [Infinity, Infinity, -Infinity, -Infinity];
12361
+ for (const ring of regions)
12362
+ for (const [x, y] of ring) {
12363
+ x0 = Math.min(x0, x);
12364
+ y0 = Math.min(y0, y);
12365
+ x1 = Math.max(x1, x);
12366
+ y1 = Math.max(y1, y);
12367
+ }
12368
+ return [x0, y0, x1, y1];
12369
+ }
12370
+ function worldBox(p) {
12371
+ const [x0, y0, x1, y1] = bounds2(p.outline);
12372
+ const lo = [Infinity, Infinity, Infinity];
12373
+ const hi = [-Infinity, -Infinity, -Infinity];
12374
+ for (const x of [x0, x1])
12375
+ for (const y of [y0, y1])
12376
+ for (const z of [0, p.thickness]) {
12377
+ const w = apply(p.matrix, x, y, z);
12378
+ for (let k = 0; k < 3; k++) {
12379
+ lo[k] = Math.min(lo[k], w[k]);
12380
+ hi[k] = Math.max(hi[k], w[k]);
12381
+ }
12382
+ }
12383
+ return [lo, hi];
12384
+ }
12385
+ function besideAssembly(placed, loose, gap = 10) {
12386
+ const lo = [Infinity, Infinity, Infinity];
12387
+ const hi = [-Infinity, -Infinity, -Infinity];
12388
+ for (const [a, b] of placed.map(worldBox))
12389
+ for (let k = 0; k < 3; k++) {
12390
+ lo[k] = Math.min(lo[k], a[k]);
12391
+ hi[k] = Math.max(hi[k], b[k]);
12392
+ }
12393
+ const sizes = loose.map((p) => bounds2(p.outline));
12394
+ const limit = sizes.reduce((w, [x0, , x1]) => Math.max(w, x1 - x0), hi[0] - lo[0]);
12395
+ let x = 0;
12396
+ let y = 0;
12397
+ let row = 0;
12398
+ return loose.map((p, i) => {
12399
+ const [x0, y0, x1, y1] = sizes[i];
12400
+ if (x > 0 && x + x1 - x0 > limit) {
12401
+ x = 0;
12402
+ y += row + gap;
12403
+ row = 0;
12404
+ }
12405
+ const matrix = [
12406
+ 1,
12407
+ 0,
12408
+ 0,
12409
+ hi[0] + gap + x - x0,
12410
+ 0,
12411
+ 1,
12412
+ 0,
12413
+ lo[1] + y - y0,
12414
+ 0,
12415
+ 0,
12416
+ 1,
12417
+ lo[2],
12418
+ 0,
12419
+ 0,
12420
+ 0,
12421
+ 1
12422
+ ];
12423
+ x += x1 - x0 + gap;
12424
+ row = Math.max(row, y1 - y0);
12425
+ return { ...p, matrix: matrix.map(clean) };
12426
+ });
12427
+ }
12428
+ var overlaps = (a, b, eps) => [0, 1, 2].every((k) => a[0][k] < b[1][k] - eps && b[0][k] < a[1][k] - eps);
12429
+ var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
12430
+ var cross = (a, b) => [
12431
+ a[1] * b[2] - a[2] * b[1],
12432
+ a[2] * b[0] - a[0] * b[2],
12433
+ a[0] * b[1] - a[1] * b[0]
12434
+ ];
12435
+ var add = (a, b, k = 1) => [a[0] + b[0] * k, a[1] + b[1] * k, a[2] + b[2] * k];
12436
+ var normal = (m) => [m[2], m[6], m[10]];
12437
+ var origin = (m) => [m[3], m[7], m[11]];
12438
+ var turn = (m, v) => [
12439
+ m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
12440
+ m[4] * v[0] + m[5] * v[1] + m[6] * v[2],
12441
+ m[8] * v[0] + m[9] * v[1] + m[10] * v[2]
12442
+ ];
12443
+ function spans(regions, from, dir) {
12444
+ const len = dir[0] * dir[0] + dir[1] * dir[1];
12445
+ const hits = [];
12446
+ for (const ring of regions)
12447
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
12448
+ const a = ring[j];
12449
+ const b = ring[i];
12450
+ const da = dir[0] * (a[1] - from[1]) - dir[1] * (a[0] - from[0]);
12451
+ const db = dir[0] * (b[1] - from[1]) - dir[1] * (b[0] - from[0]);
12452
+ if (da > 0 === db > 0) continue;
12453
+ const f = da / (da - db);
12454
+ const x = a[0] + (b[0] - a[0]) * f - from[0];
12455
+ const y = a[1] + (b[1] - a[1]) * f - from[1];
12456
+ hits.push((x * dir[0] + y * dir[1]) / len);
12457
+ }
12458
+ return hits.sort((p, q) => p - q);
12459
+ }
12460
+ function shared(a, b, eps) {
12461
+ let total = 0;
12462
+ let i = 0;
12463
+ let j = 0;
12464
+ while (i + 1 < a.length && j + 1 < b.length) {
12465
+ const length = Math.min(a[i + 1], b[j + 1]) - Math.max(a[i], b[j]);
12466
+ if (length > eps) total += length;
12467
+ if (a[i + 1] < b[j + 1]) i += 2;
12468
+ else j += 2;
12469
+ }
12470
+ return total;
12471
+ }
12472
+ function steps(lo, hi, eps, pitch) {
12473
+ const from = lo + eps;
12474
+ const to = hi - eps;
12475
+ if (to <= from) return { at: [], size: 0 };
12476
+ const n2 = Math.max(4, Math.ceil((to - from) / pitch));
12477
+ return {
12478
+ at: Array.from({ length: n2 }, (_, i) => from + (to - from) * (i + 0.5) / n2),
12479
+ size: (to - from) / n2
12480
+ };
12481
+ }
12482
+ function localLine(inverse, at, dir) {
12483
+ const p = apply(inverse, at[0], at[1], at[2]);
12484
+ const d2 = turn(inverse, dir);
12485
+ return [
12486
+ [p[0], p[1]],
12487
+ [d2[0], d2[1]]
12488
+ ];
12489
+ }
12490
+ function crossing(A, B, invA, invB, eps, pitch, volume) {
12491
+ const nA = normal(A.matrix);
12492
+ const nB = normal(B.matrix);
12493
+ const raw = cross(nA, nB);
12494
+ const size = Math.hypot(...raw);
12495
+ const line = [raw[0] / size, raw[1] / size, raw[2] / size];
12496
+ const side = cross(line, nA);
12497
+ const tilt = dot(nB, side);
12498
+ const oA = origin(A.matrix);
12499
+ const bLevel = dot(nB, origin(B.matrix));
12500
+ const depths = steps(0, A.thickness, eps, pitch);
12501
+ let total = 0;
12502
+ for (const depth of depths.at) {
12503
+ const base = add(oA, nA, depth);
12504
+ const b0 = (bLevel - dot(nB, base)) / tilt;
12505
+ const b1 = (bLevel + B.thickness - dot(nB, base)) / tilt;
12506
+ const alongs = steps(Math.min(b0, b1), Math.max(b0, b1), eps, pitch);
12507
+ for (const along of alongs.at) {
12508
+ const at = add(base, side, along);
12509
+ const [fromA, dirA] = localLine(invA, at, line);
12510
+ const [fromB, dirB] = localLine(invB, at, line);
12511
+ total += shared(spans(A.outline, fromA, dirA), spans(B.outline, fromB, dirB), eps) * depths.size * alongs.size;
12512
+ if (total > volume) return true;
12513
+ }
12514
+ }
12515
+ return false;
12516
+ }
12517
+ function stacked(A, B, invA, eps, pitch, volume) {
12518
+ const rel = [0, 1, 2, 3].flatMap(
12519
+ (r) => [0, 1, 2, 3].map((c) => {
12520
+ let v = 0;
12521
+ for (let k = 0; k < 4; k++) v += invA[r * 4 + k] * B.matrix[k * 4 + c];
12522
+ return v;
12523
+ })
12524
+ );
12525
+ const z0 = rel[11];
12526
+ const z1 = rel[11] + rel[10] * B.thickness;
12527
+ const depth = Math.min(A.thickness, Math.max(z0, z1)) - Math.max(0, Math.min(z0, z1));
12528
+ if (depth <= eps) return false;
12529
+ const moved = B.outline.map(
12530
+ (ring) => ring.map(
12531
+ ([x, y]) => [rel[0] * x + rel[1] * y + rel[3], rel[4] * x + rel[5] * y + rel[7]]
12532
+ )
12533
+ );
12534
+ const [ax0, ay0, ax1, ay1] = bounds2(A.outline);
12535
+ const [bx0, by0, bx1, by1] = bounds2(moved);
12536
+ const rows = steps(Math.max(ay0, by0), Math.min(ay1, by1), eps, pitch);
12537
+ let across = 0;
12538
+ for (const y of rows.at) {
12539
+ across += shared(spans(A.outline, [0, y], [1, 0]), spans(moved, [0, y], [1, 0]), eps) * rows.size * depth;
12540
+ if (across > volume) return true;
12541
+ }
12542
+ const columns = steps(Math.max(ax0, bx0), Math.min(ax1, bx1), eps, pitch);
12543
+ let down = 0;
12544
+ for (const x of columns.at) {
12545
+ down += shared(spans(A.outline, [x, 0], [0, 1]), spans(moved, [x, 0], [0, 1]), eps) * columns.size * depth;
12546
+ if (down > volume) return true;
12547
+ }
12548
+ return false;
12549
+ }
12550
+ function collisions(parts, maxPairs = Infinity, eps = 0.05, pitch = 0.25, volume = 1) {
12551
+ const boxes = parts.map(worldBox);
12552
+ const inverses = parts.map((p) => invertRigid(p.matrix));
12553
+ const order = parts.map((_, i) => i).sort((i, j) => boxes[i][0][0] - boxes[j][0][0]);
12554
+ const found = [];
12555
+ let checked = 0;
12556
+ for (let i = 0; i < order.length; i++)
12557
+ for (let j = i + 1; j < order.length; j++) {
12558
+ const a = order[i];
12559
+ const b = order[j];
12560
+ if (boxes[b][0][0] >= boxes[a][1][0] - eps) break;
12561
+ if (!overlaps(boxes[a], boxes[b], eps)) continue;
12562
+ if (++checked > maxPairs) return { found, complete: false };
12563
+ const [A, B] = a < b ? [parts[a], parts[b]] : [parts[b], parts[a]];
12564
+ const [invA, invB] = a < b ? [inverses[a], inverses[b]] : [inverses[b], inverses[a]];
12565
+ const parallel = Math.hypot(...cross(normal(A.matrix), normal(B.matrix))) < 1e-9;
12566
+ const hit = parallel ? stacked(A, B, invA, eps, pitch, volume) : crossing(A, B, invA, invB, eps, pitch, volume);
12567
+ if (hit) found.push([A, B]);
12568
+ }
12569
+ return { found, complete: true };
12570
+ }
12571
+
12305
12572
  // src/values.ts
12306
12573
  var isRange = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && v.type === "range";
12307
12574
  var isFn = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && v.type === "function";
@@ -12440,7 +12707,7 @@ function fmt(n2) {
12440
12707
  }
12441
12708
 
12442
12709
  // src/functions.ts
12443
- var RAD = Math.PI / 180;
12710
+ var RAD2 = Math.PI / 180;
12444
12711
  var def = (sig, run, order) => ({
12445
12712
  params: sig.replace("...", "").split(",").map((p) => p.trim()).filter(Boolean),
12446
12713
  rest: sig.endsWith("..."),
@@ -12506,8 +12773,8 @@ function argumentWork(name, args, max) {
12506
12773
  }
12507
12774
  return n2;
12508
12775
  }
12509
- var math1 = (f) => def("x", (a) => typeof a[0] === "number" ? clean(f(a[0])) : void 0);
12510
- function clean(x) {
12776
+ var math1 = (f) => def("x", (a) => typeof a[0] === "number" ? clean2(f(a[0])) : void 0);
12777
+ function clean2(x) {
12511
12778
  const r = Math.round(x);
12512
12779
  return Math.abs(x - r) < 1e-12 ? r : x;
12513
12780
  }
@@ -12538,16 +12805,16 @@ var FUNCTIONS = {
12538
12805
  // log(x) is base 10; log(base, x) takes the base first
12539
12806
  log: def(
12540
12807
  "x, base",
12541
- (a, c) => c.given < 2 ? clean(Math.log10(num(a[0], "log"))) : Math.log(num(a[0], "log")) / Math.log(num(a[1], "log")),
12808
+ (a, c) => c.given < 2 ? clean2(Math.log10(num(a[0], "log"))) : Math.log(num(a[0], "log")) / Math.log(num(a[1], "log")),
12542
12809
  (n2) => n2 >= 2 ? ["base", "x"] : ["x", "base"]
12543
12810
  ),
12544
- sin: math1((x) => Math.sin(x * RAD)),
12545
- cos: math1((x) => Math.cos(x * RAD)),
12546
- tan: math1((x) => Math.tan(x * RAD)),
12547
- asin: math1((x) => Math.asin(x) / RAD),
12548
- acos: math1((x) => Math.acos(x) / RAD),
12549
- atan: math1((x) => Math.atan(x) / RAD),
12550
- atan2: def("y, x", (a) => clean(Math.atan2(num(a[0], "atan2"), num(a[1], "atan2")) / RAD)),
12811
+ sin: math1((x) => Math.sin(x * RAD2)),
12812
+ cos: math1((x) => Math.cos(x * RAD2)),
12813
+ tan: math1((x) => Math.tan(x * RAD2)),
12814
+ asin: math1((x) => Math.asin(x) / RAD2),
12815
+ acos: math1((x) => Math.acos(x) / RAD2),
12816
+ atan: math1((x) => Math.atan(x) / RAD2),
12817
+ atan2: def("y, x", (a) => clean2(Math.atan2(num(a[0], "atan2"), num(a[1], "atan2")) / RAD2)),
12551
12818
  pow: def("x, y", (a) => Math.pow(num(a[0], "pow"), num(a[1], "pow"))),
12552
12819
  min: def(
12553
12820
  "values...",
@@ -14012,7 +14279,7 @@ var ClipperBase = class _ClipperBase {
14012
14279
  next.prevInAEL = prev;
14013
14280
  }
14014
14281
  getBounds() {
14015
- const bounds2 = {
14282
+ const bounds3 = {
14016
14283
  left: Number.MAX_SAFE_INTEGER,
14017
14284
  top: Number.MAX_SAFE_INTEGER,
14018
14285
  right: Number.MIN_SAFE_INTEGER,
@@ -14021,18 +14288,18 @@ var ClipperBase = class _ClipperBase {
14021
14288
  for (const t of this.vertexList) {
14022
14289
  let v = t;
14023
14290
  do {
14024
- if (v.pt.x < bounds2.left)
14025
- bounds2.left = v.pt.x;
14026
- if (v.pt.x > bounds2.right)
14027
- bounds2.right = v.pt.x;
14028
- if (v.pt.y < bounds2.top)
14029
- bounds2.top = v.pt.y;
14030
- if (v.pt.y > bounds2.bottom)
14031
- bounds2.bottom = v.pt.y;
14291
+ if (v.pt.x < bounds3.left)
14292
+ bounds3.left = v.pt.x;
14293
+ if (v.pt.x > bounds3.right)
14294
+ bounds3.right = v.pt.x;
14295
+ if (v.pt.y < bounds3.top)
14296
+ bounds3.top = v.pt.y;
14297
+ if (v.pt.y > bounds3.bottom)
14298
+ bounds3.bottom = v.pt.y;
14032
14299
  v = v.next;
14033
14300
  } while (v !== t);
14034
14301
  }
14035
- return Rect64Utils.isEmpty(bounds2) ? { left: 0, top: 0, right: 0, bottom: 0 } : bounds2;
14302
+ return Rect64Utils.isEmpty(bounds3) ? { left: 0, top: 0, right: 0, bottom: 0 } : bounds3;
14036
14303
  }
14037
14304
  executeInternal(ct, fillRule) {
14038
14305
  if (ct === ClipType.NoClip)
@@ -15247,17 +15514,17 @@ var ClipperBase = class _ClipperBase {
15247
15514
  return false;
15248
15515
  const maxCoord = InternalClipper.maxCoordForSafeCrossSq;
15249
15516
  if (Math.abs(a) < maxCoord && Math.abs(b) < maxCoord && Math.abs(c) < maxCoord && Math.abs(d2) < maxCoord) {
15250
- const cross2 = a * d2 - c * b;
15251
- return cross2 * cross2 / (c * c + d2 * d2) > 0.25;
15517
+ const cross3 = a * d2 - c * b;
15518
+ return cross3 * cross3 / (c * c + d2 * d2) > 0.25;
15252
15519
  }
15253
15520
  if (Number.isSafeInteger(a) && Number.isSafeInteger(b) && Number.isSafeInteger(c) && Number.isSafeInteger(d2)) {
15254
- const cross2 = BigInt(a) * BigInt(d2) - BigInt(c) * BigInt(b);
15255
- const crossSq = cross2 * cross2;
15521
+ const cross3 = BigInt(a) * BigInt(d2) - BigInt(c) * BigInt(b);
15522
+ const crossSq = cross3 * cross3;
15256
15523
  const denom = BigInt(c) * BigInt(c) + BigInt(d2) * BigInt(d2);
15257
15524
  return B42 * crossSq > denom;
15258
15525
  }
15259
- const cross = a * d2 - c * b;
15260
- return cross * cross / (c * c + d2 * d2) > 0.25;
15526
+ const cross2 = a * d2 - c * b;
15527
+ return cross2 * cross2 / (c * c + d2 * d2) > 0.25;
15261
15528
  }
15262
15529
  intersectEdges(ae1, ae2, pt) {
15263
15530
  let resultOp;
@@ -16442,8 +16709,8 @@ var ClipperOffset = class _ClipperOffset {
16442
16709
  y: Math.round(pt.y + offsetVec.y),
16443
16710
  z: ptz
16444
16711
  });
16445
- const steps = Math.ceil(this.stepsPerRad * Math.abs(angle));
16446
- for (let i = 1; i < steps; i++) {
16712
+ const steps2 = Math.ceil(this.stepsPerRad * Math.abs(angle));
16713
+ for (let i = 1; i < steps2; i++) {
16447
16714
  offsetVec = {
16448
16715
  x: offsetVec.x * this.stepCos - this.stepSin * offsetVec.y,
16449
16716
  y: offsetVec.x * this.stepSin + offsetVec.y * this.stepCos
@@ -16613,8 +16880,8 @@ var ClipperOffset = class _ClipperOffset {
16613
16880
  }
16614
16881
  const ptz = pt.z || 0;
16615
16882
  if (group.endType === EndType.Round) {
16616
- const steps = Math.ceil(this.stepsPerRad * 2 * Math.PI);
16617
- this.pathOut = _ClipperOffset.ellipse(pt, Math.abs(this.groupDelta), Math.abs(this.groupDelta), steps);
16883
+ const steps2 = Math.ceil(this.stepsPerRad * 2 * Math.PI);
16884
+ this.pathOut = _ClipperOffset.ellipse(pt, Math.abs(this.groupDelta), Math.abs(this.groupDelta), steps2);
16618
16885
  if (ptz !== 0)
16619
16886
  for (let i = 0; i < this.pathOut.length; i++)
16620
16887
  this.pathOut[i].z = ptz;
@@ -16672,20 +16939,20 @@ var ClipperOffset = class _ClipperOffset {
16672
16939
  static sqr(val) {
16673
16940
  return val * val;
16674
16941
  }
16675
- static ellipse(center, radiusX, radiusY = 0, steps = 0) {
16942
+ static ellipse(center, radiusX, radiusY = 0, steps2 = 0) {
16676
16943
  if (radiusX <= 0)
16677
16944
  return [];
16678
16945
  if (radiusY <= 0)
16679
16946
  radiusY = radiusX;
16680
- if (steps <= 2) {
16681
- steps = Math.ceil(Math.PI * Math.sqrt((radiusX + radiusY) / 2));
16947
+ if (steps2 <= 2) {
16948
+ steps2 = Math.ceil(Math.PI * Math.sqrt((radiusX + radiusY) / 2));
16682
16949
  }
16683
- const si = Math.sin(2 * Math.PI / steps);
16684
- const co = Math.cos(2 * Math.PI / steps);
16950
+ const si = Math.sin(2 * Math.PI / steps2);
16951
+ const co = Math.cos(2 * Math.PI / steps2);
16685
16952
  let dx = co;
16686
16953
  let dy = si;
16687
16954
  const result = [{ x: Math.round(center.x + radiusX), y: center.y }];
16688
- for (let i = 1; i < steps; ++i) {
16955
+ for (let i = 1; i < steps2; ++i) {
16689
16956
  result.push({
16690
16957
  x: Math.round(center.x + radiusX * dx),
16691
16958
  y: Math.round(center.y + radiusY * dy)
@@ -16831,7 +17098,7 @@ function area2(p) {
16831
17098
  a += (p[j][0] + p[i][0]) * (p[j][1] - p[i][1]);
16832
17099
  return -a / 2;
16833
17100
  }
16834
- function apply(m, [x, y]) {
17101
+ function apply2(m, [x, y]) {
16835
17102
  return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
16836
17103
  }
16837
17104
  function transformLayers(layers, m) {
@@ -16840,10 +17107,10 @@ function transformLayers(layers, m) {
16840
17107
  for (const [k, l] of layers) {
16841
17108
  out.set(k, {
16842
17109
  regions: l.regions.map((p) => {
16843
- const q = p.map((pt) => apply(m, pt));
17110
+ const q = p.map((pt) => apply2(m, pt));
16844
17111
  return flip ? q.reverse() : q;
16845
17112
  }),
16846
- lines: l.lines.map((p) => p.map((pt) => apply(m, pt)))
17113
+ lines: l.lines.map((p) => p.map((pt) => apply2(m, pt)))
16847
17114
  });
16848
17115
  }
16849
17116
  return out;
@@ -16907,17 +17174,17 @@ function layerBounds(layers, skip) {
16907
17174
  function hull(points) {
16908
17175
  const pts = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
16909
17176
  if (pts.length < 3) return pts;
16910
- const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
17177
+ const cross2 = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
16911
17178
  const lower = [];
16912
17179
  for (const p of pts) {
16913
- while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
17180
+ while (lower.length >= 2 && cross2(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
16914
17181
  lower.pop();
16915
17182
  lower.push(p);
16916
17183
  }
16917
17184
  const upper = [];
16918
17185
  for (let i = pts.length - 1; i >= 0; i--) {
16919
17186
  const p = pts[i];
16920
- while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
17187
+ while (upper.length >= 2 && cross2(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
16921
17188
  upper.pop();
16922
17189
  upper.push(p);
16923
17190
  }
@@ -17562,7 +17829,7 @@ var Parser = class _Parser {
17562
17829
 
17563
17830
  // src/stdlib.ts
17564
17831
  var LIBRARIES = {
17565
- "kerf/box": '// kerf/box: finger-jointed boxes.\n// Copyright (c) 2026 MyLaserTools. AGPL-3.0-only or MyLaserTools commercial licence.\n\n// Box from its outer size [width, depth, height] in the current material.\n// Edges pair "out" with "in", and the bottom owns every bottom corner,\n// so the six panels interlock without gaps.\nmodule box(size, lid = false, finger = undef) {\n w = size[0];\n d = size[1];\n h = size[2];\n top = lid ? "in" : "flat";\n part("bottom") finger_rect([w, d], "out", finger = finger);\n if (lid) part("lid") finger_rect([w, d], "out", finger = finger);\n part("front") finger_rect([w, h], ["in", "out", top, "out"], finger = finger);\n part("back") finger_rect([w, h], ["in", "out", top, "out"], finger = finger);\n part("left") finger_rect([d, h], ["in", "in", top, "in"], finger = finger);\n part("right") finger_rect([d, h], ["in", "in", top, "in"], finger = finger);\n}\n\n// Divider that slides between two walls: tabs on both sides drop into\n// finger_holes() cut in the walls.\nmodule divider(size, finger = undef) {\n finger_rect(size, ["flat", "out", "flat", "out"], finger = finger);\n}\n'
17832
+ "kerf/box": '// kerf/box: finger-jointed boxes.\n// Copyright (c) 2026 MyLaserTools. AGPL-3.0-only or MyLaserTools commercial licence.\n\n// Box from its outer size [width, depth, height] in the current material.\n// Edges pair "out" with "in", and the bottom owns every bottom corner,\n// so the six panels interlock without gaps.\nmodule box(size, lid = false, finger = undef) {\n w = size[0];\n d = size[1];\n h = size[2];\n t = $thickness;\n top = lid ? "in" : "flat";\n part("bottom", at = [0, 0, 0]) finger_rect([w, d], "out", finger = finger);\n if (lid) part("lid", at = [0, 0, h - t]) finger_rect([w, d], "out", finger = finger);\n part("front", at = [0, t, 0], rotate = [90, 0, 0])\n finger_rect([w, h], ["in", "out", top, "out"], finger = finger);\n part("back", at = [0, d, 0], rotate = [90, 0, 0])\n finger_rect([w, h], ["in", "out", top, "out"], finger = finger);\n part("left", rotate = [90, 0, 90])\n finger_rect([d, h], ["in", "in", top, "in"], finger = finger);\n part("right", at = [w - t, 0, 0], rotate = [90, 0, 90])\n finger_rect([d, h], ["in", "in", top, "in"], finger = finger);\n}\n\n// Divider that slides between two walls: tabs on both sides drop into\n// finger_holes() cut in the walls.\nmodule divider(size, finger = undef) {\n finger_rect(size, ["flat", "out", "flat", "out"], finger = finger);\n}\n'
17566
17833
  };
17567
17834
 
17568
17835
  // src/paths.ts
@@ -17576,7 +17843,7 @@ function flatten(cmds, tolerance = 0.02) {
17576
17843
  if (cur.length >= 2) (close ? closed : open).push(cur);
17577
17844
  cur = [];
17578
17845
  };
17579
- const steps = (len) => Math.max(2, Math.min(256, Math.ceil(Math.sqrt(len / tolerance))));
17846
+ const steps2 = (len) => Math.max(2, Math.min(256, Math.ceil(Math.sqrt(len / tolerance))));
17580
17847
  for (const c of cmds) {
17581
17848
  if (c.type === "M") {
17582
17849
  finish(false);
@@ -17587,7 +17854,7 @@ function flatten(cmds, tolerance = 0.02) {
17587
17854
  cur.push(last);
17588
17855
  } else if (c.type === "Q") {
17589
17856
  const [x0, y0] = last;
17590
- const n2 = steps(Math.hypot(c.x1 - x0, c.y1 - y0) + Math.hypot(c.x - c.x1, c.y - c.y1));
17857
+ const n2 = steps2(Math.hypot(c.x1 - x0, c.y1 - y0) + Math.hypot(c.x - c.x1, c.y - c.y1));
17591
17858
  for (let i = 1; i <= n2; i++) {
17592
17859
  const t = i / n2, u = 1 - t;
17593
17860
  cur.push([
@@ -17598,7 +17865,7 @@ function flatten(cmds, tolerance = 0.02) {
17598
17865
  last = [c.x, c.y];
17599
17866
  } else if (c.type === "C") {
17600
17867
  const [x0, y0] = last;
17601
- const n2 = steps(
17868
+ const n2 = steps2(
17602
17869
  Math.hypot(c.x1 - x0, c.y1 - y0) + Math.hypot(c.x2 - c.x1, c.y2 - c.y1) + Math.hypot(c.x - c.x2, c.y - c.y2)
17603
17870
  );
17604
17871
  for (let i = 1; i <= n2; i++) {
@@ -17869,21 +18136,22 @@ var DEFAULT_LIMITS = {
17869
18136
  maxTextLength: 2e3,
17870
18137
  maxPoints: 2e6,
17871
18138
  maxOutputPoints: 1e6,
17872
- maxSheets: 200
18139
+ maxSheets: 200,
18140
+ maxCollisionPairs: 2e4
17873
18141
  };
17874
18142
  var SHAPE_COST = 100;
17875
18143
  var LIST_COST = 4;
17876
18144
  var CHARS_PER_ITEM = 8;
17877
18145
  var shapePoints = (s) => {
17878
18146
  let n2 = SHAPE_COST;
17879
- const add = (layers) => {
18147
+ const add2 = (layers) => {
17880
18148
  for (const l of layers.values()) {
17881
18149
  for (const p of l.regions) n2 += p.length;
17882
18150
  for (const p of l.lines) n2 += p.length;
17883
18151
  }
17884
18152
  };
17885
- add(s.layers);
17886
- for (const p of s.parts) add(p.layers);
18153
+ add2(s.layers);
18154
+ for (const p of s.parts) add2(p.layers);
17887
18155
  return n2;
17888
18156
  };
17889
18157
  var Env = class {
@@ -18397,7 +18665,7 @@ var Evaluator = class {
18397
18665
  const max = this.limits.maxListLength;
18398
18666
  const parts = [];
18399
18667
  let n2 = 0;
18400
- const add = (label, v, quote) => {
18668
+ const add2 = (label, v, quote) => {
18401
18669
  if (parts.length) n2 += 2;
18402
18670
  const r = write(v, max - n2 - label.length, quote);
18403
18671
  n2 += label.length + r.text.length;
@@ -18408,8 +18676,8 @@ var Evaluator = class {
18408
18676
  );
18409
18677
  parts.push(label + r.text);
18410
18678
  };
18411
- for (const v of a.pos) add("", v, false);
18412
- for (const [k, v] of a.named) add(`${k} = `, v, true);
18679
+ for (const v of a.pos) add2("", v, false);
18680
+ for (const [k, v] of a.named) add2(`${k} = `, v, true);
18413
18681
  return parts.join(", ");
18414
18682
  }
18415
18683
  arith(op, a, b, at) {
@@ -18512,6 +18780,34 @@ var Evaluator = class {
18512
18780
  return [v[0], v[1] ?? 0];
18513
18781
  throw new Error(`${name} must be a number or [x, y], got ${show(v)}`);
18514
18782
  }
18783
+ vec3(v, name) {
18784
+ const ok = (x) => typeof x === "number" && Number.isFinite(x);
18785
+ if (Array.isArray(v) && (v.length === 2 || v.length === 3) && v.every(ok))
18786
+ return [v[0], v[1], v[2] ?? 0];
18787
+ throw new Error(`${name} must be [x, y, z], got ${show(v)}`);
18788
+ }
18789
+ placements(at, turn2, qty) {
18790
+ const list2 = (v, name) => {
18791
+ if (v === void 0) return [[0, 0, 0]];
18792
+ if (Array.isArray(v) && v.length > 0 && Array.isArray(v[0]))
18793
+ return v.map((item, i) => this.vec3(item, `${name}[${i}]`));
18794
+ return [this.vec3(v, name)];
18795
+ };
18796
+ const ats = list2(at, "at");
18797
+ const turns = list2(turn2, "rotate");
18798
+ for (const [items, name] of [
18799
+ [ats, "at"],
18800
+ [turns, "rotate"]
18801
+ ])
18802
+ if (items.length !== 1 && items.length !== qty)
18803
+ throw new Error(
18804
+ `${name} needs one placement per copy: qty is ${qty} but ${name} has ${items.length}`
18805
+ );
18806
+ return Array.from({ length: qty }, (_, i) => ({
18807
+ at: ats[ats.length === 1 ? 0 : i],
18808
+ rotate: turns[turns.length === 1 ? 0 : i]
18809
+ }));
18810
+ }
18515
18811
  points(v, name) {
18516
18812
  if (!Array.isArray(v)) throw new Error(`${name} must be a list of [x, y] points`);
18517
18813
  return v.map((p, i) => {
@@ -18832,6 +19128,9 @@ var Evaluator = class {
18832
19128
  this.copies += qty;
18833
19129
  if (!(this.copies <= this.limits.maxParts))
18834
19130
  throw new Error(`more than ${this.limits.maxParts} part copies in total, the limit`);
19131
+ const at = this.arg(c.args, "at", -1);
19132
+ const turn2 = this.arg(c.args, "rotate", -1);
19133
+ const placements = at === void 0 && turn2 === void 0 ? void 0 : this.placements(at, turn2, qty);
18835
19134
  const s = c.children();
18836
19135
  return {
18837
19136
  layers: /* @__PURE__ */ new Map(),
@@ -18843,7 +19142,8 @@ var Evaluator = class {
18843
19142
  layers: s.layers,
18844
19143
  thickness: this.thickness(c.env),
18845
19144
  kerf: toNumber(this.dyn(c.env, "$kerf")) || 0,
18846
- material: toText(this.dyn(c.env, "$material") ?? "material")
19145
+ material: toText(this.dyn(c.env, "$material") ?? "material"),
19146
+ ...placements ? { placements } : {}
18847
19147
  }
18848
19148
  ]
18849
19149
  };
@@ -19189,6 +19489,9 @@ function render(source, options = {}) {
19189
19489
  });
19190
19490
  if (!parts.length) warnings.push("Nothing to cut: the program made no shapes.");
19191
19491
  const infos = [];
19492
+ const assembly = [];
19493
+ const loose = [];
19494
+ const stackedCopies = /* @__PURE__ */ new Set();
19192
19495
  const ready = [];
19193
19496
  for (const p of parts) {
19194
19497
  const material = union2(
@@ -19209,6 +19512,30 @@ function render(source, options = {}) {
19209
19512
  }
19210
19513
  if (regions.length || lines.length) layers.set(k, { regions, lines });
19211
19514
  }
19515
+ if (material.length) {
19516
+ const flat = { name: p.name, thickness: p.thickness, outline: material };
19517
+ if (p.placements) {
19518
+ const seen = /* @__PURE__ */ new Set();
19519
+ p.placements.forEach((placement, i) => {
19520
+ const part = { ...flat, copy: i + 1, matrix: placementMatrix(placement), placed: true };
19521
+ assembly.push(part);
19522
+ const key = part.matrix.map((v) => v.toFixed(6)).join(",");
19523
+ if (seen.has(key)) stackedCopies.add(part);
19524
+ seen.add(key);
19525
+ });
19526
+ if (seen.size < p.placements.length)
19527
+ warnings.push(
19528
+ `Copies of part "${p.name}" are in the same place in the assembly. Give at a list with one placement per copy to spread them out.`
19529
+ );
19530
+ } else
19531
+ for (let i = 0; i < p.qty; i++)
19532
+ loose.push({
19533
+ ...flat,
19534
+ copy: i + 1,
19535
+ matrix: placementMatrix({ at: [0, 0, 0], rotate: [0, 0, 0] }),
19536
+ placed: false
19537
+ });
19538
+ }
19212
19539
  const n2 = pieces(material).length;
19213
19540
  if (n2 > 1)
19214
19541
  warnings.push(
@@ -19250,6 +19577,27 @@ function render(source, options = {}) {
19250
19577
  infos.push(info);
19251
19578
  ready.push({ info, layers });
19252
19579
  }
19580
+ const { found: clashes, complete } = collisions(
19581
+ assembly.filter((part) => !stackedCopies.has(part)),
19582
+ limits.maxCollisionPairs
19583
+ );
19584
+ if (!complete)
19585
+ warnings.push(
19586
+ `The assembly has more than ${limits.maxCollisionPairs} pairs of touching parts, the limit, so only those were checked for collisions.`
19587
+ );
19588
+ for (const [a, b] of clashes.slice(0, 5))
19589
+ warnings.push(
19590
+ `Parts "${a.name}"${a.copy > 1 ? ` (copy ${a.copy})` : ""} and "${b.name}"${b.copy > 1 ? ` (copy ${b.copy})` : ""} collide in the assembly. Check their edges and where they are placed.`
19591
+ );
19592
+ if (clashes.length > 5)
19593
+ warnings.push(`${clashes.length - 5} more pairs of parts collide in the assembly.`);
19594
+ if (assembly.length && loose.length) {
19595
+ const names = [...new Set(loose.map((p) => `"${p.name}"`))];
19596
+ warnings.push(
19597
+ `${names.length === 1 ? "Part" : "Parts"} ${names.slice(0, 5).join(", ")}${names.length > 5 ? ` and ${names.length - 5} more` : ""} ${names.length === 1 ? "has" : "have"} no at or rotate, so ${names.length === 1 ? "it lies" : "they lie"} flat beside the assembly.`
19598
+ );
19599
+ assembly.push(...besideAssembly(assembly, loose));
19600
+ }
19253
19601
  let outputPoints = 0;
19254
19602
  for (const r of ready) {
19255
19603
  let n2 = 0;
@@ -19396,7 +19744,7 @@ function render(source, options = {}) {
19396
19744
  };
19397
19745
  }
19398
19746
  const single = sheets.length === 1 ? sheets[0] : void 0;
19399
- const stacked = single ? [] : sheets.flatMap((s, i) => {
19747
+ const stacked2 = single ? [] : sheets.flatMap((s, i) => {
19400
19748
  const dy = sheets.slice(0, i).reduce((acc, p) => acc + p.height + 10, 0);
19401
19749
  return s.items.map((it) => ({ ...it, layers: moved(it.layers, [1, 0, 0, 1, 0, dy]) }));
19402
19750
  });
@@ -19404,8 +19752,8 @@ function render(source, options = {}) {
19404
19752
  const height = round2(
19405
19753
  Math.max(1, sheets.reduce((acc, s) => acc + s.height, 0) + 10 * Math.max(0, sheets.length - 1))
19406
19754
  );
19407
- const svg = single ? single.svg : toSvg(stacked, width, height, layerList);
19408
- const paths = single ? single.paths : layerPaths(stacked, layerList);
19755
+ const svg = single ? single.svg : toSvg(stacked2, width, height, layerList);
19756
+ const paths = single ? single.paths : layerPaths(stacked2, layerList);
19409
19757
  return {
19410
19758
  sheets,
19411
19759
  width: single?.width ?? width,
@@ -19413,6 +19761,7 @@ function render(source, options = {}) {
19413
19761
  paths,
19414
19762
  svg,
19415
19763
  parts: infos,
19764
+ assembly,
19416
19765
  layers: layerList,
19417
19766
  warnings,
19418
19767
  echoes: res.echoes
@@ -19503,7 +19852,7 @@ function parameters(source) {
19503
19852
  }
19504
19853
 
19505
19854
  // src/index.ts
19506
- var VERSION = "0.3.0";
19855
+ var VERSION = "0.4.0";
19507
19856
  var LANGUAGE_VERSION = "0.1";
19508
19857
 
19509
19858
  // fonts/AtkinsonHyperlegible-Regular.ttf