@ikijs/editor 0.30.1 → 0.32.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/index.mjs CHANGED
@@ -1023,6 +1023,14 @@ var DeletePart = class {
1023
1023
  `parts."${this.partId}": cannot delete \u2014 used as a clip mask by part "${masker.id}"; remove its clip first`
1024
1024
  );
1025
1025
  }
1026
+ const occluded = parts.find(
1027
+ (p) => p.id !== this.partId && p.occlusion?.by.includes(this.partId)
1028
+ );
1029
+ if (occluded) {
1030
+ throw new Error(
1031
+ `parts."${this.partId}": cannot delete \u2014 used as an occluder by part "${occluded.id}"; remove its occlusion first`
1032
+ );
1033
+ }
1026
1034
  const i = parts.indexOf(part);
1027
1035
  if (!this.captured) {
1028
1036
  this.removed = structuredClone(part);
@@ -1227,6 +1235,14 @@ var SetPartMesh = class {
1227
1235
  `parts."${this.partId}": cannot remove mesh \u2014 used as a clip mask by part "${masker.id}" (masks require a mesh); remove its clip first`
1228
1236
  );
1229
1237
  }
1238
+ const occluded = doc.getModel().parts.find(
1239
+ (p) => p.id !== this.partId && p.occlusion?.by.includes(this.partId)
1240
+ );
1241
+ if (occluded) {
1242
+ throw new IkiFormatError(
1243
+ `parts."${this.partId}": cannot remove mesh \u2014 used as an occluder by part "${occluded.id}" (occluders require a mesh); remove its occlusion first`
1244
+ );
1245
+ }
1230
1246
  } else {
1231
1247
  if ((part.warps?.length ?? 0) > 0) {
1232
1248
  throw new IkiFormatError(
@@ -1306,6 +1322,14 @@ function validatePhysicsCandidate(doc, candidatePhysics) {
1306
1322
  }
1307
1323
  throw e;
1308
1324
  }
1325
+ for (const rig of candidatePhysics) {
1326
+ const part = doc.getModel().parts.find((p) => p.occlusion?.parameter === rig.output.parameter);
1327
+ if (part !== void 0) {
1328
+ throw new IkiFormatError(
1329
+ `physics."${rig.id}".output.parameter "${rig.output.parameter}" keys parts."${part.id}".occlusion; an occlusion amount cannot be keyed on a physics output`
1330
+ );
1331
+ }
1332
+ }
1309
1333
  }
1310
1334
  var AddPhysicsRig = class {
1311
1335
  label = "Add physics rig";
@@ -2114,6 +2138,18 @@ var PatchError = class extends Error {
2114
2138
  this.name = "PatchError";
2115
2139
  }
2116
2140
  };
2141
+ var CustomPartError = class extends Error {
2142
+ constructor(message) {
2143
+ super(message);
2144
+ this.name = "CustomPartError";
2145
+ }
2146
+ };
2147
+ var BakeError = class extends Error {
2148
+ constructor(message) {
2149
+ super(message);
2150
+ this.name = "BakeError";
2151
+ }
2152
+ };
2117
2153
 
2118
2154
  // src/auto-rig/layout.ts
2119
2155
  function boxOfLayer(layer) {
@@ -2522,10 +2558,10 @@ function checkSidesHaveFront(roles) {
2522
2558
  function checkMouth(roles) {
2523
2559
  const has = new Set(roles);
2524
2560
  const present = LIP_ROLES.filter((r) => has.has(r));
2525
- const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
2526
- if (inside.length > 0 && present.length === 0) {
2561
+ const inside2 = LIP_INSIDE_ROLES.filter((r) => has.has(r));
2562
+ if (inside2.length > 0 && present.length === 0) {
2527
2563
  throw new Error(
2528
- `auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
2564
+ `auto-rig: ${inside2[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
2529
2565
  );
2530
2566
  }
2531
2567
  if (present.length === 0) {
@@ -2745,10 +2781,10 @@ function mouthFold(role, opening) {
2745
2781
  const stackThin = (c, y) => y > c.Tu ? c.Tu + UPPER_THIN * (y - c.Tu) : y;
2746
2782
  return (col, y, v) => {
2747
2783
  const k = 1 - v;
2748
- const inside = col >= x0 && col <= x1;
2784
+ const inside2 = col >= x0 && col <= x1;
2749
2785
  switch (role) {
2750
2786
  case "lip_lower": {
2751
- if (!inside) return [0, 0];
2787
+ if (!inside2) return [0, 0];
2752
2788
  const c = opening.at(col);
2753
2789
  return [0, (c.seam + c.overlap - c.Bl) * k];
2754
2790
  }
@@ -2772,9 +2808,9 @@ function mouthFold(role, opening) {
2772
2808
  line = opening.line(c += col < x0 ? 1 : -1);
2773
2809
  const thin = line.bottom + UPPER_THIN * (y - line.bottom);
2774
2810
  const mid = line.bottom + UPPER_THIN * (line.top - line.bottom) / 2;
2775
- const travel = inside ? opening.at(col).seam - line.bottom : 0;
2811
+ const travel = inside2 ? opening.at(col).seam - line.bottom : 0;
2776
2812
  const shut = mid + line.squash * (thin - mid) + travel;
2777
- const pull = inside ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
2813
+ const pull = inside2 ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
2778
2814
  return [pull * k, thin - y + (shut - thin) * k];
2779
2815
  }
2780
2816
  }
@@ -4107,14 +4143,14 @@ function cuesAt(ctx, land2, ax) {
4107
4143
  const l0 = land2(L.role, L.lo, L.y, ax);
4108
4144
  const l1 = land2(L.role, L.hi, L.y, ax);
4109
4145
  const r0 = land2(R.role, R.lo, R.y, ax);
4110
- const r1 = land2(R.role, R.hi, R.y, ax);
4146
+ const r12 = land2(R.role, R.hi, R.y, ax);
4111
4147
  const wl = l1 - l0;
4112
- const wr = r1 - r0;
4148
+ const wr = r12 - r0;
4113
4149
  const wl0 = L.hi - L.lo;
4114
4150
  const wr0 = R.hi - R.lo;
4115
4151
  const ratio = ax < 0 ? wl / wr / (wl0 / wr0) : wr / wl / (wr0 / wl0);
4116
4152
  const pc0 = (L.lo + L.hi + R.lo + R.hi) / 4;
4117
- const pc = (l0 + l1 + r0 + r1) / 4;
4153
+ const pc = (l0 + l1 + r0 + r12) / 4;
4118
4154
  const y = ctx.frame.eyeY;
4119
4155
  const hl0 = Math.min(...ctx.candidates.left.map((c) => c.x));
4120
4156
  const hr0 = Math.max(...ctx.candidates.right.map((c) => c.x));
@@ -4752,7 +4788,7 @@ function blendStops(values, offsets, v) {
4752
4788
  const t = (x - values[i]) / (values[i + 1] - values[i]);
4753
4789
  return offsets[i].map((o, k) => o + (offsets[i + 1][k] - o) * t);
4754
4790
  }
4755
- function land(l, offsets, x, y) {
4791
+ function bindToRest(l, x, y) {
4756
4792
  const { cols, rows, points } = l;
4757
4793
  const stride = cols + 1;
4758
4794
  let col = cols - 1;
@@ -4773,8 +4809,17 @@ function land(l, offsets, x, y) {
4773
4809
  const x1 = points[(col + 1) * 2];
4774
4810
  const yT = points[row * stride * 2 + 1];
4775
4811
  const yB = points[(row + 1) * stride * 2 + 1];
4776
- const s = clamp((x - x0) / (x1 - x0), 0, 1);
4777
- const t = clamp((yT - y) / (yT - yB), 0, 1);
4812
+ return {
4813
+ row,
4814
+ col,
4815
+ s: clamp((x - x0) / (x1 - x0), 0, 1),
4816
+ t: clamp((yT - y) / (yT - yB), 0, 1)
4817
+ };
4818
+ }
4819
+ function land(l, offsets, x, y) {
4820
+ const { cols, points } = l;
4821
+ const stride = cols + 1;
4822
+ const { row, col, s, t } = bindToRest(l, x, y);
4778
4823
  const at = (r, c) => {
4779
4824
  const k = (r * stride + c) * 2;
4780
4825
  return [points[k] + offsets[k], points[k + 1] + offsets[k + 1]];
@@ -5114,7 +5159,7 @@ function armGeometry(layer, bodyAxisX) {
5114
5159
  b.x1 - elbow.x,
5115
5160
  b.y1 - elbow.y
5116
5161
  );
5117
- const inside = (x, y) => {
5162
+ const inside2 = (x, y) => {
5118
5163
  const k = Math.floor(y) - bbox.y;
5119
5164
  if (k < 0 || k >= bbox.h) return false;
5120
5165
  const [rs, re] = span3(k);
@@ -5123,7 +5168,7 @@ function armGeometry(layer, bodyAxisX) {
5123
5168
  const capRays = Array.from({ length: CAP_SEGMENTS + 1 }, (_, i) => {
5124
5169
  const a = Math.PI * i / CAP_SEGMENTS;
5125
5170
  let t = 1;
5126
- while (t <= capRadius + CAP_INSET && inside(
5171
+ while (t <= capRadius + CAP_INSET && inside2(
5127
5172
  elbow.x + canvasW / 2 + t * Math.cos(a),
5128
5173
  canvasH / 2 - elbow.y - t * Math.sin(a)
5129
5174
  )) {
@@ -5325,6 +5370,675 @@ function forearmPoseDeformer(role, arm) {
5325
5370
  };
5326
5371
  }
5327
5372
 
5373
+ // src/auto-rig/chain.ts
5374
+ function multiply(a, b) {
5375
+ return [
5376
+ a[0] * b[0] + a[2] * b[1],
5377
+ a[1] * b[0] + a[3] * b[1],
5378
+ a[0] * b[2] + a[2] * b[3],
5379
+ a[1] * b[2] + a[3] * b[3],
5380
+ a[0] * b[4] + a[2] * b[5] + a[4],
5381
+ a[1] * b[4] + a[3] * b[5] + a[5]
5382
+ ];
5383
+ }
5384
+ function valueOf(parameters, values, id) {
5385
+ const p = parameters.find((q) => q.id === id);
5386
+ if (p === void 0) return 0;
5387
+ const v = values[id] ?? p.default;
5388
+ return Math.min(p.max, Math.max(p.min, v));
5389
+ }
5390
+ function ownMatrix(d, parameters, values) {
5391
+ let x = d.transform?.x ?? 0;
5392
+ let y = d.transform?.y ?? 0;
5393
+ let rotation = d.transform?.rotation ?? 0;
5394
+ let sx = d.transform?.scaleX ?? 1;
5395
+ let sy = d.transform?.scaleY ?? 1;
5396
+ for (const b of d.bindings ?? []) {
5397
+ const p = parameters.find((q) => q.id === b.parameter);
5398
+ const t = p === void 0 || p.max === p.min ? 0 : (valueOf(parameters, values, b.parameter) - p.min) / (p.max - p.min);
5399
+ const v = b.from + (b.to - b.from) * t;
5400
+ if (b.channel === "translateX") x += v;
5401
+ else if (b.channel === "translateY") y += v;
5402
+ else if (b.channel === "rotate") rotation += v;
5403
+ else if (b.channel === "scaleX") sx += v;
5404
+ else if (b.channel === "scaleY") sy += v;
5405
+ }
5406
+ const r = rotation * Math.PI / 180;
5407
+ const c = Math.cos(r);
5408
+ const s = Math.sin(r);
5409
+ const trs = [c * sx, s * sx, -s * sy, c * sy, x, y];
5410
+ return multiply(multiply([1, 0, 0, 1, d.pivot.x, d.pivot.y], trs), [
5411
+ 1,
5412
+ 0,
5413
+ 0,
5414
+ 1,
5415
+ -d.pivot.x,
5416
+ -d.pivot.y
5417
+ ]);
5418
+ }
5419
+ function localGrid(w, parameters, values) {
5420
+ const points = [...w.grid.points];
5421
+ for (const warp of w.warps ?? []) {
5422
+ const offsets = blendStops(
5423
+ warp.keyforms.map((k) => k.value),
5424
+ warp.keyforms.map((k) => k.offsets),
5425
+ valueOf(parameters, values, warp.parameter)
5426
+ );
5427
+ for (let i = 0; i < points.length; i++) points[i] += offsets[i];
5428
+ }
5429
+ return points;
5430
+ }
5431
+ function riddenFrame(pivot, w, q) {
5432
+ const { cols, points: rest } = w.grid;
5433
+ const stride = cols + 1;
5434
+ const { row, col, s, t } = bindToRest(w.grid, pivot.x, pivot.y);
5435
+ const xl = rest[col * 2];
5436
+ const xr = rest[(col + 1) * 2];
5437
+ const yt = rest[row * stride * 2 + 1];
5438
+ const yb = rest[(row + 1) * stride * 2 + 1];
5439
+ const i00 = (row * stride + col) * 2;
5440
+ const i10 = (row * stride + col + 1) * 2;
5441
+ const i01 = ((row + 1) * stride + col) * 2;
5442
+ const i11 = ((row + 1) * stride + col + 1) * 2;
5443
+ const lerp = (a, b, k) => a + (b - a) * k;
5444
+ const topX = lerp(q[i00], q[i10], s);
5445
+ const topY = lerp(q[i00 + 1], q[i10 + 1], s);
5446
+ const botX = lerp(q[i01], q[i11], s);
5447
+ const botY = lerp(q[i01 + 1], q[i11 + 1], s);
5448
+ const px = lerp(topX, botX, t);
5449
+ const py = lerp(topY, botY, t);
5450
+ const width = xr - xl;
5451
+ const height = yt - yb;
5452
+ const exX = lerp(q[i10] - q[i00], q[i11] - q[i01], t) / width;
5453
+ const exY = lerp(q[i10 + 1] - q[i00 + 1], q[i11 + 1] - q[i01 + 1], t) / width;
5454
+ const eyX = -lerp(q[i01] - q[i00], q[i11] - q[i10], s) / height;
5455
+ const eyY = -lerp(q[i01 + 1] - q[i00 + 1], q[i11 + 1] - q[i10 + 1], s) / height;
5456
+ const theta = Math.atan2(exY - eyX, exX + eyY);
5457
+ const c = Math.cos(theta);
5458
+ const sn = Math.sin(theta);
5459
+ return [
5460
+ c,
5461
+ sn,
5462
+ -sn,
5463
+ c,
5464
+ px - (c * pivot.x - sn * pivot.y),
5465
+ py - (sn * pivot.x + c * pivot.y)
5466
+ ];
5467
+ }
5468
+ function matrixWorldAt(deformers2, parameters, id, values = {}) {
5469
+ for (const key of Object.keys(values)) {
5470
+ if (!parameters.some((q) => q.id === key)) {
5471
+ throw new Error(`chain: unknown parameter "${key}"`);
5472
+ }
5473
+ }
5474
+ const d = deformers2.find((x) => x.id === id);
5475
+ if (d === void 0) throw new Error(`chain: no deformer "${id}"`);
5476
+ if (d.kind === "warp") {
5477
+ throw new Error(`chain: "${id}" is a warp deformer, not a matrix`);
5478
+ }
5479
+ const own = ownMatrix(d, parameters, values);
5480
+ if (d.parent === void 0) return own;
5481
+ const parent = deformers2.find((x) => x.id === d.parent);
5482
+ if (parent === void 0) {
5483
+ throw new Error(`chain: "${id}" has an unknown parent "${d.parent}"`);
5484
+ }
5485
+ if (parent.kind !== "warp") {
5486
+ throw new Error(
5487
+ `chain: "${id}" hangs from the matrix deformer "${parent.id}"; only a root warp is supported`
5488
+ );
5489
+ }
5490
+ if (parent.parent !== void 0) {
5491
+ throw new Error(
5492
+ `chain: "${id}" hangs from warp "${parent.id}", which has a parent "${parent.parent}"; only a root warp is supported`
5493
+ );
5494
+ }
5495
+ if (parent.warp2d !== void 0) {
5496
+ throw new Error(
5497
+ `chain: "${id}" hangs from warp "${parent.id}", which carries a warp2d; only 1D warps are supported`
5498
+ );
5499
+ }
5500
+ return multiply(
5501
+ riddenFrame(d.pivot, parent, localGrid(parent, parameters, values)),
5502
+ own
5503
+ );
5504
+ }
5505
+
5506
+ // src/auto-rig/bakes.ts
5507
+ var TARGET_KEYS = [
5508
+ "id",
5509
+ "param",
5510
+ "parts",
5511
+ "box",
5512
+ "grid",
5513
+ "stops",
5514
+ "mirror",
5515
+ "why"
5516
+ ];
5517
+ var BOX_KEYS = ["x", "y", "w", "h"];
5518
+ var MAX_VERTICES = 65536;
5519
+ var CELL_PX = [4, 400];
5520
+ var MAX_MESH_COLS = 64;
5521
+ var MAX_MESH_ROWS = 400;
5522
+ var DEFAULT_BOX_MARGIN = 2;
5523
+ var SAME_STOP = 1e-9;
5524
+ var PLAIN_DECIMAL = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
5525
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
5526
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5527
+ var show = (v) => typeof v === "number" ? String(v) : JSON.stringify(v) ?? String(v);
5528
+ function mirrorLattice(lattice2, cols, rows) {
5529
+ const out = new Array(lattice2.length);
5530
+ for (let r = 0; r <= rows; r++) {
5531
+ for (let c = 0; c <= cols; c++) {
5532
+ const [dx, dy] = lattice2[r * (cols + 1) + (cols - c)];
5533
+ out[r * (cols + 1) + c] = [-dx, dy];
5534
+ }
5535
+ }
5536
+ return out;
5537
+ }
5538
+ var r1 = (v) => Math.round(v * 10) / 10;
5539
+ var findStop = (stops, v) => [...stops.keys()].find((k) => Math.abs(k - v) <= SAME_STOP);
5540
+ function checkGrid(at, grid) {
5541
+ if (!isObject(grid)) {
5542
+ throw new BakeError(`${at}.grid must be { cols, rows }, got ${show(grid)}`);
5543
+ }
5544
+ checkedKeys(`${at}.grid`, grid, ["cols", "rows"], BakeError);
5545
+ const out = { cols: 0, rows: 0 };
5546
+ for (const key of ["cols", "rows"]) {
5547
+ const v = checkedNumber(
5548
+ `${at}.grid.${key}`,
5549
+ grid[key],
5550
+ 1,
5551
+ 32,
5552
+ false,
5553
+ BakeError
5554
+ );
5555
+ if (!Number.isInteger(v)) {
5556
+ throw new BakeError(`${at}.grid.${key} must be an integer, got ${v}`);
5557
+ }
5558
+ out[key] = v;
5559
+ }
5560
+ return out;
5561
+ }
5562
+ function checkBox(at, box, partBoxes) {
5563
+ if (box === void 0) {
5564
+ const x0 = Math.min(...partBoxes.map((b) => b.x)) - DEFAULT_BOX_MARGIN;
5565
+ const y0 = Math.min(...partBoxes.map((b) => b.y)) - DEFAULT_BOX_MARGIN;
5566
+ const x1 = Math.max(...partBoxes.map((b) => b.x + b.w)) + DEFAULT_BOX_MARGIN;
5567
+ const y1 = Math.max(...partBoxes.map((b) => b.y + b.h)) + DEFAULT_BOX_MARGIN;
5568
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
5569
+ }
5570
+ if (!isObject(box)) {
5571
+ throw new BakeError(`${at}.box must be { x, y, w, h }, got ${show(box)}`);
5572
+ }
5573
+ checkedKeys(`${at}.box`, box, BOX_KEYS, BakeError);
5574
+ const [x, y] = ["x", "y"].map(
5575
+ (k) => checkedNumber(
5576
+ `${at}.box.${k}`,
5577
+ box[k],
5578
+ -Infinity,
5579
+ Infinity,
5580
+ false,
5581
+ BakeError
5582
+ )
5583
+ );
5584
+ const [w, h] = ["w", "h"].map(
5585
+ (k) => checkedNumber(`${at}.box.${k}`, box[k], 0, Infinity, true, BakeError)
5586
+ );
5587
+ return { x, y, w, h };
5588
+ }
5589
+ function checkStops(at, t, info, rest, cols, rows) {
5590
+ if (!isObject(t.stops)) {
5591
+ throw new BakeError(
5592
+ `${at}.stops must be an object keyed by a ${info.id} value, got ${show(t.stops)}`
5593
+ );
5594
+ }
5595
+ const n = (cols + 1) * (rows + 1);
5596
+ const stops = /* @__PURE__ */ new Map();
5597
+ for (const [key, list] of Object.entries(t.stops)) {
5598
+ const sat = `${at}.stops["${key}"]`;
5599
+ if (!PLAIN_DECIMAL.test(key)) {
5600
+ throw new BakeError(`${sat} must be keyed by a plain decimal number`);
5601
+ }
5602
+ const v = Number(key);
5603
+ if (!Number.isFinite(v) || v < info.min || v > info.max) {
5604
+ throw new BakeError(
5605
+ `${sat} is outside ${info.id}'s range ${info.min} to ${info.max}`
5606
+ );
5607
+ }
5608
+ if (Math.abs(v - rest) <= SAME_STOP) {
5609
+ throw new BakeError(
5610
+ `${sat} is ${info.id}'s rest value ${rest}: rest is implicit, leave it out`
5611
+ );
5612
+ }
5613
+ if (findStop(stops, v) !== void 0) {
5614
+ throw new BakeError(`${sat} repeats an earlier stop`);
5615
+ }
5616
+ if (!Array.isArray(list) || list.length !== n) {
5617
+ throw new BakeError(
5618
+ `${sat} must be ${n} points [dx, dy] (${cols + 1} x ${rows + 1}), got ${Array.isArray(list) ? `${list.length} points` : show(list)}`
5619
+ );
5620
+ }
5621
+ list.forEach((pt, k) => {
5622
+ if (!Array.isArray(pt) || pt.length !== 2) {
5623
+ throw new BakeError(`${sat}[${k}] must be [dx, dy], got ${show(pt)}`);
5624
+ }
5625
+ pt.forEach((c, j) => {
5626
+ if (!finite(c)) {
5627
+ throw new BakeError(
5628
+ `${sat}[${k}][${j}] must be a finite number, got ${String(c)}`
5629
+ );
5630
+ }
5631
+ });
5632
+ });
5633
+ stops.set(v, list);
5634
+ }
5635
+ if (stops.size === 0) {
5636
+ throw new BakeError(`${at}.stops must have at least one stop`);
5637
+ }
5638
+ if (t.mirror === true) {
5639
+ for (const [v, lattice2] of [...stops]) {
5640
+ const m = 2 * rest - v;
5641
+ if (m < info.min - SAME_STOP || m > info.max + SAME_STOP) continue;
5642
+ if (Math.abs(m - rest) <= SAME_STOP || findStop(stops, m) !== void 0) {
5643
+ continue;
5644
+ }
5645
+ stops.set(
5646
+ Math.min(info.max, Math.max(info.min, m)),
5647
+ mirrorLattice(lattice2, cols, rows)
5648
+ );
5649
+ }
5650
+ }
5651
+ return stops;
5652
+ }
5653
+ function checkTarget(t, i, customParts, paramById) {
5654
+ const at = `bakes.targets[${i}]`;
5655
+ if (!isObject(t)) {
5656
+ throw new BakeError(`${at} must be a plain object, got ${show(t)}`);
5657
+ }
5658
+ checkedKeys(at, t, TARGET_KEYS, BakeError);
5659
+ if (t.id !== void 0 && typeof t.id !== "string") {
5660
+ throw new BakeError(`${at}.id must be a string, got ${show(t.id)}`);
5661
+ }
5662
+ if (t.why !== void 0 && typeof t.why !== "string") {
5663
+ throw new BakeError(`${at}.why must be a string, got ${show(t.why)}`);
5664
+ }
5665
+ const info = typeof t.param === "string" ? paramById.get(t.param) : void 0;
5666
+ if (info === void 0) {
5667
+ throw new BakeError(
5668
+ `${at}.param must be a parameter the model declares, got ${show(t.param)}`
5669
+ );
5670
+ }
5671
+ const rest = Math.min(info.max, Math.max(info.min, info.default));
5672
+ const customIds = customParts.map((c) => c.id);
5673
+ if (!Array.isArray(t.parts) || t.parts.length === 0) {
5674
+ throw new BakeError(
5675
+ `${at}.parts must be a non-empty array of part ids, got ${show(t.parts)}`
5676
+ );
5677
+ }
5678
+ const parts = t.parts;
5679
+ parts.forEach((id, k) => {
5680
+ if (typeof id !== "string" || !customIds.includes(id)) {
5681
+ throw new BakeError(
5682
+ `${at}.parts[${k}] must be a custom part id (${customIds.join(", ")}): a bake moves custom parts only, got ${show(id)}`
5683
+ );
5684
+ }
5685
+ if (parts.indexOf(id) !== k) {
5686
+ throw new BakeError(`${at}.parts[${k}] repeats "${id}"`);
5687
+ }
5688
+ });
5689
+ const { cols, rows } = checkGrid(at, t.grid);
5690
+ const partBoxes = parts.map(
5691
+ (id) => customParts.find((c) => c.id === id).layer.bbox
5692
+ );
5693
+ const box = checkBox(at, t.box, partBoxes);
5694
+ if (t.mirror !== void 0 && typeof t.mirror !== "boolean") {
5695
+ throw new BakeError(
5696
+ `${at}.mirror must be true or false, got ${show(t.mirror)}`
5697
+ );
5698
+ }
5699
+ const stops = checkStops(at, t, info, rest, cols, rows);
5700
+ return {
5701
+ id: typeof t.id === "string" ? t.id : `target${i}`,
5702
+ param: info.id,
5703
+ rest,
5704
+ parts,
5705
+ box,
5706
+ cols,
5707
+ rows,
5708
+ stops
5709
+ };
5710
+ }
5711
+ function checkMesh(at, choice, box) {
5712
+ if (!isObject(choice)) {
5713
+ throw new BakeError(
5714
+ `${at} must be { rows, cols } or { cellPx }, got ${show(choice)}`
5715
+ );
5716
+ }
5717
+ checkedKeys(at, choice, ["rows", "cols", "cellPx"], BakeError);
5718
+ if (choice.cellPx !== void 0) {
5719
+ if (choice.rows !== void 0 || choice.cols !== void 0) {
5720
+ throw new BakeError(`${at} takes { cellPx } or { rows, cols }, not both`);
5721
+ }
5722
+ const cell = checkedNumber(
5723
+ `${at}.cellPx`,
5724
+ choice.cellPx,
5725
+ CELL_PX[0],
5726
+ CELL_PX[1],
5727
+ false,
5728
+ BakeError
5729
+ );
5730
+ const count = (Math.max(1, Math.round(box.w / cell)) + 1) * (Math.max(1, Math.round(box.h / cell)) + 1);
5731
+ if (count > MAX_VERTICES) {
5732
+ throw new BakeError(
5733
+ `${at}.cellPx ${cell} makes ${count} vertices, over the format's ${MAX_VERTICES} limit`
5734
+ );
5735
+ }
5736
+ return { cellPx: cell };
5737
+ }
5738
+ const cols = checkedNumber(
5739
+ `${at}.cols`,
5740
+ choice.cols,
5741
+ 1,
5742
+ MAX_MESH_COLS,
5743
+ false,
5744
+ BakeError
5745
+ );
5746
+ if (!Number.isInteger(cols)) {
5747
+ throw new BakeError(`${at}.cols must be an integer, got ${cols}`);
5748
+ }
5749
+ const ys = choice.rows;
5750
+ if (!Array.isArray(ys) || ys.length < 2 || ys.length > MAX_MESH_ROWS) {
5751
+ throw new BakeError(
5752
+ `${at}.rows must be an array of 2 to ${MAX_MESH_ROWS} canvas-px y values, got ${Array.isArray(ys) ? `${ys.length} values` : show(ys)}`
5753
+ );
5754
+ }
5755
+ ys.forEach((y, k) => {
5756
+ if (!finite(y)) {
5757
+ throw new BakeError(
5758
+ `${at}.rows[${k}] must be a finite number, got ${String(y)}`
5759
+ );
5760
+ }
5761
+ if (k > 0 && !(y > ys[k - 1])) {
5762
+ throw new BakeError(
5763
+ `${at}.rows[${k}] must be below the row before it (rows are strictly increasing), got ${y} after ${String(ys[k - 1])}`
5764
+ );
5765
+ }
5766
+ });
5767
+ const last = ys.length - 1;
5768
+ if (Math.abs(ys[0] - box.y) > 0.5) {
5769
+ throw new BakeError(
5770
+ `${at}.rows[0] must be the part's box top ${r1(box.y)} (within 0.5 px), got ${String(ys[0])}`
5771
+ );
5772
+ }
5773
+ if (Math.abs(ys[last] - (box.y + box.h)) > 0.5) {
5774
+ throw new BakeError(
5775
+ `${at}.rows[${last}] must be the part's box bottom ${r1(box.y + box.h)} (within 0.5 px), got ${String(ys[last])}`
5776
+ );
5777
+ }
5778
+ return { rows: ys, cols };
5779
+ }
5780
+ function parseBakes(bakes, customParts, parameters) {
5781
+ if (customParts === void 0 || customParts.length === 0) {
5782
+ throw new BakeError(
5783
+ "bakes needs customParts: a bake moves custom parts only, and none were given"
5784
+ );
5785
+ }
5786
+ if (!isObject(bakes)) {
5787
+ throw new BakeError(`bakes must be a plain object, got ${show(bakes)}`);
5788
+ }
5789
+ checkedKeys("bakes", bakes, ["targets", "meshes"], BakeError);
5790
+ const { targets, meshes } = bakes;
5791
+ if (!Array.isArray(targets) || targets.length === 0) {
5792
+ throw new BakeError("bakes.targets must be a non-empty array");
5793
+ }
5794
+ const paramById = new Map(parameters.map((p) => [p.id, p]));
5795
+ const parsedTargets = targets.map(
5796
+ (t, i) => checkTarget(t, i, customParts, paramById)
5797
+ );
5798
+ const parsedMeshes = /* @__PURE__ */ new Map();
5799
+ if (meshes !== void 0) {
5800
+ if (!isObject(meshes)) {
5801
+ throw new BakeError(
5802
+ `bakes.meshes must be an object keyed by part id, got ${show(meshes)}`
5803
+ );
5804
+ }
5805
+ for (const [id, choice] of Object.entries(meshes)) {
5806
+ const at = `bakes.meshes.${id}`;
5807
+ const part = customParts.find((c) => c.id === id);
5808
+ if (part === void 0) {
5809
+ throw new BakeError(
5810
+ `${at}: "${id}" is not a custom part (${customParts.map((c) => c.id).join(", ")}): only a custom part is re-meshed`
5811
+ );
5812
+ }
5813
+ parsedMeshes.set(id, checkMesh(at, choice, part.layer.bbox));
5814
+ }
5815
+ }
5816
+ return { targets: parsedTargets, meshes: parsedMeshes };
5817
+ }
5818
+ var OFFSET_STEP = 1e-6;
5819
+ var RIGID_BEND = 25e-4;
5820
+ function rigidResidual(rest, moved) {
5821
+ const n = rest.length;
5822
+ if (moved.length !== n) {
5823
+ throw new Error(
5824
+ `rigidResidual: ${n} rest points but ${moved.length} moved points`
5825
+ );
5826
+ }
5827
+ if (n === 0) return 0;
5828
+ let pcx = 0;
5829
+ let pcy = 0;
5830
+ let qcx = 0;
5831
+ let qcy = 0;
5832
+ for (let i = 0; i < n; i++) {
5833
+ pcx += rest[i][0];
5834
+ pcy += rest[i][1];
5835
+ qcx += moved[i][0];
5836
+ qcy += moved[i][1];
5837
+ }
5838
+ pcx /= n;
5839
+ pcy /= n;
5840
+ qcx /= n;
5841
+ qcy /= n;
5842
+ let dot = 0;
5843
+ let cross = 0;
5844
+ for (let i = 0; i < n; i++) {
5845
+ const px = rest[i][0] - pcx;
5846
+ const py = rest[i][1] - pcy;
5847
+ const qx = moved[i][0] - qcx;
5848
+ const qy = moved[i][1] - qcy;
5849
+ dot += px * qx + py * qy;
5850
+ cross += px * qy - py * qx;
5851
+ }
5852
+ const theta = Math.atan2(cross, dot);
5853
+ const c = Math.cos(theta);
5854
+ const s = Math.sin(theta);
5855
+ let worst = 0;
5856
+ for (let i = 0; i < n; i++) {
5857
+ const px = rest[i][0] - pcx;
5858
+ const py = rest[i][1] - pcy;
5859
+ const ex = qcx + c * px - s * py - moved[i][0];
5860
+ const ey = qcy + s * px + c * py - moved[i][1];
5861
+ worst = Math.max(worst, Math.hypot(ex, ey));
5862
+ }
5863
+ return worst;
5864
+ }
5865
+ function latticeAt(t, lattice2, x, y) {
5866
+ const { cols, rows, box } = t;
5867
+ const u = clamp((x - box.x) / box.w, 0, 1) * cols;
5868
+ const v = clamp((y - box.y) / box.h, 0, 1) * rows;
5869
+ const c = Math.min(Math.floor(u), cols - 1);
5870
+ const r = Math.min(Math.floor(v), rows - 1);
5871
+ const fx = u - c;
5872
+ const fy = v - r;
5873
+ const at = (rr, cc) => lattice2[rr * (cols + 1) + cc];
5874
+ const mix = (k) => {
5875
+ const top = at(r, c)[k] + (at(r, c + 1)[k] - at(r, c)[k]) * fx;
5876
+ const bot = at(r + 1, c)[k] + (at(r + 1, c + 1)[k] - at(r + 1, c)[k]) * fx;
5877
+ return top + (bot - top) * fy;
5878
+ };
5879
+ return [mix(0), mix(1)];
5880
+ }
5881
+ function targetField(t, v) {
5882
+ const side = Math.sign(v - t.rest);
5883
+ const knots = [...t.stops.keys()].filter((k) => Math.sign(k - t.rest) === side).sort((a2, b) => Math.abs(a2 - t.rest) - Math.abs(b - t.rest));
5884
+ const a = Math.abs(v - t.rest);
5885
+ return (x, y) => {
5886
+ if (knots.length === 0) return [0, 0];
5887
+ const at = (k) => latticeAt(t, t.stops.get(k), x, y);
5888
+ let lo = 0;
5889
+ let loVal = [0, 0];
5890
+ for (const k of knots) {
5891
+ const hi = Math.abs(k - t.rest);
5892
+ const hiVal = at(k);
5893
+ if (a <= hi + SAME_STOP) {
5894
+ const f = hi - lo > 0 ? Math.min(1, (a - lo) / (hi - lo)) : 1;
5895
+ return [
5896
+ loVal[0] + (hiVal[0] - loVal[0]) * f,
5897
+ loVal[1] + (hiVal[1] - loVal[1]) * f
5898
+ ];
5899
+ }
5900
+ lo = hi;
5901
+ loVal = hiVal;
5902
+ }
5903
+ return loVal;
5904
+ };
5905
+ }
5906
+ var apply = (m, x, y) => [
5907
+ m[0] * x + m[2] * y + m[4],
5908
+ m[1] * x + m[3] * y + m[5]
5909
+ ];
5910
+ function invert(m) {
5911
+ const det = m[0] * m[3] - m[1] * m[2];
5912
+ const a = m[3] / det;
5913
+ const b = -m[1] / det;
5914
+ const c = -m[2] / det;
5915
+ const d = m[0] / det;
5916
+ return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
5917
+ }
5918
+ function remesh(part, entry, choice, canvas) {
5919
+ const b = boxOfPixels(entry.layer.bbox, canvas.width, canvas.height);
5920
+ if ("cellPx" in choice) {
5921
+ part.mesh = gridMesh(
5922
+ Math.max(1, Math.round(bw(b) / choice.cellPx)),
5923
+ Math.max(1, Math.round(bh(b) / choice.cellPx))
5924
+ );
5925
+ return;
5926
+ }
5927
+ const xs = Array.from(
5928
+ { length: choice.cols + 1 },
5929
+ (_, c) => b.x0 + bw(b) * c / choice.cols
5930
+ );
5931
+ part.mesh = columnMesh(b, {
5932
+ xs,
5933
+ ys: choice.rows.map((y) => canvas.height / 2 - y)
5934
+ });
5935
+ }
5936
+ function applyBakes(parsed, parts, entries, ctx) {
5937
+ const { canvas, deformers: deformers2, parameters, faceWidth } = ctx;
5938
+ const partOf = (id) => parts.find((p) => p.id === id);
5939
+ const entryOf = (id) => entries.find((e) => e.id === id);
5940
+ for (const [id, choice] of parsed.meshes) {
5941
+ remesh(partOf(id), entryOf(id), choice, canvas);
5942
+ }
5943
+ const ids = [...new Set(parsed.targets.flatMap((t) => t.parts))];
5944
+ const reports = [];
5945
+ for (const id of ids) {
5946
+ const part = partOf(id);
5947
+ const n = part.mesh.vertices.length / 2;
5948
+ const own = parsed.targets.filter((t) => t.parts.includes(id));
5949
+ const w0 = matrixWorldAt(deformers2, parameters, part.deformer);
5950
+ const local = [];
5951
+ const restModel = [];
5952
+ const restCanvas = [];
5953
+ for (let i = 0; i < n; i++) {
5954
+ const p = [
5955
+ part.transform.x + part.mesh.vertices[i * 2] * part.width,
5956
+ part.transform.y + part.mesh.vertices[i * 2 + 1] * part.height
5957
+ ];
5958
+ const r = apply(w0, p[0], p[1]);
5959
+ local.push(p);
5960
+ restModel.push(r);
5961
+ restCanvas.push([r[0] + canvas.width / 2, canvas.height / 2 - r[1]]);
5962
+ }
5963
+ for (const param of [...new Set(own.map((t) => t.param))]) {
5964
+ const targets = own.filter((t) => t.param === param);
5965
+ const rest = targets[0].rest;
5966
+ const stops = [];
5967
+ for (const v of targets.flatMap((t) => [...t.stops.keys()]).sort((a, b) => a - b)) {
5968
+ if (stops.length === 0 || v - stops[stops.length - 1] > SAME_STOP) {
5969
+ stops.push(v);
5970
+ }
5971
+ }
5972
+ const range = parameters.find((p) => p.id === param);
5973
+ const ends = [];
5974
+ for (const [outer, end] of [
5975
+ [stops.filter((v) => v < rest).shift(), range.min],
5976
+ [stops.filter((v) => v > rest).pop(), range.max]
5977
+ ]) {
5978
+ if (outer !== void 0 && Math.abs(end - outer) > SAME_STOP) {
5979
+ ends.push(end);
5980
+ }
5981
+ }
5982
+ const keyforms = [{ value: rest, offsets: new Array(2 * n).fill(0) }];
5983
+ const wanted = [];
5984
+ const endWanted = [];
5985
+ for (const v of [...stops, ...ends]) {
5986
+ const disp = new Array(2 * n).fill(0);
5987
+ const fields2 = targets.map((t) => targetField(t, v));
5988
+ for (let i = 0; i < n; i++) {
5989
+ for (const field of fields2) {
5990
+ const [dx, dy] = field(restCanvas[i][0], restCanvas[i][1]);
5991
+ disp[i * 2] += dx;
5992
+ disp[i * 2 + 1] += dy;
5993
+ }
5994
+ }
5995
+ (ends.includes(v) ? endWanted : wanted).push(disp);
5996
+ if (entryOf(id).rigid === true) {
5997
+ const moved = restCanvas.map(
5998
+ (p, i) => [p[0] + disp[i * 2], p[1] + disp[i * 2 + 1]]
5999
+ );
6000
+ const bend = rigidResidual(restCanvas, moved);
6001
+ if (bend > RIGID_BEND * faceWidth) {
6002
+ const names = targets.map(
6003
+ (t) => `bakes.targets[${parsed.targets.indexOf(t)}] ("${t.id}")`
6004
+ ).join(", ");
6005
+ throw new BakeError(
6006
+ `${names}: "${id}" is rigid, but at ${param} ${v} these targets together bend it by ${bend.toFixed(2)} px, over the ${(RIGID_BEND * faceWidth).toFixed(2)} px a rigid part may bend (${RIGID_BEND} face widths): give every control point one displacement, or a rotation`
6007
+ );
6008
+ }
6009
+ }
6010
+ const inv = invert(
6011
+ matrixWorldAt(deformers2, parameters, part.deformer, { [param]: v })
6012
+ );
6013
+ const offsets = [];
6014
+ for (let i = 0; i < n; i++) {
6015
+ const [px, py] = apply(
6016
+ inv,
6017
+ restModel[i][0] + disp[i * 2],
6018
+ restModel[i][1] - disp[i * 2 + 1]
6019
+ );
6020
+ offsets.push(
6021
+ roundTo((px - local[i][0]) / part.width, OFFSET_STEP),
6022
+ roundTo((py - local[i][1]) / part.height, OFFSET_STEP)
6023
+ );
6024
+ }
6025
+ keyforms.push({ value: v, offsets });
6026
+ }
6027
+ keyforms.sort((a, b) => a.value - b.value);
6028
+ part.warps = [...part.warps ?? [], { parameter: param, keyforms }];
6029
+ reports.push({
6030
+ part: id,
6031
+ param,
6032
+ stops,
6033
+ vertexCount: n,
6034
+ wanted,
6035
+ ends: ends.map((value, i) => ({ value, wanted: endWanted[i] }))
6036
+ });
6037
+ }
6038
+ }
6039
+ return reports;
6040
+ }
6041
+
5328
6042
  // src/auto-rig/generate.ts
5329
6043
  import {
5330
6044
  IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
@@ -5533,8 +6247,8 @@ function defaultMotions(declared) {
5533
6247
  }
5534
6248
 
5535
6249
  // src/auto-rig/checks.ts
5536
- var finite = (v) => typeof v === "number" && Number.isFinite(v);
5537
- var isObject = (v) => typeof v === "object" && v !== null;
6250
+ var finite2 = (v) => typeof v === "number" && Number.isFinite(v);
6251
+ var isObject2 = (v) => typeof v === "object" && v !== null;
5538
6252
  var MIN_FACE_PX = 24;
5539
6253
  function checkLayers(layers, canvas) {
5540
6254
  if (!Array.isArray(layers) || layers.length === 0) {
@@ -5542,10 +6256,10 @@ function checkLayers(layers, canvas) {
5542
6256
  "auto-rig: generateIkiFromLayerSet: layers must be a non-empty array"
5543
6257
  );
5544
6258
  }
5545
- if (!(finite(canvas?.width) && canvas.width > 0)) {
6259
+ if (!(finite2(canvas?.width) && canvas.width > 0)) {
5546
6260
  throw new Error("auto-rig: canvas.width must be a positive number");
5547
6261
  }
5548
- if (!(finite(canvas?.height) && canvas.height > 0)) {
6262
+ if (!(finite2(canvas?.height) && canvas.height > 0)) {
5549
6263
  throw new Error("auto-rig: canvas.height must be a positive number");
5550
6264
  }
5551
6265
  const seen = /* @__PURE__ */ new Set();
@@ -5557,7 +6271,7 @@ function checkLayers(layers, canvas) {
5557
6271
  seen.add(l.role);
5558
6272
  const at = `auto-rig: layer "${l.fileName}"`;
5559
6273
  const { x, y, w, h } = l.bbox ?? {};
5560
- if (![x, y, w, h].every(finite) || w <= 0 || h <= 0 || x < 0 || y < 0 || x + w > canvas.width || y + h > canvas.height) {
6274
+ if (![x, y, w, h].every(finite2) || w <= 0 || h <= 0 || x < 0 || y < 0 || x + w > canvas.width || y + h > canvas.height) {
5561
6275
  throw new Error(
5562
6276
  `${at} has a bbox outside its ${canvas.width}x${canvas.height} canvas`
5563
6277
  );
@@ -5585,7 +6299,7 @@ function checkLayers(layers, canvas) {
5585
6299
  );
5586
6300
  }
5587
6301
  rows.forEach((v, i) => {
5588
- if (!finite(v) || v < 0 || v > w / 2 + 0.5) {
6302
+ if (!finite2(v) || v < 0 || v > w / 2 + 0.5) {
5589
6303
  throw new Error(
5590
6304
  `${at}: rowHalfWidths[${i}] ${v} is not a half-width inside its crop`
5591
6305
  );
@@ -5626,7 +6340,7 @@ function checkLayers(layers, canvas) {
5626
6340
  }
5627
6341
  if (l.denseCore !== void 0) {
5628
6342
  const c = l.denseCore;
5629
- if (![c.x, c.y, c.w, c.h].every(finite) || c.w <= 0 || c.h <= 0 || c.x < x - 1 || c.y < y - 1 || c.x + c.w > x + w + 1 || c.y + c.h > y + h + 1) {
6343
+ if (![c.x, c.y, c.w, c.h].every(finite2) || c.w <= 0 || c.h <= 0 || c.x < x - 1 || c.y < y - 1 || c.x + c.w > x + w + 1 || c.y + c.h > y + h + 1) {
5630
6344
  throw new Error(`${at}: denseCore is not a box inside its crop`);
5631
6345
  }
5632
6346
  }
@@ -5649,7 +6363,7 @@ function checkLayers(layers, canvas) {
5649
6363
  }
5650
6364
  function checkHeadEdges(edges, byRole) {
5651
6365
  if (edges === void 0) return;
5652
- if (!isObject(edges))
6366
+ if (!isObject2(edges))
5653
6367
  throw new TurnTargetError("headEdges must be an object");
5654
6368
  for (const side of ["left", "right"]) {
5655
6369
  const list = edges[side];
@@ -5657,12 +6371,12 @@ function checkHeadEdges(edges, byRole) {
5657
6371
  throw new TurnTargetError(`headEdges.${side} must be an array`);
5658
6372
  }
5659
6373
  list.forEach((e, i) => {
5660
- if (!isObject(e) || typeof e.role !== "string" || !byRole.has(e.role)) {
6374
+ if (!isObject2(e) || typeof e.role !== "string" || !byRole.has(e.role)) {
5661
6375
  throw new TurnTargetError(
5662
6376
  `headEdges.${side}[${i}].role is not a layer in this set`
5663
6377
  );
5664
6378
  }
5665
- if (!finite(e.x)) {
6379
+ if (!finite2(e.x)) {
5666
6380
  throw new TurnTargetError(
5667
6381
  `headEdges.${side}[${i}].x must be a finite number`
5668
6382
  );
@@ -5682,25 +6396,25 @@ function checkHeadEdges(edges, byRole) {
5682
6396
  }
5683
6397
  function checkStrandEdges(strand, irises, hair) {
5684
6398
  if (strand === void 0) return;
5685
- if (!isObject(strand)) {
6399
+ if (!isObject2(strand)) {
5686
6400
  throw new TurnTargetError("strandEdges must be an object");
5687
6401
  }
5688
6402
  for (const side of ["left", "right"]) {
5689
6403
  const s = strand[side];
5690
6404
  if (s === void 0) continue;
5691
6405
  const at = `strandEdges.${side}`;
5692
- if (!isObject(s)) throw new TurnTargetError(`${at} must be an object`);
6406
+ if (!isObject2(s)) throw new TurnTargetError(`${at} must be an object`);
5693
6407
  if (hair === void 0) {
5694
6408
  throw new TurnTargetError(
5695
6409
  `${at} names a bangs' run, but this layer set has no hair_front`
5696
6410
  );
5697
6411
  }
5698
6412
  for (const k of ["y", "irisOuter", "irisInner", "runOuter"]) {
5699
- if (!finite(s[k])) {
6413
+ if (!finite2(s[k])) {
5700
6414
  throw new TurnTargetError(`${at}.${k} must be a finite number`);
5701
6415
  }
5702
6416
  }
5703
- if (s.runFace !== null && !finite(s.runFace)) {
6417
+ if (s.runFace !== null && !finite2(s.runFace)) {
5704
6418
  throw new TurnTargetError(
5705
6419
  `${at}.runFace must be a finite number or null`
5706
6420
  );
@@ -6171,9 +6885,9 @@ function replay(op, i, ctx, sums) {
6171
6885
  const x = span2(`${name}.region.x`, region.x);
6172
6886
  const y = span2(`${name}.region.y`, region.y);
6173
6887
  const by = fields(`${name}.by`, o.by, BY_KEYS);
6174
- const dx = finite2(`${name}.by.dx`, by.dx);
6175
- const dy = finite2(`${name}.by.dy`, by.dy);
6176
- const falloff = o.falloff === void 0 ? 0 : finite2(`${name}.falloff`, o.falloff, 0);
6888
+ const dx = finite3(`${name}.by.dx`, by.dx);
6889
+ const dy = finite3(`${name}.by.dy`, by.dy);
6890
+ const falloff = o.falloff === void 0 ? 0 : finite3(`${name}.falloff`, o.falloff, 0);
6177
6891
  if (o.why !== void 0) text(`${name}.why`, o.why);
6178
6892
  const box = ctx.boxOf(role);
6179
6893
  if (box === void 0) {
@@ -6341,10 +7055,10 @@ function fields(name, v, known) {
6341
7055
  function numbers(name, v) {
6342
7056
  const o = fields(name, v, Object.keys(v ?? {}));
6343
7057
  return Object.fromEntries(
6344
- Object.entries(o).map(([k, n]) => [k, finite2(`${name}.${k}`, n)])
7058
+ Object.entries(o).map(([k, n]) => [k, finite3(`${name}.${k}`, n)])
6345
7059
  );
6346
7060
  }
6347
- var finite2 = (name, v, lo = -Infinity) => asPatch(() => checkedNumber(name, v, lo, Infinity));
7061
+ var finite3 = (name, v, lo = -Infinity) => asPatch(() => checkedNumber(name, v, lo, Infinity));
6348
7062
  function text(name, v) {
6349
7063
  if (typeof v !== "string") {
6350
7064
  throw new PatchError(`${name} must be a string, got ${String(v)}`);
@@ -6357,8 +7071,8 @@ function span2(name, v) {
6357
7071
  `${name} must be [from, to], got ${JSON.stringify(v)}`
6358
7072
  );
6359
7073
  }
6360
- const lo = finite2(`${name}[0]`, v[0]);
6361
- const hi = finite2(`${name}[1]`, v[1]);
7074
+ const lo = finite3(`${name}[0]`, v[0]);
7075
+ const hi = finite3(`${name}[1]`, v[1]);
6362
7076
  if (lo > hi) {
6363
7077
  throw new PatchError(
6364
7078
  `${name} [${lo}, ${hi}] runs backwards: ${name}[0] must not exceed ${name}[1]`
@@ -6367,6 +7081,224 @@ function span2(name, v) {
6367
7081
  return [lo, hi];
6368
7082
  }
6369
7083
 
7084
+ // src/auto-rig/custom-parts.ts
7085
+ var KEYS = [
7086
+ "id",
7087
+ "layer",
7088
+ "anchor",
7089
+ "tie",
7090
+ "after",
7091
+ "deformer",
7092
+ "rigid",
7093
+ "occlusion"
7094
+ ];
7095
+ var OCCLUSION_KEYS = ["by", "parameter", "stops"];
7096
+ var STOP_KEYS = ["value", "amount"];
7097
+ var LAYER_KEYS = ["fileName", "bbox"];
7098
+ var finite4 = (v) => typeof v === "number" && Number.isFinite(v);
7099
+ var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7100
+ var inside = (b, [x, y]) => x >= b.x && x < b.x + b.w && y >= b.y && y < b.y + b.h;
7101
+ var RESERVED = new Set(
7102
+ ROLE_TABLE.flatMap((r) => [r.role, ...partIdsOfRole(r.role)])
7103
+ );
7104
+ function checkCustomParts(customParts, byRole, canvas, parameters, physicsOutputs) {
7105
+ if (!Array.isArray(customParts)) {
7106
+ throw new CustomPartError(
7107
+ `customParts must be an array, got ${customParts === null ? "null" : typeof customParts}`
7108
+ );
7109
+ }
7110
+ const present = [...byRole.keys()];
7111
+ const roleField = (at, v) => {
7112
+ const layer = typeof v === "string" ? byRole.get(v) : void 0;
7113
+ if (layer === void 0) {
7114
+ throw new CustomPartError(
7115
+ `${at} must be a role of the set, one of ${present.join(", ")}; got ${JSON.stringify(v)}`
7116
+ );
7117
+ }
7118
+ return layer;
7119
+ };
7120
+ const ids = /* @__PURE__ */ new Set();
7121
+ customParts.forEach((entry, i) => {
7122
+ const at = `customParts[${i}]`;
7123
+ if (!isObject3(entry)) {
7124
+ throw new CustomPartError(`${at} must be a plain object`);
7125
+ }
7126
+ checkedKeys(at, entry, KEYS, CustomPartError);
7127
+ const { id, layer, anchor, tie, after, deformer, rigid, occlusion } = entry;
7128
+ if (typeof id !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(id)) {
7129
+ throw new CustomPartError(
7130
+ `${at}.id must be a plain identifier (letters, digits, underscore; not starting with a digit), got ${JSON.stringify(id)}`
7131
+ );
7132
+ }
7133
+ if (RESERVED.has(id)) {
7134
+ throw new CustomPartError(
7135
+ `${at}.id "${id}" is a role's name or a part id a role yields: those are reserved, also for roles added later, so pick a name no role would take`
7136
+ );
7137
+ }
7138
+ if (ids.has(id)) {
7139
+ throw new CustomPartError(
7140
+ `${at}.id "${id}" repeats an earlier entry's id`
7141
+ );
7142
+ }
7143
+ ids.add(id);
7144
+ if (!isObject3(layer)) {
7145
+ throw new CustomPartError(`${at}.layer must be a plain object`);
7146
+ }
7147
+ checkedKeys(`${at}.layer`, layer, LAYER_KEYS, CustomPartError);
7148
+ if (typeof layer.fileName !== "string" || layer.fileName === "") {
7149
+ throw new CustomPartError(
7150
+ `${at}.layer.fileName must be a non-empty string`
7151
+ );
7152
+ }
7153
+ const b = isObject3(layer.bbox) ? layer.bbox : {};
7154
+ const { x, y, w, h } = b;
7155
+ if (!(finite4(x) && finite4(y) && finite4(w) && finite4(h)) || w <= 0 || h <= 0 || x < 0 || y < 0 || x + w > canvas.width || y + h > canvas.height) {
7156
+ throw new CustomPartError(
7157
+ `${at}.layer.bbox must be a finite, non-empty box inside the ${canvas.width}x${canvas.height} canvas, got ${JSON.stringify(layer.bbox)}`
7158
+ );
7159
+ }
7160
+ const box = { x, y, w, h };
7161
+ const anchorLayer = roleField(`${at}.anchor`, anchor);
7162
+ roleField(`${at}.after`, after);
7163
+ if (!Array.isArray(tie) || tie.length !== 2 || !tie.every(finite4) || !inside(box, tie) || !inside(anchorLayer.bbox, tie)) {
7164
+ throw new CustomPartError(
7165
+ `${at}.tie must be [x, y], two finite canvas px inside both the part's layer box and the anchor "${String(anchor)}" layer box, got ${JSON.stringify(tie)}`
7166
+ );
7167
+ }
7168
+ if (deformer !== void 0 && deformer !== "headDeformer") {
7169
+ throw new CustomPartError(
7170
+ `${at}.deformer "${String(deformer)}" is not accepted: headDeformer is the only deformer a custom part hangs on`
7171
+ );
7172
+ }
7173
+ if (rigid !== void 0 && typeof rigid !== "boolean") {
7174
+ throw new CustomPartError(
7175
+ `${at}.rigid must be true or false, got ${JSON.stringify(rigid)}`
7176
+ );
7177
+ }
7178
+ if (occlusion !== void 0) {
7179
+ checkOcclusion(
7180
+ `${at}.occlusion`,
7181
+ occlusion,
7182
+ present,
7183
+ parameters,
7184
+ physicsOutputs
7185
+ );
7186
+ }
7187
+ });
7188
+ return customParts;
7189
+ }
7190
+ function checkOcclusion(at, occlusion, present, parameters, physicsOutputs) {
7191
+ if (!isObject3(occlusion)) {
7192
+ throw new CustomPartError(`${at} must be a plain object`);
7193
+ }
7194
+ checkedKeys(at, occlusion, OCCLUSION_KEYS, CustomPartError);
7195
+ const { by, parameter, stops } = occlusion;
7196
+ if (!Array.isArray(by) || by.length === 0) {
7197
+ throw new CustomPartError(
7198
+ `${at}.by must be a non-empty array of roles of the set, one of ${present.join(", ")}`
7199
+ );
7200
+ }
7201
+ by.forEach((role, j) => {
7202
+ if (typeof role !== "string" || !present.includes(role)) {
7203
+ throw new CustomPartError(
7204
+ `${at}.by[${j}] must be a role of the set, one of ${present.join(", ")}; got ${JSON.stringify(role)}`
7205
+ );
7206
+ }
7207
+ if (by.indexOf(role) !== j) {
7208
+ throw new CustomPartError(
7209
+ `${at}.by[${j}] "${role}" repeats an earlier role`
7210
+ );
7211
+ }
7212
+ });
7213
+ const descriptor = parameters.find((p) => p.id === parameter);
7214
+ if (typeof parameter !== "string" || descriptor === void 0) {
7215
+ throw new CustomPartError(
7216
+ `${at}.parameter must be a parameter the rig declares, got ${JSON.stringify(parameter)}`
7217
+ );
7218
+ }
7219
+ if (physicsOutputs.has(parameter)) {
7220
+ throw new CustomPartError(
7221
+ `${at}.parameter "${parameter}" is a physics output; an occlusion amount cannot be keyed on one`
7222
+ );
7223
+ }
7224
+ if (!Array.isArray(stops) || stops.length === 0) {
7225
+ throw new CustomPartError(`${at}.stops must be a non-empty array`);
7226
+ }
7227
+ const { min, max } = descriptor;
7228
+ let previous = -Infinity;
7229
+ stops.forEach((stop, k) => {
7230
+ const s = `${at}.stops[${k}]`;
7231
+ if (!isObject3(stop))
7232
+ throw new CustomPartError(`${s} must be a plain object`);
7233
+ checkedKeys(s, stop, STOP_KEYS, CustomPartError);
7234
+ const { value, amount } = stop;
7235
+ if (!finite4(value)) {
7236
+ throw new CustomPartError(
7237
+ `${s}.value must be a finite number, got ${JSON.stringify(value)}`
7238
+ );
7239
+ }
7240
+ if (value < min || value > max) {
7241
+ throw new CustomPartError(
7242
+ `${s}.value ${value} is outside parameter "${parameter}" range [${min},${max}]`
7243
+ );
7244
+ }
7245
+ if (value <= previous) {
7246
+ throw new CustomPartError(`${s}.value must be strictly ascending`);
7247
+ }
7248
+ previous = value;
7249
+ if (!finite4(amount) || amount < 0 || amount > 1) {
7250
+ throw new CustomPartError(
7251
+ `${s}.amount must be a number in 0..1, got ${JSON.stringify(amount)}`
7252
+ );
7253
+ }
7254
+ });
7255
+ }
7256
+ var CELL_PX2 = 20;
7257
+ function buildCustomParts(customParts, canvas) {
7258
+ return customParts.map(({ id, layer }) => {
7259
+ const b = boxOfPixels(layer.bbox, canvas.width, canvas.height);
7260
+ return {
7261
+ id,
7262
+ color: [1, 1, 1, 1],
7263
+ width: bw(b),
7264
+ height: bh(b),
7265
+ transform: { x: cx(b), y: cy(b) },
7266
+ order: 0,
7267
+ deformer: "headDeformer",
7268
+ mesh: gridMesh(
7269
+ Math.max(1, Math.round(bw(b) / CELL_PX2)),
7270
+ Math.max(1, Math.round(bh(b) / CELL_PX2))
7271
+ )
7272
+ };
7273
+ });
7274
+ }
7275
+ function writeOcclusion(customParts, built, parts) {
7276
+ const byId = new Map(parts.map((p) => [p.id, p]));
7277
+ customParts.forEach((entry, i) => {
7278
+ if (entry.occlusion === void 0) return;
7279
+ const { by, parameter, stops } = entry.occlusion;
7280
+ const ids = [];
7281
+ by.forEach((role, j) => {
7282
+ for (const id of partIdsOfRole(role)) {
7283
+ const part = byId.get(id);
7284
+ if (part === void 0) continue;
7285
+ const fault = part.clip !== void 0 ? { is: "is clipped", kind: "a clipped part" } : part.mesh === void 0 ? { is: "has no mesh", kind: "a part without a mesh" } : void 0;
7286
+ if (fault !== void 0) {
7287
+ throw new CustomPartError(
7288
+ `customParts[${i}].occlusion.by[${j}] "${role}" yields part "${id}", which ${fault.is}; ${fault.kind} cannot occlude`
7289
+ );
7290
+ }
7291
+ ids.push(id);
7292
+ }
7293
+ });
7294
+ built[i].occlusion = {
7295
+ by: ids,
7296
+ parameter,
7297
+ stops: stops.map((s) => ({ value: s.value, amount: s.amount }))
7298
+ };
7299
+ });
7300
+ }
7301
+
6370
7302
  // src/auto-rig/generate.ts
6371
7303
  var MESH_CELLS = {
6372
7304
  face: { px: 28, min: 4, max: 24 },
@@ -6408,6 +7340,16 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
6408
7340
  const hasNose = has("nose");
6409
7341
  const plateHalf = byRole.get("face").cropW / 2;
6410
7342
  checkHeadEdges(options.headEdges, byRole);
7343
+ const physics = has("hair_front") ? hairPhysics(has("hair_back")) : void 0;
7344
+ const parameters = declareParameters(new Set(byRole.keys()));
7345
+ const entries = options.customParts !== void 0 && !(Array.isArray(options.customParts) && options.customParts.length === 0) ? checkCustomParts(
7346
+ options.customParts,
7347
+ byRole,
7348
+ canvas,
7349
+ parameters,
7350
+ new Set((physics ?? []).map((r) => r.output.parameter))
7351
+ ) : [];
7352
+ const bakes = options.bakes === void 0 ? void 0 : parseBakes(options.bakes, entries, parameters);
6411
7353
  checkStrandEdges(
6412
7354
  options.strandEdges,
6413
7355
  sideIrises(byRole),
@@ -6557,7 +7499,6 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
6557
7499
  ...part.warps ?? []
6558
7500
  ];
6559
7501
  }
6560
- const parameters = declareParameters(new Set(byRole.keys()));
6561
7502
  const reports = options.patches === void 0 ? void 0 : applyPatches(options.patches, {
6562
7503
  grids: new Map(
6563
7504
  [...grids].map(([family, g]) => [WARP_ID[family], g])
@@ -6567,19 +7508,51 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
6567
7508
  faceWidth: byRole.get("face").cropW,
6568
7509
  parameters
6569
7510
  });
7511
+ const deformerList = deformers(
7512
+ frame,
7513
+ grids,
7514
+ bodyWarp?.deformer,
7515
+ arms,
7516
+ byRole
7517
+ );
7518
+ let bakeReports;
7519
+ if (entries.length > 0) {
7520
+ const built = buildCustomParts(entries, canvas);
7521
+ entries.forEach((entry, i) => {
7522
+ const group = new Set(partIdsOfRole(entry.after));
7523
+ for (let j = 0; j < i; j++) {
7524
+ if (entries[j].after === entry.after) group.add(entries[j].id);
7525
+ }
7526
+ const last = parts.map((p) => group.has(p.id)).lastIndexOf(true);
7527
+ parts.splice(last + 1, 0, built[i]);
7528
+ });
7529
+ parts.forEach((p, i) => {
7530
+ p.order = i;
7531
+ });
7532
+ writeOcclusion(entries, built, parts);
7533
+ if (bakes !== void 0) {
7534
+ bakeReports = applyBakes(bakes, built, entries, {
7535
+ canvas,
7536
+ deformers: deformerList,
7537
+ parameters,
7538
+ faceWidth: byRole.get("face").cropW
7539
+ });
7540
+ }
7541
+ }
6570
7542
  const model = {
6571
7543
  version: IKI_FORMAT_VERSION2,
6572
7544
  name: "auto-rigged",
6573
7545
  canvas: { width: canvas.width, height: canvas.height },
6574
7546
  parameters,
6575
7547
  parts,
6576
- deformers: deformers(frame, grids, bodyWarp?.deformer, arms, byRole),
6577
- ...has("hair_front") ? { physics: hairPhysics(has("hair_back")) } : {},
7548
+ deformers: deformerList,
7549
+ ...physics !== void 0 ? { physics } : {},
6578
7550
  expressions: defaultExpressions(new Set(parameters.map((p) => p.id))),
6579
7551
  motions: defaultMotions(new Set(parameters.map((p) => p.id)))
6580
7552
  };
6581
7553
  const valid = parseIkiModel3(model);
6582
7554
  if (reports !== void 0) options.onPatchesApplied?.(reports);
7555
+ if (bakeReports !== void 0) options.onBakesApplied?.(bakeReports);
6583
7556
  options.onRigPlanResolved?.(
6584
7557
  rigPlanReport(options.rigPlan ?? {}, plan, targets, fit)
6585
7558
  );
@@ -7061,7 +8034,9 @@ export {
7061
8034
  AddDeformer,
7062
8035
  AddPart,
7063
8036
  AddPhysicsRig,
8037
+ BakeError,
7064
8038
  CaptureGridKeyform,
8039
+ CustomPartError,
7065
8040
  DEFAULT_TURN_TARGETS,
7066
8041
  DeleteDeformer,
7067
8042
  DeletePart,
@@ -7117,6 +8092,7 @@ export {
7117
8092
  packAtlas,
7118
8093
  parseLayerRoles,
7119
8094
  partIdsOfRole,
8095
+ rigidResidual,
7120
8096
  styleRemovedMessage,
7121
8097
  upsertGridKeyform,
7122
8098
  uvRectFor,