@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.js CHANGED
@@ -26,7 +26,9 @@ __export(index_exports, {
26
26
  AddDeformer: () => AddDeformer,
27
27
  AddPart: () => AddPart,
28
28
  AddPhysicsRig: () => AddPhysicsRig,
29
+ BakeError: () => BakeError,
29
30
  CaptureGridKeyform: () => CaptureGridKeyform,
31
+ CustomPartError: () => CustomPartError,
30
32
  DEFAULT_TURN_TARGETS: () => DEFAULT_TURN_TARGETS,
31
33
  DeleteDeformer: () => DeleteDeformer,
32
34
  DeletePart: () => DeletePart,
@@ -82,6 +84,7 @@ __export(index_exports, {
82
84
  packAtlas: () => packAtlas,
83
85
  parseLayerRoles: () => parseLayerRoles,
84
86
  partIdsOfRole: () => partIdsOfRole,
87
+ rigidResidual: () => rigidResidual,
85
88
  styleRemovedMessage: () => styleRemovedMessage,
86
89
  upsertGridKeyform: () => upsertGridKeyform,
87
90
  uvRectFor: () => uvRectFor,
@@ -1112,6 +1115,14 @@ var DeletePart = class {
1112
1115
  `parts."${this.partId}": cannot delete \u2014 used as a clip mask by part "${masker.id}"; remove its clip first`
1113
1116
  );
1114
1117
  }
1118
+ const occluded = parts.find(
1119
+ (p) => p.id !== this.partId && p.occlusion?.by.includes(this.partId)
1120
+ );
1121
+ if (occluded) {
1122
+ throw new Error(
1123
+ `parts."${this.partId}": cannot delete \u2014 used as an occluder by part "${occluded.id}"; remove its occlusion first`
1124
+ );
1125
+ }
1115
1126
  const i = parts.indexOf(part);
1116
1127
  if (!this.captured) {
1117
1128
  this.removed = structuredClone(part);
@@ -1316,6 +1327,14 @@ var SetPartMesh = class {
1316
1327
  `parts."${this.partId}": cannot remove mesh \u2014 used as a clip mask by part "${masker.id}" (masks require a mesh); remove its clip first`
1317
1328
  );
1318
1329
  }
1330
+ const occluded = doc.getModel().parts.find(
1331
+ (p) => p.id !== this.partId && p.occlusion?.by.includes(this.partId)
1332
+ );
1333
+ if (occluded) {
1334
+ throw new import_format2.IkiFormatError(
1335
+ `parts."${this.partId}": cannot remove mesh \u2014 used as an occluder by part "${occluded.id}" (occluders require a mesh); remove its occlusion first`
1336
+ );
1337
+ }
1319
1338
  } else {
1320
1339
  if ((part.warps?.length ?? 0) > 0) {
1321
1340
  throw new import_format2.IkiFormatError(
@@ -1395,6 +1414,14 @@ function validatePhysicsCandidate(doc, candidatePhysics) {
1395
1414
  }
1396
1415
  throw e;
1397
1416
  }
1417
+ for (const rig of candidatePhysics) {
1418
+ const part = doc.getModel().parts.find((p) => p.occlusion?.parameter === rig.output.parameter);
1419
+ if (part !== void 0) {
1420
+ throw new import_format2.IkiFormatError(
1421
+ `physics."${rig.id}".output.parameter "${rig.output.parameter}" keys parts."${part.id}".occlusion; an occlusion amount cannot be keyed on a physics output`
1422
+ );
1423
+ }
1424
+ }
1398
1425
  }
1399
1426
  var AddPhysicsRig = class {
1400
1427
  label = "Add physics rig";
@@ -2203,6 +2230,18 @@ var PatchError = class extends Error {
2203
2230
  this.name = "PatchError";
2204
2231
  }
2205
2232
  };
2233
+ var CustomPartError = class extends Error {
2234
+ constructor(message) {
2235
+ super(message);
2236
+ this.name = "CustomPartError";
2237
+ }
2238
+ };
2239
+ var BakeError = class extends Error {
2240
+ constructor(message) {
2241
+ super(message);
2242
+ this.name = "BakeError";
2243
+ }
2244
+ };
2206
2245
 
2207
2246
  // src/auto-rig/layout.ts
2208
2247
  function boxOfLayer(layer) {
@@ -2607,10 +2646,10 @@ function checkSidesHaveFront(roles) {
2607
2646
  function checkMouth(roles) {
2608
2647
  const has = new Set(roles);
2609
2648
  const present = LIP_ROLES.filter((r) => has.has(r));
2610
- const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
2611
- if (inside.length > 0 && present.length === 0) {
2649
+ const inside2 = LIP_INSIDE_ROLES.filter((r) => has.has(r));
2650
+ if (inside2.length > 0 && present.length === 0) {
2612
2651
  throw new Error(
2613
- `auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
2652
+ `auto-rig: ${inside2[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
2614
2653
  );
2615
2654
  }
2616
2655
  if (present.length === 0) {
@@ -2830,10 +2869,10 @@ function mouthFold(role, opening) {
2830
2869
  const stackThin = (c, y) => y > c.Tu ? c.Tu + UPPER_THIN * (y - c.Tu) : y;
2831
2870
  return (col, y, v) => {
2832
2871
  const k = 1 - v;
2833
- const inside = col >= x0 && col <= x1;
2872
+ const inside2 = col >= x0 && col <= x1;
2834
2873
  switch (role) {
2835
2874
  case "lip_lower": {
2836
- if (!inside) return [0, 0];
2875
+ if (!inside2) return [0, 0];
2837
2876
  const c = opening.at(col);
2838
2877
  return [0, (c.seam + c.overlap - c.Bl) * k];
2839
2878
  }
@@ -2857,9 +2896,9 @@ function mouthFold(role, opening) {
2857
2896
  line = opening.line(c += col < x0 ? 1 : -1);
2858
2897
  const thin = line.bottom + UPPER_THIN * (y - line.bottom);
2859
2898
  const mid = line.bottom + UPPER_THIN * (line.top - line.bottom) / 2;
2860
- const travel = inside ? opening.at(col).seam - line.bottom : 0;
2899
+ const travel = inside2 ? opening.at(col).seam - line.bottom : 0;
2861
2900
  const shut = mid + line.squash * (thin - mid) + travel;
2862
- const pull = inside ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
2901
+ const pull = inside2 ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
2863
2902
  return [pull * k, thin - y + (shut - thin) * k];
2864
2903
  }
2865
2904
  }
@@ -4192,14 +4231,14 @@ function cuesAt(ctx, land2, ax) {
4192
4231
  const l0 = land2(L.role, L.lo, L.y, ax);
4193
4232
  const l1 = land2(L.role, L.hi, L.y, ax);
4194
4233
  const r0 = land2(R.role, R.lo, R.y, ax);
4195
- const r1 = land2(R.role, R.hi, R.y, ax);
4234
+ const r12 = land2(R.role, R.hi, R.y, ax);
4196
4235
  const wl = l1 - l0;
4197
- const wr = r1 - r0;
4236
+ const wr = r12 - r0;
4198
4237
  const wl0 = L.hi - L.lo;
4199
4238
  const wr0 = R.hi - R.lo;
4200
4239
  const ratio = ax < 0 ? wl / wr / (wl0 / wr0) : wr / wl / (wr0 / wl0);
4201
4240
  const pc0 = (L.lo + L.hi + R.lo + R.hi) / 4;
4202
- const pc = (l0 + l1 + r0 + r1) / 4;
4241
+ const pc = (l0 + l1 + r0 + r12) / 4;
4203
4242
  const y = ctx.frame.eyeY;
4204
4243
  const hl0 = Math.min(...ctx.candidates.left.map((c) => c.x));
4205
4244
  const hr0 = Math.max(...ctx.candidates.right.map((c) => c.x));
@@ -4833,7 +4872,7 @@ function blendStops(values, offsets, v) {
4833
4872
  const t = (x - values[i]) / (values[i + 1] - values[i]);
4834
4873
  return offsets[i].map((o, k) => o + (offsets[i + 1][k] - o) * t);
4835
4874
  }
4836
- function land(l, offsets, x, y) {
4875
+ function bindToRest(l, x, y) {
4837
4876
  const { cols, rows, points } = l;
4838
4877
  const stride = cols + 1;
4839
4878
  let col = cols - 1;
@@ -4854,8 +4893,17 @@ function land(l, offsets, x, y) {
4854
4893
  const x1 = points[(col + 1) * 2];
4855
4894
  const yT = points[row * stride * 2 + 1];
4856
4895
  const yB = points[(row + 1) * stride * 2 + 1];
4857
- const s = clamp((x - x0) / (x1 - x0), 0, 1);
4858
- const t = clamp((yT - y) / (yT - yB), 0, 1);
4896
+ return {
4897
+ row,
4898
+ col,
4899
+ s: clamp((x - x0) / (x1 - x0), 0, 1),
4900
+ t: clamp((yT - y) / (yT - yB), 0, 1)
4901
+ };
4902
+ }
4903
+ function land(l, offsets, x, y) {
4904
+ const { cols, points } = l;
4905
+ const stride = cols + 1;
4906
+ const { row, col, s, t } = bindToRest(l, x, y);
4859
4907
  const at = (r, c) => {
4860
4908
  const k = (r * stride + c) * 2;
4861
4909
  return [points[k] + offsets[k], points[k + 1] + offsets[k + 1]];
@@ -5195,7 +5243,7 @@ function armGeometry(layer, bodyAxisX) {
5195
5243
  b.x1 - elbow.x,
5196
5244
  b.y1 - elbow.y
5197
5245
  );
5198
- const inside = (x, y) => {
5246
+ const inside2 = (x, y) => {
5199
5247
  const k = Math.floor(y) - bbox.y;
5200
5248
  if (k < 0 || k >= bbox.h) return false;
5201
5249
  const [rs, re] = span3(k);
@@ -5204,7 +5252,7 @@ function armGeometry(layer, bodyAxisX) {
5204
5252
  const capRays = Array.from({ length: CAP_SEGMENTS + 1 }, (_, i) => {
5205
5253
  const a = Math.PI * i / CAP_SEGMENTS;
5206
5254
  let t = 1;
5207
- while (t <= capRadius + CAP_INSET && inside(
5255
+ while (t <= capRadius + CAP_INSET && inside2(
5208
5256
  elbow.x + canvasW / 2 + t * Math.cos(a),
5209
5257
  canvasH / 2 - elbow.y - t * Math.sin(a)
5210
5258
  )) {
@@ -5404,6 +5452,675 @@ function forearmPoseDeformer(role, arm) {
5404
5452
  };
5405
5453
  }
5406
5454
 
5455
+ // src/auto-rig/chain.ts
5456
+ function multiply(a, b) {
5457
+ return [
5458
+ a[0] * b[0] + a[2] * b[1],
5459
+ a[1] * b[0] + a[3] * b[1],
5460
+ a[0] * b[2] + a[2] * b[3],
5461
+ a[1] * b[2] + a[3] * b[3],
5462
+ a[0] * b[4] + a[2] * b[5] + a[4],
5463
+ a[1] * b[4] + a[3] * b[5] + a[5]
5464
+ ];
5465
+ }
5466
+ function valueOf(parameters, values, id) {
5467
+ const p = parameters.find((q) => q.id === id);
5468
+ if (p === void 0) return 0;
5469
+ const v = values[id] ?? p.default;
5470
+ return Math.min(p.max, Math.max(p.min, v));
5471
+ }
5472
+ function ownMatrix(d, parameters, values) {
5473
+ let x = d.transform?.x ?? 0;
5474
+ let y = d.transform?.y ?? 0;
5475
+ let rotation = d.transform?.rotation ?? 0;
5476
+ let sx = d.transform?.scaleX ?? 1;
5477
+ let sy = d.transform?.scaleY ?? 1;
5478
+ for (const b of d.bindings ?? []) {
5479
+ const p = parameters.find((q) => q.id === b.parameter);
5480
+ const t = p === void 0 || p.max === p.min ? 0 : (valueOf(parameters, values, b.parameter) - p.min) / (p.max - p.min);
5481
+ const v = b.from + (b.to - b.from) * t;
5482
+ if (b.channel === "translateX") x += v;
5483
+ else if (b.channel === "translateY") y += v;
5484
+ else if (b.channel === "rotate") rotation += v;
5485
+ else if (b.channel === "scaleX") sx += v;
5486
+ else if (b.channel === "scaleY") sy += v;
5487
+ }
5488
+ const r = rotation * Math.PI / 180;
5489
+ const c = Math.cos(r);
5490
+ const s = Math.sin(r);
5491
+ const trs = [c * sx, s * sx, -s * sy, c * sy, x, y];
5492
+ return multiply(multiply([1, 0, 0, 1, d.pivot.x, d.pivot.y], trs), [
5493
+ 1,
5494
+ 0,
5495
+ 0,
5496
+ 1,
5497
+ -d.pivot.x,
5498
+ -d.pivot.y
5499
+ ]);
5500
+ }
5501
+ function localGrid(w, parameters, values) {
5502
+ const points = [...w.grid.points];
5503
+ for (const warp of w.warps ?? []) {
5504
+ const offsets = blendStops(
5505
+ warp.keyforms.map((k) => k.value),
5506
+ warp.keyforms.map((k) => k.offsets),
5507
+ valueOf(parameters, values, warp.parameter)
5508
+ );
5509
+ for (let i = 0; i < points.length; i++) points[i] += offsets[i];
5510
+ }
5511
+ return points;
5512
+ }
5513
+ function riddenFrame(pivot, w, q) {
5514
+ const { cols, points: rest } = w.grid;
5515
+ const stride = cols + 1;
5516
+ const { row, col, s, t } = bindToRest(w.grid, pivot.x, pivot.y);
5517
+ const xl = rest[col * 2];
5518
+ const xr = rest[(col + 1) * 2];
5519
+ const yt = rest[row * stride * 2 + 1];
5520
+ const yb = rest[(row + 1) * stride * 2 + 1];
5521
+ const i00 = (row * stride + col) * 2;
5522
+ const i10 = (row * stride + col + 1) * 2;
5523
+ const i01 = ((row + 1) * stride + col) * 2;
5524
+ const i11 = ((row + 1) * stride + col + 1) * 2;
5525
+ const lerp = (a, b, k) => a + (b - a) * k;
5526
+ const topX = lerp(q[i00], q[i10], s);
5527
+ const topY = lerp(q[i00 + 1], q[i10 + 1], s);
5528
+ const botX = lerp(q[i01], q[i11], s);
5529
+ const botY = lerp(q[i01 + 1], q[i11 + 1], s);
5530
+ const px = lerp(topX, botX, t);
5531
+ const py = lerp(topY, botY, t);
5532
+ const width = xr - xl;
5533
+ const height = yt - yb;
5534
+ const exX = lerp(q[i10] - q[i00], q[i11] - q[i01], t) / width;
5535
+ const exY = lerp(q[i10 + 1] - q[i00 + 1], q[i11 + 1] - q[i01 + 1], t) / width;
5536
+ const eyX = -lerp(q[i01] - q[i00], q[i11] - q[i10], s) / height;
5537
+ const eyY = -lerp(q[i01 + 1] - q[i00 + 1], q[i11 + 1] - q[i10 + 1], s) / height;
5538
+ const theta = Math.atan2(exY - eyX, exX + eyY);
5539
+ const c = Math.cos(theta);
5540
+ const sn = Math.sin(theta);
5541
+ return [
5542
+ c,
5543
+ sn,
5544
+ -sn,
5545
+ c,
5546
+ px - (c * pivot.x - sn * pivot.y),
5547
+ py - (sn * pivot.x + c * pivot.y)
5548
+ ];
5549
+ }
5550
+ function matrixWorldAt(deformers2, parameters, id, values = {}) {
5551
+ for (const key of Object.keys(values)) {
5552
+ if (!parameters.some((q) => q.id === key)) {
5553
+ throw new Error(`chain: unknown parameter "${key}"`);
5554
+ }
5555
+ }
5556
+ const d = deformers2.find((x) => x.id === id);
5557
+ if (d === void 0) throw new Error(`chain: no deformer "${id}"`);
5558
+ if (d.kind === "warp") {
5559
+ throw new Error(`chain: "${id}" is a warp deformer, not a matrix`);
5560
+ }
5561
+ const own = ownMatrix(d, parameters, values);
5562
+ if (d.parent === void 0) return own;
5563
+ const parent = deformers2.find((x) => x.id === d.parent);
5564
+ if (parent === void 0) {
5565
+ throw new Error(`chain: "${id}" has an unknown parent "${d.parent}"`);
5566
+ }
5567
+ if (parent.kind !== "warp") {
5568
+ throw new Error(
5569
+ `chain: "${id}" hangs from the matrix deformer "${parent.id}"; only a root warp is supported`
5570
+ );
5571
+ }
5572
+ if (parent.parent !== void 0) {
5573
+ throw new Error(
5574
+ `chain: "${id}" hangs from warp "${parent.id}", which has a parent "${parent.parent}"; only a root warp is supported`
5575
+ );
5576
+ }
5577
+ if (parent.warp2d !== void 0) {
5578
+ throw new Error(
5579
+ `chain: "${id}" hangs from warp "${parent.id}", which carries a warp2d; only 1D warps are supported`
5580
+ );
5581
+ }
5582
+ return multiply(
5583
+ riddenFrame(d.pivot, parent, localGrid(parent, parameters, values)),
5584
+ own
5585
+ );
5586
+ }
5587
+
5588
+ // src/auto-rig/bakes.ts
5589
+ var TARGET_KEYS = [
5590
+ "id",
5591
+ "param",
5592
+ "parts",
5593
+ "box",
5594
+ "grid",
5595
+ "stops",
5596
+ "mirror",
5597
+ "why"
5598
+ ];
5599
+ var BOX_KEYS = ["x", "y", "w", "h"];
5600
+ var MAX_VERTICES = 65536;
5601
+ var CELL_PX = [4, 400];
5602
+ var MAX_MESH_COLS = 64;
5603
+ var MAX_MESH_ROWS = 400;
5604
+ var DEFAULT_BOX_MARGIN = 2;
5605
+ var SAME_STOP = 1e-9;
5606
+ var PLAIN_DECIMAL = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
5607
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
5608
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5609
+ var show = (v) => typeof v === "number" ? String(v) : JSON.stringify(v) ?? String(v);
5610
+ function mirrorLattice(lattice2, cols, rows) {
5611
+ const out = new Array(lattice2.length);
5612
+ for (let r = 0; r <= rows; r++) {
5613
+ for (let c = 0; c <= cols; c++) {
5614
+ const [dx, dy] = lattice2[r * (cols + 1) + (cols - c)];
5615
+ out[r * (cols + 1) + c] = [-dx, dy];
5616
+ }
5617
+ }
5618
+ return out;
5619
+ }
5620
+ var r1 = (v) => Math.round(v * 10) / 10;
5621
+ var findStop = (stops, v) => [...stops.keys()].find((k) => Math.abs(k - v) <= SAME_STOP);
5622
+ function checkGrid(at, grid) {
5623
+ if (!isObject(grid)) {
5624
+ throw new BakeError(`${at}.grid must be { cols, rows }, got ${show(grid)}`);
5625
+ }
5626
+ checkedKeys(`${at}.grid`, grid, ["cols", "rows"], BakeError);
5627
+ const out = { cols: 0, rows: 0 };
5628
+ for (const key of ["cols", "rows"]) {
5629
+ const v = checkedNumber(
5630
+ `${at}.grid.${key}`,
5631
+ grid[key],
5632
+ 1,
5633
+ 32,
5634
+ false,
5635
+ BakeError
5636
+ );
5637
+ if (!Number.isInteger(v)) {
5638
+ throw new BakeError(`${at}.grid.${key} must be an integer, got ${v}`);
5639
+ }
5640
+ out[key] = v;
5641
+ }
5642
+ return out;
5643
+ }
5644
+ function checkBox(at, box, partBoxes) {
5645
+ if (box === void 0) {
5646
+ const x0 = Math.min(...partBoxes.map((b) => b.x)) - DEFAULT_BOX_MARGIN;
5647
+ const y0 = Math.min(...partBoxes.map((b) => b.y)) - DEFAULT_BOX_MARGIN;
5648
+ const x1 = Math.max(...partBoxes.map((b) => b.x + b.w)) + DEFAULT_BOX_MARGIN;
5649
+ const y1 = Math.max(...partBoxes.map((b) => b.y + b.h)) + DEFAULT_BOX_MARGIN;
5650
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
5651
+ }
5652
+ if (!isObject(box)) {
5653
+ throw new BakeError(`${at}.box must be { x, y, w, h }, got ${show(box)}`);
5654
+ }
5655
+ checkedKeys(`${at}.box`, box, BOX_KEYS, BakeError);
5656
+ const [x, y] = ["x", "y"].map(
5657
+ (k) => checkedNumber(
5658
+ `${at}.box.${k}`,
5659
+ box[k],
5660
+ -Infinity,
5661
+ Infinity,
5662
+ false,
5663
+ BakeError
5664
+ )
5665
+ );
5666
+ const [w, h] = ["w", "h"].map(
5667
+ (k) => checkedNumber(`${at}.box.${k}`, box[k], 0, Infinity, true, BakeError)
5668
+ );
5669
+ return { x, y, w, h };
5670
+ }
5671
+ function checkStops(at, t, info, rest, cols, rows) {
5672
+ if (!isObject(t.stops)) {
5673
+ throw new BakeError(
5674
+ `${at}.stops must be an object keyed by a ${info.id} value, got ${show(t.stops)}`
5675
+ );
5676
+ }
5677
+ const n = (cols + 1) * (rows + 1);
5678
+ const stops = /* @__PURE__ */ new Map();
5679
+ for (const [key, list] of Object.entries(t.stops)) {
5680
+ const sat = `${at}.stops["${key}"]`;
5681
+ if (!PLAIN_DECIMAL.test(key)) {
5682
+ throw new BakeError(`${sat} must be keyed by a plain decimal number`);
5683
+ }
5684
+ const v = Number(key);
5685
+ if (!Number.isFinite(v) || v < info.min || v > info.max) {
5686
+ throw new BakeError(
5687
+ `${sat} is outside ${info.id}'s range ${info.min} to ${info.max}`
5688
+ );
5689
+ }
5690
+ if (Math.abs(v - rest) <= SAME_STOP) {
5691
+ throw new BakeError(
5692
+ `${sat} is ${info.id}'s rest value ${rest}: rest is implicit, leave it out`
5693
+ );
5694
+ }
5695
+ if (findStop(stops, v) !== void 0) {
5696
+ throw new BakeError(`${sat} repeats an earlier stop`);
5697
+ }
5698
+ if (!Array.isArray(list) || list.length !== n) {
5699
+ throw new BakeError(
5700
+ `${sat} must be ${n} points [dx, dy] (${cols + 1} x ${rows + 1}), got ${Array.isArray(list) ? `${list.length} points` : show(list)}`
5701
+ );
5702
+ }
5703
+ list.forEach((pt, k) => {
5704
+ if (!Array.isArray(pt) || pt.length !== 2) {
5705
+ throw new BakeError(`${sat}[${k}] must be [dx, dy], got ${show(pt)}`);
5706
+ }
5707
+ pt.forEach((c, j) => {
5708
+ if (!finite(c)) {
5709
+ throw new BakeError(
5710
+ `${sat}[${k}][${j}] must be a finite number, got ${String(c)}`
5711
+ );
5712
+ }
5713
+ });
5714
+ });
5715
+ stops.set(v, list);
5716
+ }
5717
+ if (stops.size === 0) {
5718
+ throw new BakeError(`${at}.stops must have at least one stop`);
5719
+ }
5720
+ if (t.mirror === true) {
5721
+ for (const [v, lattice2] of [...stops]) {
5722
+ const m = 2 * rest - v;
5723
+ if (m < info.min - SAME_STOP || m > info.max + SAME_STOP) continue;
5724
+ if (Math.abs(m - rest) <= SAME_STOP || findStop(stops, m) !== void 0) {
5725
+ continue;
5726
+ }
5727
+ stops.set(
5728
+ Math.min(info.max, Math.max(info.min, m)),
5729
+ mirrorLattice(lattice2, cols, rows)
5730
+ );
5731
+ }
5732
+ }
5733
+ return stops;
5734
+ }
5735
+ function checkTarget(t, i, customParts, paramById) {
5736
+ const at = `bakes.targets[${i}]`;
5737
+ if (!isObject(t)) {
5738
+ throw new BakeError(`${at} must be a plain object, got ${show(t)}`);
5739
+ }
5740
+ checkedKeys(at, t, TARGET_KEYS, BakeError);
5741
+ if (t.id !== void 0 && typeof t.id !== "string") {
5742
+ throw new BakeError(`${at}.id must be a string, got ${show(t.id)}`);
5743
+ }
5744
+ if (t.why !== void 0 && typeof t.why !== "string") {
5745
+ throw new BakeError(`${at}.why must be a string, got ${show(t.why)}`);
5746
+ }
5747
+ const info = typeof t.param === "string" ? paramById.get(t.param) : void 0;
5748
+ if (info === void 0) {
5749
+ throw new BakeError(
5750
+ `${at}.param must be a parameter the model declares, got ${show(t.param)}`
5751
+ );
5752
+ }
5753
+ const rest = Math.min(info.max, Math.max(info.min, info.default));
5754
+ const customIds = customParts.map((c) => c.id);
5755
+ if (!Array.isArray(t.parts) || t.parts.length === 0) {
5756
+ throw new BakeError(
5757
+ `${at}.parts must be a non-empty array of part ids, got ${show(t.parts)}`
5758
+ );
5759
+ }
5760
+ const parts = t.parts;
5761
+ parts.forEach((id, k) => {
5762
+ if (typeof id !== "string" || !customIds.includes(id)) {
5763
+ throw new BakeError(
5764
+ `${at}.parts[${k}] must be a custom part id (${customIds.join(", ")}): a bake moves custom parts only, got ${show(id)}`
5765
+ );
5766
+ }
5767
+ if (parts.indexOf(id) !== k) {
5768
+ throw new BakeError(`${at}.parts[${k}] repeats "${id}"`);
5769
+ }
5770
+ });
5771
+ const { cols, rows } = checkGrid(at, t.grid);
5772
+ const partBoxes = parts.map(
5773
+ (id) => customParts.find((c) => c.id === id).layer.bbox
5774
+ );
5775
+ const box = checkBox(at, t.box, partBoxes);
5776
+ if (t.mirror !== void 0 && typeof t.mirror !== "boolean") {
5777
+ throw new BakeError(
5778
+ `${at}.mirror must be true or false, got ${show(t.mirror)}`
5779
+ );
5780
+ }
5781
+ const stops = checkStops(at, t, info, rest, cols, rows);
5782
+ return {
5783
+ id: typeof t.id === "string" ? t.id : `target${i}`,
5784
+ param: info.id,
5785
+ rest,
5786
+ parts,
5787
+ box,
5788
+ cols,
5789
+ rows,
5790
+ stops
5791
+ };
5792
+ }
5793
+ function checkMesh(at, choice, box) {
5794
+ if (!isObject(choice)) {
5795
+ throw new BakeError(
5796
+ `${at} must be { rows, cols } or { cellPx }, got ${show(choice)}`
5797
+ );
5798
+ }
5799
+ checkedKeys(at, choice, ["rows", "cols", "cellPx"], BakeError);
5800
+ if (choice.cellPx !== void 0) {
5801
+ if (choice.rows !== void 0 || choice.cols !== void 0) {
5802
+ throw new BakeError(`${at} takes { cellPx } or { rows, cols }, not both`);
5803
+ }
5804
+ const cell = checkedNumber(
5805
+ `${at}.cellPx`,
5806
+ choice.cellPx,
5807
+ CELL_PX[0],
5808
+ CELL_PX[1],
5809
+ false,
5810
+ BakeError
5811
+ );
5812
+ const count = (Math.max(1, Math.round(box.w / cell)) + 1) * (Math.max(1, Math.round(box.h / cell)) + 1);
5813
+ if (count > MAX_VERTICES) {
5814
+ throw new BakeError(
5815
+ `${at}.cellPx ${cell} makes ${count} vertices, over the format's ${MAX_VERTICES} limit`
5816
+ );
5817
+ }
5818
+ return { cellPx: cell };
5819
+ }
5820
+ const cols = checkedNumber(
5821
+ `${at}.cols`,
5822
+ choice.cols,
5823
+ 1,
5824
+ MAX_MESH_COLS,
5825
+ false,
5826
+ BakeError
5827
+ );
5828
+ if (!Number.isInteger(cols)) {
5829
+ throw new BakeError(`${at}.cols must be an integer, got ${cols}`);
5830
+ }
5831
+ const ys = choice.rows;
5832
+ if (!Array.isArray(ys) || ys.length < 2 || ys.length > MAX_MESH_ROWS) {
5833
+ throw new BakeError(
5834
+ `${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)}`
5835
+ );
5836
+ }
5837
+ ys.forEach((y, k) => {
5838
+ if (!finite(y)) {
5839
+ throw new BakeError(
5840
+ `${at}.rows[${k}] must be a finite number, got ${String(y)}`
5841
+ );
5842
+ }
5843
+ if (k > 0 && !(y > ys[k - 1])) {
5844
+ throw new BakeError(
5845
+ `${at}.rows[${k}] must be below the row before it (rows are strictly increasing), got ${y} after ${String(ys[k - 1])}`
5846
+ );
5847
+ }
5848
+ });
5849
+ const last = ys.length - 1;
5850
+ if (Math.abs(ys[0] - box.y) > 0.5) {
5851
+ throw new BakeError(
5852
+ `${at}.rows[0] must be the part's box top ${r1(box.y)} (within 0.5 px), got ${String(ys[0])}`
5853
+ );
5854
+ }
5855
+ if (Math.abs(ys[last] - (box.y + box.h)) > 0.5) {
5856
+ throw new BakeError(
5857
+ `${at}.rows[${last}] must be the part's box bottom ${r1(box.y + box.h)} (within 0.5 px), got ${String(ys[last])}`
5858
+ );
5859
+ }
5860
+ return { rows: ys, cols };
5861
+ }
5862
+ function parseBakes(bakes, customParts, parameters) {
5863
+ if (customParts === void 0 || customParts.length === 0) {
5864
+ throw new BakeError(
5865
+ "bakes needs customParts: a bake moves custom parts only, and none were given"
5866
+ );
5867
+ }
5868
+ if (!isObject(bakes)) {
5869
+ throw new BakeError(`bakes must be a plain object, got ${show(bakes)}`);
5870
+ }
5871
+ checkedKeys("bakes", bakes, ["targets", "meshes"], BakeError);
5872
+ const { targets, meshes } = bakes;
5873
+ if (!Array.isArray(targets) || targets.length === 0) {
5874
+ throw new BakeError("bakes.targets must be a non-empty array");
5875
+ }
5876
+ const paramById = new Map(parameters.map((p) => [p.id, p]));
5877
+ const parsedTargets = targets.map(
5878
+ (t, i) => checkTarget(t, i, customParts, paramById)
5879
+ );
5880
+ const parsedMeshes = /* @__PURE__ */ new Map();
5881
+ if (meshes !== void 0) {
5882
+ if (!isObject(meshes)) {
5883
+ throw new BakeError(
5884
+ `bakes.meshes must be an object keyed by part id, got ${show(meshes)}`
5885
+ );
5886
+ }
5887
+ for (const [id, choice] of Object.entries(meshes)) {
5888
+ const at = `bakes.meshes.${id}`;
5889
+ const part = customParts.find((c) => c.id === id);
5890
+ if (part === void 0) {
5891
+ throw new BakeError(
5892
+ `${at}: "${id}" is not a custom part (${customParts.map((c) => c.id).join(", ")}): only a custom part is re-meshed`
5893
+ );
5894
+ }
5895
+ parsedMeshes.set(id, checkMesh(at, choice, part.layer.bbox));
5896
+ }
5897
+ }
5898
+ return { targets: parsedTargets, meshes: parsedMeshes };
5899
+ }
5900
+ var OFFSET_STEP = 1e-6;
5901
+ var RIGID_BEND = 25e-4;
5902
+ function rigidResidual(rest, moved) {
5903
+ const n = rest.length;
5904
+ if (moved.length !== n) {
5905
+ throw new Error(
5906
+ `rigidResidual: ${n} rest points but ${moved.length} moved points`
5907
+ );
5908
+ }
5909
+ if (n === 0) return 0;
5910
+ let pcx = 0;
5911
+ let pcy = 0;
5912
+ let qcx = 0;
5913
+ let qcy = 0;
5914
+ for (let i = 0; i < n; i++) {
5915
+ pcx += rest[i][0];
5916
+ pcy += rest[i][1];
5917
+ qcx += moved[i][0];
5918
+ qcy += moved[i][1];
5919
+ }
5920
+ pcx /= n;
5921
+ pcy /= n;
5922
+ qcx /= n;
5923
+ qcy /= n;
5924
+ let dot = 0;
5925
+ let cross = 0;
5926
+ for (let i = 0; i < n; i++) {
5927
+ const px = rest[i][0] - pcx;
5928
+ const py = rest[i][1] - pcy;
5929
+ const qx = moved[i][0] - qcx;
5930
+ const qy = moved[i][1] - qcy;
5931
+ dot += px * qx + py * qy;
5932
+ cross += px * qy - py * qx;
5933
+ }
5934
+ const theta = Math.atan2(cross, dot);
5935
+ const c = Math.cos(theta);
5936
+ const s = Math.sin(theta);
5937
+ let worst = 0;
5938
+ for (let i = 0; i < n; i++) {
5939
+ const px = rest[i][0] - pcx;
5940
+ const py = rest[i][1] - pcy;
5941
+ const ex = qcx + c * px - s * py - moved[i][0];
5942
+ const ey = qcy + s * px + c * py - moved[i][1];
5943
+ worst = Math.max(worst, Math.hypot(ex, ey));
5944
+ }
5945
+ return worst;
5946
+ }
5947
+ function latticeAt(t, lattice2, x, y) {
5948
+ const { cols, rows, box } = t;
5949
+ const u = clamp((x - box.x) / box.w, 0, 1) * cols;
5950
+ const v = clamp((y - box.y) / box.h, 0, 1) * rows;
5951
+ const c = Math.min(Math.floor(u), cols - 1);
5952
+ const r = Math.min(Math.floor(v), rows - 1);
5953
+ const fx = u - c;
5954
+ const fy = v - r;
5955
+ const at = (rr, cc) => lattice2[rr * (cols + 1) + cc];
5956
+ const mix = (k) => {
5957
+ const top = at(r, c)[k] + (at(r, c + 1)[k] - at(r, c)[k]) * fx;
5958
+ const bot = at(r + 1, c)[k] + (at(r + 1, c + 1)[k] - at(r + 1, c)[k]) * fx;
5959
+ return top + (bot - top) * fy;
5960
+ };
5961
+ return [mix(0), mix(1)];
5962
+ }
5963
+ function targetField(t, v) {
5964
+ const side = Math.sign(v - t.rest);
5965
+ 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));
5966
+ const a = Math.abs(v - t.rest);
5967
+ return (x, y) => {
5968
+ if (knots.length === 0) return [0, 0];
5969
+ const at = (k) => latticeAt(t, t.stops.get(k), x, y);
5970
+ let lo = 0;
5971
+ let loVal = [0, 0];
5972
+ for (const k of knots) {
5973
+ const hi = Math.abs(k - t.rest);
5974
+ const hiVal = at(k);
5975
+ if (a <= hi + SAME_STOP) {
5976
+ const f = hi - lo > 0 ? Math.min(1, (a - lo) / (hi - lo)) : 1;
5977
+ return [
5978
+ loVal[0] + (hiVal[0] - loVal[0]) * f,
5979
+ loVal[1] + (hiVal[1] - loVal[1]) * f
5980
+ ];
5981
+ }
5982
+ lo = hi;
5983
+ loVal = hiVal;
5984
+ }
5985
+ return loVal;
5986
+ };
5987
+ }
5988
+ var apply = (m, x, y) => [
5989
+ m[0] * x + m[2] * y + m[4],
5990
+ m[1] * x + m[3] * y + m[5]
5991
+ ];
5992
+ function invert(m) {
5993
+ const det = m[0] * m[3] - m[1] * m[2];
5994
+ const a = m[3] / det;
5995
+ const b = -m[1] / det;
5996
+ const c = -m[2] / det;
5997
+ const d = m[0] / det;
5998
+ return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
5999
+ }
6000
+ function remesh(part, entry, choice, canvas) {
6001
+ const b = boxOfPixels(entry.layer.bbox, canvas.width, canvas.height);
6002
+ if ("cellPx" in choice) {
6003
+ part.mesh = gridMesh(
6004
+ Math.max(1, Math.round(bw(b) / choice.cellPx)),
6005
+ Math.max(1, Math.round(bh(b) / choice.cellPx))
6006
+ );
6007
+ return;
6008
+ }
6009
+ const xs = Array.from(
6010
+ { length: choice.cols + 1 },
6011
+ (_, c) => b.x0 + bw(b) * c / choice.cols
6012
+ );
6013
+ part.mesh = columnMesh(b, {
6014
+ xs,
6015
+ ys: choice.rows.map((y) => canvas.height / 2 - y)
6016
+ });
6017
+ }
6018
+ function applyBakes(parsed, parts, entries, ctx) {
6019
+ const { canvas, deformers: deformers2, parameters, faceWidth } = ctx;
6020
+ const partOf = (id) => parts.find((p) => p.id === id);
6021
+ const entryOf = (id) => entries.find((e) => e.id === id);
6022
+ for (const [id, choice] of parsed.meshes) {
6023
+ remesh(partOf(id), entryOf(id), choice, canvas);
6024
+ }
6025
+ const ids = [...new Set(parsed.targets.flatMap((t) => t.parts))];
6026
+ const reports = [];
6027
+ for (const id of ids) {
6028
+ const part = partOf(id);
6029
+ const n = part.mesh.vertices.length / 2;
6030
+ const own = parsed.targets.filter((t) => t.parts.includes(id));
6031
+ const w0 = matrixWorldAt(deformers2, parameters, part.deformer);
6032
+ const local = [];
6033
+ const restModel = [];
6034
+ const restCanvas = [];
6035
+ for (let i = 0; i < n; i++) {
6036
+ const p = [
6037
+ part.transform.x + part.mesh.vertices[i * 2] * part.width,
6038
+ part.transform.y + part.mesh.vertices[i * 2 + 1] * part.height
6039
+ ];
6040
+ const r = apply(w0, p[0], p[1]);
6041
+ local.push(p);
6042
+ restModel.push(r);
6043
+ restCanvas.push([r[0] + canvas.width / 2, canvas.height / 2 - r[1]]);
6044
+ }
6045
+ for (const param of [...new Set(own.map((t) => t.param))]) {
6046
+ const targets = own.filter((t) => t.param === param);
6047
+ const rest = targets[0].rest;
6048
+ const stops = [];
6049
+ for (const v of targets.flatMap((t) => [...t.stops.keys()]).sort((a, b) => a - b)) {
6050
+ if (stops.length === 0 || v - stops[stops.length - 1] > SAME_STOP) {
6051
+ stops.push(v);
6052
+ }
6053
+ }
6054
+ const range = parameters.find((p) => p.id === param);
6055
+ const ends = [];
6056
+ for (const [outer, end] of [
6057
+ [stops.filter((v) => v < rest).shift(), range.min],
6058
+ [stops.filter((v) => v > rest).pop(), range.max]
6059
+ ]) {
6060
+ if (outer !== void 0 && Math.abs(end - outer) > SAME_STOP) {
6061
+ ends.push(end);
6062
+ }
6063
+ }
6064
+ const keyforms = [{ value: rest, offsets: new Array(2 * n).fill(0) }];
6065
+ const wanted = [];
6066
+ const endWanted = [];
6067
+ for (const v of [...stops, ...ends]) {
6068
+ const disp = new Array(2 * n).fill(0);
6069
+ const fields2 = targets.map((t) => targetField(t, v));
6070
+ for (let i = 0; i < n; i++) {
6071
+ for (const field of fields2) {
6072
+ const [dx, dy] = field(restCanvas[i][0], restCanvas[i][1]);
6073
+ disp[i * 2] += dx;
6074
+ disp[i * 2 + 1] += dy;
6075
+ }
6076
+ }
6077
+ (ends.includes(v) ? endWanted : wanted).push(disp);
6078
+ if (entryOf(id).rigid === true) {
6079
+ const moved = restCanvas.map(
6080
+ (p, i) => [p[0] + disp[i * 2], p[1] + disp[i * 2 + 1]]
6081
+ );
6082
+ const bend = rigidResidual(restCanvas, moved);
6083
+ if (bend > RIGID_BEND * faceWidth) {
6084
+ const names = targets.map(
6085
+ (t) => `bakes.targets[${parsed.targets.indexOf(t)}] ("${t.id}")`
6086
+ ).join(", ");
6087
+ throw new BakeError(
6088
+ `${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`
6089
+ );
6090
+ }
6091
+ }
6092
+ const inv = invert(
6093
+ matrixWorldAt(deformers2, parameters, part.deformer, { [param]: v })
6094
+ );
6095
+ const offsets = [];
6096
+ for (let i = 0; i < n; i++) {
6097
+ const [px, py] = apply(
6098
+ inv,
6099
+ restModel[i][0] + disp[i * 2],
6100
+ restModel[i][1] - disp[i * 2 + 1]
6101
+ );
6102
+ offsets.push(
6103
+ roundTo((px - local[i][0]) / part.width, OFFSET_STEP),
6104
+ roundTo((py - local[i][1]) / part.height, OFFSET_STEP)
6105
+ );
6106
+ }
6107
+ keyforms.push({ value: v, offsets });
6108
+ }
6109
+ keyforms.sort((a, b) => a.value - b.value);
6110
+ part.warps = [...part.warps ?? [], { parameter: param, keyforms }];
6111
+ reports.push({
6112
+ part: id,
6113
+ param,
6114
+ stops,
6115
+ vertexCount: n,
6116
+ wanted,
6117
+ ends: ends.map((value, i) => ({ value, wanted: endWanted[i] }))
6118
+ });
6119
+ }
6120
+ }
6121
+ return reports;
6122
+ }
6123
+
5407
6124
  // src/auto-rig/generate.ts
5408
6125
  var import_format12 = require("@ikijs/format");
5409
6126
 
@@ -5606,8 +6323,8 @@ function defaultMotions(declared) {
5606
6323
  }
5607
6324
 
5608
6325
  // src/auto-rig/checks.ts
5609
- var finite = (v) => typeof v === "number" && Number.isFinite(v);
5610
- var isObject = (v) => typeof v === "object" && v !== null;
6326
+ var finite2 = (v) => typeof v === "number" && Number.isFinite(v);
6327
+ var isObject2 = (v) => typeof v === "object" && v !== null;
5611
6328
  var MIN_FACE_PX = 24;
5612
6329
  function checkLayers(layers, canvas) {
5613
6330
  if (!Array.isArray(layers) || layers.length === 0) {
@@ -5615,10 +6332,10 @@ function checkLayers(layers, canvas) {
5615
6332
  "auto-rig: generateIkiFromLayerSet: layers must be a non-empty array"
5616
6333
  );
5617
6334
  }
5618
- if (!(finite(canvas?.width) && canvas.width > 0)) {
6335
+ if (!(finite2(canvas?.width) && canvas.width > 0)) {
5619
6336
  throw new Error("auto-rig: canvas.width must be a positive number");
5620
6337
  }
5621
- if (!(finite(canvas?.height) && canvas.height > 0)) {
6338
+ if (!(finite2(canvas?.height) && canvas.height > 0)) {
5622
6339
  throw new Error("auto-rig: canvas.height must be a positive number");
5623
6340
  }
5624
6341
  const seen = /* @__PURE__ */ new Set();
@@ -5630,7 +6347,7 @@ function checkLayers(layers, canvas) {
5630
6347
  seen.add(l.role);
5631
6348
  const at = `auto-rig: layer "${l.fileName}"`;
5632
6349
  const { x, y, w, h } = l.bbox ?? {};
5633
- if (![x, y, w, h].every(finite) || w <= 0 || h <= 0 || x < 0 || y < 0 || x + w > canvas.width || y + h > canvas.height) {
6350
+ if (![x, y, w, h].every(finite2) || w <= 0 || h <= 0 || x < 0 || y < 0 || x + w > canvas.width || y + h > canvas.height) {
5634
6351
  throw new Error(
5635
6352
  `${at} has a bbox outside its ${canvas.width}x${canvas.height} canvas`
5636
6353
  );
@@ -5658,7 +6375,7 @@ function checkLayers(layers, canvas) {
5658
6375
  );
5659
6376
  }
5660
6377
  rows.forEach((v, i) => {
5661
- if (!finite(v) || v < 0 || v > w / 2 + 0.5) {
6378
+ if (!finite2(v) || v < 0 || v > w / 2 + 0.5) {
5662
6379
  throw new Error(
5663
6380
  `${at}: rowHalfWidths[${i}] ${v} is not a half-width inside its crop`
5664
6381
  );
@@ -5699,7 +6416,7 @@ function checkLayers(layers, canvas) {
5699
6416
  }
5700
6417
  if (l.denseCore !== void 0) {
5701
6418
  const c = l.denseCore;
5702
- 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) {
6419
+ 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) {
5703
6420
  throw new Error(`${at}: denseCore is not a box inside its crop`);
5704
6421
  }
5705
6422
  }
@@ -5722,7 +6439,7 @@ function checkLayers(layers, canvas) {
5722
6439
  }
5723
6440
  function checkHeadEdges(edges, byRole) {
5724
6441
  if (edges === void 0) return;
5725
- if (!isObject(edges))
6442
+ if (!isObject2(edges))
5726
6443
  throw new TurnTargetError("headEdges must be an object");
5727
6444
  for (const side of ["left", "right"]) {
5728
6445
  const list = edges[side];
@@ -5730,12 +6447,12 @@ function checkHeadEdges(edges, byRole) {
5730
6447
  throw new TurnTargetError(`headEdges.${side} must be an array`);
5731
6448
  }
5732
6449
  list.forEach((e, i) => {
5733
- if (!isObject(e) || typeof e.role !== "string" || !byRole.has(e.role)) {
6450
+ if (!isObject2(e) || typeof e.role !== "string" || !byRole.has(e.role)) {
5734
6451
  throw new TurnTargetError(
5735
6452
  `headEdges.${side}[${i}].role is not a layer in this set`
5736
6453
  );
5737
6454
  }
5738
- if (!finite(e.x)) {
6455
+ if (!finite2(e.x)) {
5739
6456
  throw new TurnTargetError(
5740
6457
  `headEdges.${side}[${i}].x must be a finite number`
5741
6458
  );
@@ -5755,25 +6472,25 @@ function checkHeadEdges(edges, byRole) {
5755
6472
  }
5756
6473
  function checkStrandEdges(strand, irises, hair) {
5757
6474
  if (strand === void 0) return;
5758
- if (!isObject(strand)) {
6475
+ if (!isObject2(strand)) {
5759
6476
  throw new TurnTargetError("strandEdges must be an object");
5760
6477
  }
5761
6478
  for (const side of ["left", "right"]) {
5762
6479
  const s = strand[side];
5763
6480
  if (s === void 0) continue;
5764
6481
  const at = `strandEdges.${side}`;
5765
- if (!isObject(s)) throw new TurnTargetError(`${at} must be an object`);
6482
+ if (!isObject2(s)) throw new TurnTargetError(`${at} must be an object`);
5766
6483
  if (hair === void 0) {
5767
6484
  throw new TurnTargetError(
5768
6485
  `${at} names a bangs' run, but this layer set has no hair_front`
5769
6486
  );
5770
6487
  }
5771
6488
  for (const k of ["y", "irisOuter", "irisInner", "runOuter"]) {
5772
- if (!finite(s[k])) {
6489
+ if (!finite2(s[k])) {
5773
6490
  throw new TurnTargetError(`${at}.${k} must be a finite number`);
5774
6491
  }
5775
6492
  }
5776
- if (s.runFace !== null && !finite(s.runFace)) {
6493
+ if (s.runFace !== null && !finite2(s.runFace)) {
5777
6494
  throw new TurnTargetError(
5778
6495
  `${at}.runFace must be a finite number or null`
5779
6496
  );
@@ -6242,9 +6959,9 @@ function replay(op, i, ctx, sums) {
6242
6959
  const x = span2(`${name}.region.x`, region.x);
6243
6960
  const y = span2(`${name}.region.y`, region.y);
6244
6961
  const by = fields(`${name}.by`, o.by, BY_KEYS);
6245
- const dx = finite2(`${name}.by.dx`, by.dx);
6246
- const dy = finite2(`${name}.by.dy`, by.dy);
6247
- const falloff = o.falloff === void 0 ? 0 : finite2(`${name}.falloff`, o.falloff, 0);
6962
+ const dx = finite3(`${name}.by.dx`, by.dx);
6963
+ const dy = finite3(`${name}.by.dy`, by.dy);
6964
+ const falloff = o.falloff === void 0 ? 0 : finite3(`${name}.falloff`, o.falloff, 0);
6248
6965
  if (o.why !== void 0) text(`${name}.why`, o.why);
6249
6966
  const box = ctx.boxOf(role);
6250
6967
  if (box === void 0) {
@@ -6412,10 +7129,10 @@ function fields(name, v, known) {
6412
7129
  function numbers(name, v) {
6413
7130
  const o = fields(name, v, Object.keys(v ?? {}));
6414
7131
  return Object.fromEntries(
6415
- Object.entries(o).map(([k, n]) => [k, finite2(`${name}.${k}`, n)])
7132
+ Object.entries(o).map(([k, n]) => [k, finite3(`${name}.${k}`, n)])
6416
7133
  );
6417
7134
  }
6418
- var finite2 = (name, v, lo = -Infinity) => asPatch(() => checkedNumber(name, v, lo, Infinity));
7135
+ var finite3 = (name, v, lo = -Infinity) => asPatch(() => checkedNumber(name, v, lo, Infinity));
6419
7136
  function text(name, v) {
6420
7137
  if (typeof v !== "string") {
6421
7138
  throw new PatchError(`${name} must be a string, got ${String(v)}`);
@@ -6428,8 +7145,8 @@ function span2(name, v) {
6428
7145
  `${name} must be [from, to], got ${JSON.stringify(v)}`
6429
7146
  );
6430
7147
  }
6431
- const lo = finite2(`${name}[0]`, v[0]);
6432
- const hi = finite2(`${name}[1]`, v[1]);
7148
+ const lo = finite3(`${name}[0]`, v[0]);
7149
+ const hi = finite3(`${name}[1]`, v[1]);
6433
7150
  if (lo > hi) {
6434
7151
  throw new PatchError(
6435
7152
  `${name} [${lo}, ${hi}] runs backwards: ${name}[0] must not exceed ${name}[1]`
@@ -6438,6 +7155,224 @@ function span2(name, v) {
6438
7155
  return [lo, hi];
6439
7156
  }
6440
7157
 
7158
+ // src/auto-rig/custom-parts.ts
7159
+ var KEYS = [
7160
+ "id",
7161
+ "layer",
7162
+ "anchor",
7163
+ "tie",
7164
+ "after",
7165
+ "deformer",
7166
+ "rigid",
7167
+ "occlusion"
7168
+ ];
7169
+ var OCCLUSION_KEYS = ["by", "parameter", "stops"];
7170
+ var STOP_KEYS = ["value", "amount"];
7171
+ var LAYER_KEYS = ["fileName", "bbox"];
7172
+ var finite4 = (v) => typeof v === "number" && Number.isFinite(v);
7173
+ var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7174
+ var inside = (b, [x, y]) => x >= b.x && x < b.x + b.w && y >= b.y && y < b.y + b.h;
7175
+ var RESERVED = new Set(
7176
+ ROLE_TABLE.flatMap((r) => [r.role, ...partIdsOfRole(r.role)])
7177
+ );
7178
+ function checkCustomParts(customParts, byRole, canvas, parameters, physicsOutputs) {
7179
+ if (!Array.isArray(customParts)) {
7180
+ throw new CustomPartError(
7181
+ `customParts must be an array, got ${customParts === null ? "null" : typeof customParts}`
7182
+ );
7183
+ }
7184
+ const present = [...byRole.keys()];
7185
+ const roleField = (at, v) => {
7186
+ const layer = typeof v === "string" ? byRole.get(v) : void 0;
7187
+ if (layer === void 0) {
7188
+ throw new CustomPartError(
7189
+ `${at} must be a role of the set, one of ${present.join(", ")}; got ${JSON.stringify(v)}`
7190
+ );
7191
+ }
7192
+ return layer;
7193
+ };
7194
+ const ids = /* @__PURE__ */ new Set();
7195
+ customParts.forEach((entry, i) => {
7196
+ const at = `customParts[${i}]`;
7197
+ if (!isObject3(entry)) {
7198
+ throw new CustomPartError(`${at} must be a plain object`);
7199
+ }
7200
+ checkedKeys(at, entry, KEYS, CustomPartError);
7201
+ const { id, layer, anchor, tie, after, deformer, rigid, occlusion } = entry;
7202
+ if (typeof id !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(id)) {
7203
+ throw new CustomPartError(
7204
+ `${at}.id must be a plain identifier (letters, digits, underscore; not starting with a digit), got ${JSON.stringify(id)}`
7205
+ );
7206
+ }
7207
+ if (RESERVED.has(id)) {
7208
+ throw new CustomPartError(
7209
+ `${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`
7210
+ );
7211
+ }
7212
+ if (ids.has(id)) {
7213
+ throw new CustomPartError(
7214
+ `${at}.id "${id}" repeats an earlier entry's id`
7215
+ );
7216
+ }
7217
+ ids.add(id);
7218
+ if (!isObject3(layer)) {
7219
+ throw new CustomPartError(`${at}.layer must be a plain object`);
7220
+ }
7221
+ checkedKeys(`${at}.layer`, layer, LAYER_KEYS, CustomPartError);
7222
+ if (typeof layer.fileName !== "string" || layer.fileName === "") {
7223
+ throw new CustomPartError(
7224
+ `${at}.layer.fileName must be a non-empty string`
7225
+ );
7226
+ }
7227
+ const b = isObject3(layer.bbox) ? layer.bbox : {};
7228
+ const { x, y, w, h } = b;
7229
+ 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) {
7230
+ throw new CustomPartError(
7231
+ `${at}.layer.bbox must be a finite, non-empty box inside the ${canvas.width}x${canvas.height} canvas, got ${JSON.stringify(layer.bbox)}`
7232
+ );
7233
+ }
7234
+ const box = { x, y, w, h };
7235
+ const anchorLayer = roleField(`${at}.anchor`, anchor);
7236
+ roleField(`${at}.after`, after);
7237
+ if (!Array.isArray(tie) || tie.length !== 2 || !tie.every(finite4) || !inside(box, tie) || !inside(anchorLayer.bbox, tie)) {
7238
+ throw new CustomPartError(
7239
+ `${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)}`
7240
+ );
7241
+ }
7242
+ if (deformer !== void 0 && deformer !== "headDeformer") {
7243
+ throw new CustomPartError(
7244
+ `${at}.deformer "${String(deformer)}" is not accepted: headDeformer is the only deformer a custom part hangs on`
7245
+ );
7246
+ }
7247
+ if (rigid !== void 0 && typeof rigid !== "boolean") {
7248
+ throw new CustomPartError(
7249
+ `${at}.rigid must be true or false, got ${JSON.stringify(rigid)}`
7250
+ );
7251
+ }
7252
+ if (occlusion !== void 0) {
7253
+ checkOcclusion(
7254
+ `${at}.occlusion`,
7255
+ occlusion,
7256
+ present,
7257
+ parameters,
7258
+ physicsOutputs
7259
+ );
7260
+ }
7261
+ });
7262
+ return customParts;
7263
+ }
7264
+ function checkOcclusion(at, occlusion, present, parameters, physicsOutputs) {
7265
+ if (!isObject3(occlusion)) {
7266
+ throw new CustomPartError(`${at} must be a plain object`);
7267
+ }
7268
+ checkedKeys(at, occlusion, OCCLUSION_KEYS, CustomPartError);
7269
+ const { by, parameter, stops } = occlusion;
7270
+ if (!Array.isArray(by) || by.length === 0) {
7271
+ throw new CustomPartError(
7272
+ `${at}.by must be a non-empty array of roles of the set, one of ${present.join(", ")}`
7273
+ );
7274
+ }
7275
+ by.forEach((role, j) => {
7276
+ if (typeof role !== "string" || !present.includes(role)) {
7277
+ throw new CustomPartError(
7278
+ `${at}.by[${j}] must be a role of the set, one of ${present.join(", ")}; got ${JSON.stringify(role)}`
7279
+ );
7280
+ }
7281
+ if (by.indexOf(role) !== j) {
7282
+ throw new CustomPartError(
7283
+ `${at}.by[${j}] "${role}" repeats an earlier role`
7284
+ );
7285
+ }
7286
+ });
7287
+ const descriptor = parameters.find((p) => p.id === parameter);
7288
+ if (typeof parameter !== "string" || descriptor === void 0) {
7289
+ throw new CustomPartError(
7290
+ `${at}.parameter must be a parameter the rig declares, got ${JSON.stringify(parameter)}`
7291
+ );
7292
+ }
7293
+ if (physicsOutputs.has(parameter)) {
7294
+ throw new CustomPartError(
7295
+ `${at}.parameter "${parameter}" is a physics output; an occlusion amount cannot be keyed on one`
7296
+ );
7297
+ }
7298
+ if (!Array.isArray(stops) || stops.length === 0) {
7299
+ throw new CustomPartError(`${at}.stops must be a non-empty array`);
7300
+ }
7301
+ const { min, max } = descriptor;
7302
+ let previous = -Infinity;
7303
+ stops.forEach((stop, k) => {
7304
+ const s = `${at}.stops[${k}]`;
7305
+ if (!isObject3(stop))
7306
+ throw new CustomPartError(`${s} must be a plain object`);
7307
+ checkedKeys(s, stop, STOP_KEYS, CustomPartError);
7308
+ const { value, amount } = stop;
7309
+ if (!finite4(value)) {
7310
+ throw new CustomPartError(
7311
+ `${s}.value must be a finite number, got ${JSON.stringify(value)}`
7312
+ );
7313
+ }
7314
+ if (value < min || value > max) {
7315
+ throw new CustomPartError(
7316
+ `${s}.value ${value} is outside parameter "${parameter}" range [${min},${max}]`
7317
+ );
7318
+ }
7319
+ if (value <= previous) {
7320
+ throw new CustomPartError(`${s}.value must be strictly ascending`);
7321
+ }
7322
+ previous = value;
7323
+ if (!finite4(amount) || amount < 0 || amount > 1) {
7324
+ throw new CustomPartError(
7325
+ `${s}.amount must be a number in 0..1, got ${JSON.stringify(amount)}`
7326
+ );
7327
+ }
7328
+ });
7329
+ }
7330
+ var CELL_PX2 = 20;
7331
+ function buildCustomParts(customParts, canvas) {
7332
+ return customParts.map(({ id, layer }) => {
7333
+ const b = boxOfPixels(layer.bbox, canvas.width, canvas.height);
7334
+ return {
7335
+ id,
7336
+ color: [1, 1, 1, 1],
7337
+ width: bw(b),
7338
+ height: bh(b),
7339
+ transform: { x: cx(b), y: cy(b) },
7340
+ order: 0,
7341
+ deformer: "headDeformer",
7342
+ mesh: gridMesh(
7343
+ Math.max(1, Math.round(bw(b) / CELL_PX2)),
7344
+ Math.max(1, Math.round(bh(b) / CELL_PX2))
7345
+ )
7346
+ };
7347
+ });
7348
+ }
7349
+ function writeOcclusion(customParts, built, parts) {
7350
+ const byId = new Map(parts.map((p) => [p.id, p]));
7351
+ customParts.forEach((entry, i) => {
7352
+ if (entry.occlusion === void 0) return;
7353
+ const { by, parameter, stops } = entry.occlusion;
7354
+ const ids = [];
7355
+ by.forEach((role, j) => {
7356
+ for (const id of partIdsOfRole(role)) {
7357
+ const part = byId.get(id);
7358
+ if (part === void 0) continue;
7359
+ 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;
7360
+ if (fault !== void 0) {
7361
+ throw new CustomPartError(
7362
+ `customParts[${i}].occlusion.by[${j}] "${role}" yields part "${id}", which ${fault.is}; ${fault.kind} cannot occlude`
7363
+ );
7364
+ }
7365
+ ids.push(id);
7366
+ }
7367
+ });
7368
+ built[i].occlusion = {
7369
+ by: ids,
7370
+ parameter,
7371
+ stops: stops.map((s) => ({ value: s.value, amount: s.amount }))
7372
+ };
7373
+ });
7374
+ }
7375
+
6441
7376
  // src/auto-rig/generate.ts
6442
7377
  var MESH_CELLS = {
6443
7378
  face: { px: 28, min: 4, max: 24 },
@@ -6479,6 +7414,16 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
6479
7414
  const hasNose = has("nose");
6480
7415
  const plateHalf = byRole.get("face").cropW / 2;
6481
7416
  checkHeadEdges(options.headEdges, byRole);
7417
+ const physics = has("hair_front") ? hairPhysics(has("hair_back")) : void 0;
7418
+ const parameters = declareParameters(new Set(byRole.keys()));
7419
+ const entries = options.customParts !== void 0 && !(Array.isArray(options.customParts) && options.customParts.length === 0) ? checkCustomParts(
7420
+ options.customParts,
7421
+ byRole,
7422
+ canvas,
7423
+ parameters,
7424
+ new Set((physics ?? []).map((r) => r.output.parameter))
7425
+ ) : [];
7426
+ const bakes = options.bakes === void 0 ? void 0 : parseBakes(options.bakes, entries, parameters);
6482
7427
  checkStrandEdges(
6483
7428
  options.strandEdges,
6484
7429
  sideIrises(byRole),
@@ -6628,7 +7573,6 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
6628
7573
  ...part.warps ?? []
6629
7574
  ];
6630
7575
  }
6631
- const parameters = declareParameters(new Set(byRole.keys()));
6632
7576
  const reports = options.patches === void 0 ? void 0 : applyPatches(options.patches, {
6633
7577
  grids: new Map(
6634
7578
  [...grids].map(([family, g]) => [WARP_ID[family], g])
@@ -6638,19 +7582,51 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
6638
7582
  faceWidth: byRole.get("face").cropW,
6639
7583
  parameters
6640
7584
  });
7585
+ const deformerList = deformers(
7586
+ frame,
7587
+ grids,
7588
+ bodyWarp?.deformer,
7589
+ arms,
7590
+ byRole
7591
+ );
7592
+ let bakeReports;
7593
+ if (entries.length > 0) {
7594
+ const built = buildCustomParts(entries, canvas);
7595
+ entries.forEach((entry, i) => {
7596
+ const group = new Set(partIdsOfRole(entry.after));
7597
+ for (let j = 0; j < i; j++) {
7598
+ if (entries[j].after === entry.after) group.add(entries[j].id);
7599
+ }
7600
+ const last = parts.map((p) => group.has(p.id)).lastIndexOf(true);
7601
+ parts.splice(last + 1, 0, built[i]);
7602
+ });
7603
+ parts.forEach((p, i) => {
7604
+ p.order = i;
7605
+ });
7606
+ writeOcclusion(entries, built, parts);
7607
+ if (bakes !== void 0) {
7608
+ bakeReports = applyBakes(bakes, built, entries, {
7609
+ canvas,
7610
+ deformers: deformerList,
7611
+ parameters,
7612
+ faceWidth: byRole.get("face").cropW
7613
+ });
7614
+ }
7615
+ }
6641
7616
  const model = {
6642
7617
  version: import_format12.IKI_FORMAT_VERSION,
6643
7618
  name: "auto-rigged",
6644
7619
  canvas: { width: canvas.width, height: canvas.height },
6645
7620
  parameters,
6646
7621
  parts,
6647
- deformers: deformers(frame, grids, bodyWarp?.deformer, arms, byRole),
6648
- ...has("hair_front") ? { physics: hairPhysics(has("hair_back")) } : {},
7622
+ deformers: deformerList,
7623
+ ...physics !== void 0 ? { physics } : {},
6649
7624
  expressions: defaultExpressions(new Set(parameters.map((p) => p.id))),
6650
7625
  motions: defaultMotions(new Set(parameters.map((p) => p.id)))
6651
7626
  };
6652
7627
  const valid = (0, import_format12.parseIkiModel)(model);
6653
7628
  if (reports !== void 0) options.onPatchesApplied?.(reports);
7629
+ if (bakeReports !== void 0) options.onBakesApplied?.(bakeReports);
6654
7630
  options.onRigPlanResolved?.(
6655
7631
  rigPlanReport(options.rigPlan ?? {}, plan, targets, fit)
6656
7632
  );
@@ -7133,7 +8109,9 @@ function declareParameters(roles) {
7133
8109
  AddDeformer,
7134
8110
  AddPart,
7135
8111
  AddPhysicsRig,
8112
+ BakeError,
7136
8113
  CaptureGridKeyform,
8114
+ CustomPartError,
7137
8115
  DEFAULT_TURN_TARGETS,
7138
8116
  DeleteDeformer,
7139
8117
  DeletePart,
@@ -7189,6 +8167,7 @@ function declareParameters(roles) {
7189
8167
  packAtlas,
7190
8168
  parseLayerRoles,
7191
8169
  partIdsOfRole,
8170
+ rigidResidual,
7192
8171
  styleRemovedMessage,
7193
8172
  upsertGridKeyform,
7194
8173
  uvRectFor,