@ikijs/editor 0.30.1 → 0.31.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
@@ -2114,6 +2114,18 @@ var PatchError = class extends Error {
2114
2114
  this.name = "PatchError";
2115
2115
  }
2116
2116
  };
2117
+ var CustomPartError = class extends Error {
2118
+ constructor(message) {
2119
+ super(message);
2120
+ this.name = "CustomPartError";
2121
+ }
2122
+ };
2123
+ var BakeError = class extends Error {
2124
+ constructor(message) {
2125
+ super(message);
2126
+ this.name = "BakeError";
2127
+ }
2128
+ };
2117
2129
 
2118
2130
  // src/auto-rig/layout.ts
2119
2131
  function boxOfLayer(layer) {
@@ -2522,10 +2534,10 @@ function checkSidesHaveFront(roles) {
2522
2534
  function checkMouth(roles) {
2523
2535
  const has = new Set(roles);
2524
2536
  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) {
2537
+ const inside2 = LIP_INSIDE_ROLES.filter((r) => has.has(r));
2538
+ if (inside2.length > 0 && present.length === 0) {
2527
2539
  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)`
2540
+ `auto-rig: ${inside2[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
2529
2541
  );
2530
2542
  }
2531
2543
  if (present.length === 0) {
@@ -2745,10 +2757,10 @@ function mouthFold(role, opening) {
2745
2757
  const stackThin = (c, y) => y > c.Tu ? c.Tu + UPPER_THIN * (y - c.Tu) : y;
2746
2758
  return (col, y, v) => {
2747
2759
  const k = 1 - v;
2748
- const inside = col >= x0 && col <= x1;
2760
+ const inside2 = col >= x0 && col <= x1;
2749
2761
  switch (role) {
2750
2762
  case "lip_lower": {
2751
- if (!inside) return [0, 0];
2763
+ if (!inside2) return [0, 0];
2752
2764
  const c = opening.at(col);
2753
2765
  return [0, (c.seam + c.overlap - c.Bl) * k];
2754
2766
  }
@@ -2772,9 +2784,9 @@ function mouthFold(role, opening) {
2772
2784
  line = opening.line(c += col < x0 ? 1 : -1);
2773
2785
  const thin = line.bottom + UPPER_THIN * (y - line.bottom);
2774
2786
  const mid = line.bottom + UPPER_THIN * (line.top - line.bottom) / 2;
2775
- const travel = inside ? opening.at(col).seam - line.bottom : 0;
2787
+ const travel = inside2 ? opening.at(col).seam - line.bottom : 0;
2776
2788
  const shut = mid + line.squash * (thin - mid) + travel;
2777
- const pull = inside ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
2789
+ const pull = inside2 ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
2778
2790
  return [pull * k, thin - y + (shut - thin) * k];
2779
2791
  }
2780
2792
  }
@@ -4107,14 +4119,14 @@ function cuesAt(ctx, land2, ax) {
4107
4119
  const l0 = land2(L.role, L.lo, L.y, ax);
4108
4120
  const l1 = land2(L.role, L.hi, L.y, ax);
4109
4121
  const r0 = land2(R.role, R.lo, R.y, ax);
4110
- const r1 = land2(R.role, R.hi, R.y, ax);
4122
+ const r12 = land2(R.role, R.hi, R.y, ax);
4111
4123
  const wl = l1 - l0;
4112
- const wr = r1 - r0;
4124
+ const wr = r12 - r0;
4113
4125
  const wl0 = L.hi - L.lo;
4114
4126
  const wr0 = R.hi - R.lo;
4115
4127
  const ratio = ax < 0 ? wl / wr / (wl0 / wr0) : wr / wl / (wr0 / wl0);
4116
4128
  const pc0 = (L.lo + L.hi + R.lo + R.hi) / 4;
4117
- const pc = (l0 + l1 + r0 + r1) / 4;
4129
+ const pc = (l0 + l1 + r0 + r12) / 4;
4118
4130
  const y = ctx.frame.eyeY;
4119
4131
  const hl0 = Math.min(...ctx.candidates.left.map((c) => c.x));
4120
4132
  const hr0 = Math.max(...ctx.candidates.right.map((c) => c.x));
@@ -4752,7 +4764,7 @@ function blendStops(values, offsets, v) {
4752
4764
  const t = (x - values[i]) / (values[i + 1] - values[i]);
4753
4765
  return offsets[i].map((o, k) => o + (offsets[i + 1][k] - o) * t);
4754
4766
  }
4755
- function land(l, offsets, x, y) {
4767
+ function bindToRest(l, x, y) {
4756
4768
  const { cols, rows, points } = l;
4757
4769
  const stride = cols + 1;
4758
4770
  let col = cols - 1;
@@ -4773,8 +4785,17 @@ function land(l, offsets, x, y) {
4773
4785
  const x1 = points[(col + 1) * 2];
4774
4786
  const yT = points[row * stride * 2 + 1];
4775
4787
  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);
4788
+ return {
4789
+ row,
4790
+ col,
4791
+ s: clamp((x - x0) / (x1 - x0), 0, 1),
4792
+ t: clamp((yT - y) / (yT - yB), 0, 1)
4793
+ };
4794
+ }
4795
+ function land(l, offsets, x, y) {
4796
+ const { cols, points } = l;
4797
+ const stride = cols + 1;
4798
+ const { row, col, s, t } = bindToRest(l, x, y);
4778
4799
  const at = (r, c) => {
4779
4800
  const k = (r * stride + c) * 2;
4780
4801
  return [points[k] + offsets[k], points[k + 1] + offsets[k + 1]];
@@ -5114,7 +5135,7 @@ function armGeometry(layer, bodyAxisX) {
5114
5135
  b.x1 - elbow.x,
5115
5136
  b.y1 - elbow.y
5116
5137
  );
5117
- const inside = (x, y) => {
5138
+ const inside2 = (x, y) => {
5118
5139
  const k = Math.floor(y) - bbox.y;
5119
5140
  if (k < 0 || k >= bbox.h) return false;
5120
5141
  const [rs, re] = span3(k);
@@ -5123,7 +5144,7 @@ function armGeometry(layer, bodyAxisX) {
5123
5144
  const capRays = Array.from({ length: CAP_SEGMENTS + 1 }, (_, i) => {
5124
5145
  const a = Math.PI * i / CAP_SEGMENTS;
5125
5146
  let t = 1;
5126
- while (t <= capRadius + CAP_INSET && inside(
5147
+ while (t <= capRadius + CAP_INSET && inside2(
5127
5148
  elbow.x + canvasW / 2 + t * Math.cos(a),
5128
5149
  canvasH / 2 - elbow.y - t * Math.sin(a)
5129
5150
  )) {
@@ -5325,6 +5346,675 @@ function forearmPoseDeformer(role, arm) {
5325
5346
  };
5326
5347
  }
5327
5348
 
5349
+ // src/auto-rig/chain.ts
5350
+ function multiply(a, b) {
5351
+ return [
5352
+ a[0] * b[0] + a[2] * b[1],
5353
+ a[1] * b[0] + a[3] * b[1],
5354
+ a[0] * b[2] + a[2] * b[3],
5355
+ a[1] * b[2] + a[3] * b[3],
5356
+ a[0] * b[4] + a[2] * b[5] + a[4],
5357
+ a[1] * b[4] + a[3] * b[5] + a[5]
5358
+ ];
5359
+ }
5360
+ function valueOf(parameters, values, id) {
5361
+ const p = parameters.find((q) => q.id === id);
5362
+ if (p === void 0) return 0;
5363
+ const v = values[id] ?? p.default;
5364
+ return Math.min(p.max, Math.max(p.min, v));
5365
+ }
5366
+ function ownMatrix(d, parameters, values) {
5367
+ let x = d.transform?.x ?? 0;
5368
+ let y = d.transform?.y ?? 0;
5369
+ let rotation = d.transform?.rotation ?? 0;
5370
+ let sx = d.transform?.scaleX ?? 1;
5371
+ let sy = d.transform?.scaleY ?? 1;
5372
+ for (const b of d.bindings ?? []) {
5373
+ const p = parameters.find((q) => q.id === b.parameter);
5374
+ const t = p === void 0 || p.max === p.min ? 0 : (valueOf(parameters, values, b.parameter) - p.min) / (p.max - p.min);
5375
+ const v = b.from + (b.to - b.from) * t;
5376
+ if (b.channel === "translateX") x += v;
5377
+ else if (b.channel === "translateY") y += v;
5378
+ else if (b.channel === "rotate") rotation += v;
5379
+ else if (b.channel === "scaleX") sx += v;
5380
+ else if (b.channel === "scaleY") sy += v;
5381
+ }
5382
+ const r = rotation * Math.PI / 180;
5383
+ const c = Math.cos(r);
5384
+ const s = Math.sin(r);
5385
+ const trs = [c * sx, s * sx, -s * sy, c * sy, x, y];
5386
+ return multiply(multiply([1, 0, 0, 1, d.pivot.x, d.pivot.y], trs), [
5387
+ 1,
5388
+ 0,
5389
+ 0,
5390
+ 1,
5391
+ -d.pivot.x,
5392
+ -d.pivot.y
5393
+ ]);
5394
+ }
5395
+ function localGrid(w, parameters, values) {
5396
+ const points = [...w.grid.points];
5397
+ for (const warp of w.warps ?? []) {
5398
+ const offsets = blendStops(
5399
+ warp.keyforms.map((k) => k.value),
5400
+ warp.keyforms.map((k) => k.offsets),
5401
+ valueOf(parameters, values, warp.parameter)
5402
+ );
5403
+ for (let i = 0; i < points.length; i++) points[i] += offsets[i];
5404
+ }
5405
+ return points;
5406
+ }
5407
+ function riddenFrame(pivot, w, q) {
5408
+ const { cols, points: rest } = w.grid;
5409
+ const stride = cols + 1;
5410
+ const { row, col, s, t } = bindToRest(w.grid, pivot.x, pivot.y);
5411
+ const xl = rest[col * 2];
5412
+ const xr = rest[(col + 1) * 2];
5413
+ const yt = rest[row * stride * 2 + 1];
5414
+ const yb = rest[(row + 1) * stride * 2 + 1];
5415
+ const i00 = (row * stride + col) * 2;
5416
+ const i10 = (row * stride + col + 1) * 2;
5417
+ const i01 = ((row + 1) * stride + col) * 2;
5418
+ const i11 = ((row + 1) * stride + col + 1) * 2;
5419
+ const lerp = (a, b, k) => a + (b - a) * k;
5420
+ const topX = lerp(q[i00], q[i10], s);
5421
+ const topY = lerp(q[i00 + 1], q[i10 + 1], s);
5422
+ const botX = lerp(q[i01], q[i11], s);
5423
+ const botY = lerp(q[i01 + 1], q[i11 + 1], s);
5424
+ const px = lerp(topX, botX, t);
5425
+ const py = lerp(topY, botY, t);
5426
+ const width = xr - xl;
5427
+ const height = yt - yb;
5428
+ const exX = lerp(q[i10] - q[i00], q[i11] - q[i01], t) / width;
5429
+ const exY = lerp(q[i10 + 1] - q[i00 + 1], q[i11 + 1] - q[i01 + 1], t) / width;
5430
+ const eyX = -lerp(q[i01] - q[i00], q[i11] - q[i10], s) / height;
5431
+ const eyY = -lerp(q[i01 + 1] - q[i00 + 1], q[i11 + 1] - q[i10 + 1], s) / height;
5432
+ const theta = Math.atan2(exY - eyX, exX + eyY);
5433
+ const c = Math.cos(theta);
5434
+ const sn = Math.sin(theta);
5435
+ return [
5436
+ c,
5437
+ sn,
5438
+ -sn,
5439
+ c,
5440
+ px - (c * pivot.x - sn * pivot.y),
5441
+ py - (sn * pivot.x + c * pivot.y)
5442
+ ];
5443
+ }
5444
+ function matrixWorldAt(deformers2, parameters, id, values = {}) {
5445
+ for (const key of Object.keys(values)) {
5446
+ if (!parameters.some((q) => q.id === key)) {
5447
+ throw new Error(`chain: unknown parameter "${key}"`);
5448
+ }
5449
+ }
5450
+ const d = deformers2.find((x) => x.id === id);
5451
+ if (d === void 0) throw new Error(`chain: no deformer "${id}"`);
5452
+ if (d.kind === "warp") {
5453
+ throw new Error(`chain: "${id}" is a warp deformer, not a matrix`);
5454
+ }
5455
+ const own = ownMatrix(d, parameters, values);
5456
+ if (d.parent === void 0) return own;
5457
+ const parent = deformers2.find((x) => x.id === d.parent);
5458
+ if (parent === void 0) {
5459
+ throw new Error(`chain: "${id}" has an unknown parent "${d.parent}"`);
5460
+ }
5461
+ if (parent.kind !== "warp") {
5462
+ throw new Error(
5463
+ `chain: "${id}" hangs from the matrix deformer "${parent.id}"; only a root warp is supported`
5464
+ );
5465
+ }
5466
+ if (parent.parent !== void 0) {
5467
+ throw new Error(
5468
+ `chain: "${id}" hangs from warp "${parent.id}", which has a parent "${parent.parent}"; only a root warp is supported`
5469
+ );
5470
+ }
5471
+ if (parent.warp2d !== void 0) {
5472
+ throw new Error(
5473
+ `chain: "${id}" hangs from warp "${parent.id}", which carries a warp2d; only 1D warps are supported`
5474
+ );
5475
+ }
5476
+ return multiply(
5477
+ riddenFrame(d.pivot, parent, localGrid(parent, parameters, values)),
5478
+ own
5479
+ );
5480
+ }
5481
+
5482
+ // src/auto-rig/bakes.ts
5483
+ var TARGET_KEYS = [
5484
+ "id",
5485
+ "param",
5486
+ "parts",
5487
+ "box",
5488
+ "grid",
5489
+ "stops",
5490
+ "mirror",
5491
+ "why"
5492
+ ];
5493
+ var BOX_KEYS = ["x", "y", "w", "h"];
5494
+ var MAX_VERTICES = 65536;
5495
+ var CELL_PX = [4, 400];
5496
+ var MAX_MESH_COLS = 64;
5497
+ var MAX_MESH_ROWS = 400;
5498
+ var DEFAULT_BOX_MARGIN = 2;
5499
+ var SAME_STOP = 1e-9;
5500
+ var PLAIN_DECIMAL = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
5501
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
5502
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5503
+ var show = (v) => typeof v === "number" ? String(v) : JSON.stringify(v) ?? String(v);
5504
+ function mirrorLattice(lattice2, cols, rows) {
5505
+ const out = new Array(lattice2.length);
5506
+ for (let r = 0; r <= rows; r++) {
5507
+ for (let c = 0; c <= cols; c++) {
5508
+ const [dx, dy] = lattice2[r * (cols + 1) + (cols - c)];
5509
+ out[r * (cols + 1) + c] = [-dx, dy];
5510
+ }
5511
+ }
5512
+ return out;
5513
+ }
5514
+ var r1 = (v) => Math.round(v * 10) / 10;
5515
+ var findStop = (stops, v) => [...stops.keys()].find((k) => Math.abs(k - v) <= SAME_STOP);
5516
+ function checkGrid(at, grid) {
5517
+ if (!isObject(grid)) {
5518
+ throw new BakeError(`${at}.grid must be { cols, rows }, got ${show(grid)}`);
5519
+ }
5520
+ checkedKeys(`${at}.grid`, grid, ["cols", "rows"], BakeError);
5521
+ const out = { cols: 0, rows: 0 };
5522
+ for (const key of ["cols", "rows"]) {
5523
+ const v = checkedNumber(
5524
+ `${at}.grid.${key}`,
5525
+ grid[key],
5526
+ 1,
5527
+ 32,
5528
+ false,
5529
+ BakeError
5530
+ );
5531
+ if (!Number.isInteger(v)) {
5532
+ throw new BakeError(`${at}.grid.${key} must be an integer, got ${v}`);
5533
+ }
5534
+ out[key] = v;
5535
+ }
5536
+ return out;
5537
+ }
5538
+ function checkBox(at, box, partBoxes) {
5539
+ if (box === void 0) {
5540
+ const x0 = Math.min(...partBoxes.map((b) => b.x)) - DEFAULT_BOX_MARGIN;
5541
+ const y0 = Math.min(...partBoxes.map((b) => b.y)) - DEFAULT_BOX_MARGIN;
5542
+ const x1 = Math.max(...partBoxes.map((b) => b.x + b.w)) + DEFAULT_BOX_MARGIN;
5543
+ const y1 = Math.max(...partBoxes.map((b) => b.y + b.h)) + DEFAULT_BOX_MARGIN;
5544
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
5545
+ }
5546
+ if (!isObject(box)) {
5547
+ throw new BakeError(`${at}.box must be { x, y, w, h }, got ${show(box)}`);
5548
+ }
5549
+ checkedKeys(`${at}.box`, box, BOX_KEYS, BakeError);
5550
+ const [x, y] = ["x", "y"].map(
5551
+ (k) => checkedNumber(
5552
+ `${at}.box.${k}`,
5553
+ box[k],
5554
+ -Infinity,
5555
+ Infinity,
5556
+ false,
5557
+ BakeError
5558
+ )
5559
+ );
5560
+ const [w, h] = ["w", "h"].map(
5561
+ (k) => checkedNumber(`${at}.box.${k}`, box[k], 0, Infinity, true, BakeError)
5562
+ );
5563
+ return { x, y, w, h };
5564
+ }
5565
+ function checkStops(at, t, info, rest, cols, rows) {
5566
+ if (!isObject(t.stops)) {
5567
+ throw new BakeError(
5568
+ `${at}.stops must be an object keyed by a ${info.id} value, got ${show(t.stops)}`
5569
+ );
5570
+ }
5571
+ const n = (cols + 1) * (rows + 1);
5572
+ const stops = /* @__PURE__ */ new Map();
5573
+ for (const [key, list] of Object.entries(t.stops)) {
5574
+ const sat = `${at}.stops["${key}"]`;
5575
+ if (!PLAIN_DECIMAL.test(key)) {
5576
+ throw new BakeError(`${sat} must be keyed by a plain decimal number`);
5577
+ }
5578
+ const v = Number(key);
5579
+ if (!Number.isFinite(v) || v < info.min || v > info.max) {
5580
+ throw new BakeError(
5581
+ `${sat} is outside ${info.id}'s range ${info.min} to ${info.max}`
5582
+ );
5583
+ }
5584
+ if (Math.abs(v - rest) <= SAME_STOP) {
5585
+ throw new BakeError(
5586
+ `${sat} is ${info.id}'s rest value ${rest}: rest is implicit, leave it out`
5587
+ );
5588
+ }
5589
+ if (findStop(stops, v) !== void 0) {
5590
+ throw new BakeError(`${sat} repeats an earlier stop`);
5591
+ }
5592
+ if (!Array.isArray(list) || list.length !== n) {
5593
+ throw new BakeError(
5594
+ `${sat} must be ${n} points [dx, dy] (${cols + 1} x ${rows + 1}), got ${Array.isArray(list) ? `${list.length} points` : show(list)}`
5595
+ );
5596
+ }
5597
+ list.forEach((pt, k) => {
5598
+ if (!Array.isArray(pt) || pt.length !== 2) {
5599
+ throw new BakeError(`${sat}[${k}] must be [dx, dy], got ${show(pt)}`);
5600
+ }
5601
+ pt.forEach((c, j) => {
5602
+ if (!finite(c)) {
5603
+ throw new BakeError(
5604
+ `${sat}[${k}][${j}] must be a finite number, got ${String(c)}`
5605
+ );
5606
+ }
5607
+ });
5608
+ });
5609
+ stops.set(v, list);
5610
+ }
5611
+ if (stops.size === 0) {
5612
+ throw new BakeError(`${at}.stops must have at least one stop`);
5613
+ }
5614
+ if (t.mirror === true) {
5615
+ for (const [v, lattice2] of [...stops]) {
5616
+ const m = 2 * rest - v;
5617
+ if (m < info.min - SAME_STOP || m > info.max + SAME_STOP) continue;
5618
+ if (Math.abs(m - rest) <= SAME_STOP || findStop(stops, m) !== void 0) {
5619
+ continue;
5620
+ }
5621
+ stops.set(
5622
+ Math.min(info.max, Math.max(info.min, m)),
5623
+ mirrorLattice(lattice2, cols, rows)
5624
+ );
5625
+ }
5626
+ }
5627
+ return stops;
5628
+ }
5629
+ function checkTarget(t, i, customParts, paramById) {
5630
+ const at = `bakes.targets[${i}]`;
5631
+ if (!isObject(t)) {
5632
+ throw new BakeError(`${at} must be a plain object, got ${show(t)}`);
5633
+ }
5634
+ checkedKeys(at, t, TARGET_KEYS, BakeError);
5635
+ if (t.id !== void 0 && typeof t.id !== "string") {
5636
+ throw new BakeError(`${at}.id must be a string, got ${show(t.id)}`);
5637
+ }
5638
+ if (t.why !== void 0 && typeof t.why !== "string") {
5639
+ throw new BakeError(`${at}.why must be a string, got ${show(t.why)}`);
5640
+ }
5641
+ const info = typeof t.param === "string" ? paramById.get(t.param) : void 0;
5642
+ if (info === void 0) {
5643
+ throw new BakeError(
5644
+ `${at}.param must be a parameter the model declares, got ${show(t.param)}`
5645
+ );
5646
+ }
5647
+ const rest = Math.min(info.max, Math.max(info.min, info.default));
5648
+ const customIds = customParts.map((c) => c.id);
5649
+ if (!Array.isArray(t.parts) || t.parts.length === 0) {
5650
+ throw new BakeError(
5651
+ `${at}.parts must be a non-empty array of part ids, got ${show(t.parts)}`
5652
+ );
5653
+ }
5654
+ const parts = t.parts;
5655
+ parts.forEach((id, k) => {
5656
+ if (typeof id !== "string" || !customIds.includes(id)) {
5657
+ throw new BakeError(
5658
+ `${at}.parts[${k}] must be a custom part id (${customIds.join(", ")}): a bake moves custom parts only, got ${show(id)}`
5659
+ );
5660
+ }
5661
+ if (parts.indexOf(id) !== k) {
5662
+ throw new BakeError(`${at}.parts[${k}] repeats "${id}"`);
5663
+ }
5664
+ });
5665
+ const { cols, rows } = checkGrid(at, t.grid);
5666
+ const partBoxes = parts.map(
5667
+ (id) => customParts.find((c) => c.id === id).layer.bbox
5668
+ );
5669
+ const box = checkBox(at, t.box, partBoxes);
5670
+ if (t.mirror !== void 0 && typeof t.mirror !== "boolean") {
5671
+ throw new BakeError(
5672
+ `${at}.mirror must be true or false, got ${show(t.mirror)}`
5673
+ );
5674
+ }
5675
+ const stops = checkStops(at, t, info, rest, cols, rows);
5676
+ return {
5677
+ id: typeof t.id === "string" ? t.id : `target${i}`,
5678
+ param: info.id,
5679
+ rest,
5680
+ parts,
5681
+ box,
5682
+ cols,
5683
+ rows,
5684
+ stops
5685
+ };
5686
+ }
5687
+ function checkMesh(at, choice, box) {
5688
+ if (!isObject(choice)) {
5689
+ throw new BakeError(
5690
+ `${at} must be { rows, cols } or { cellPx }, got ${show(choice)}`
5691
+ );
5692
+ }
5693
+ checkedKeys(at, choice, ["rows", "cols", "cellPx"], BakeError);
5694
+ if (choice.cellPx !== void 0) {
5695
+ if (choice.rows !== void 0 || choice.cols !== void 0) {
5696
+ throw new BakeError(`${at} takes { cellPx } or { rows, cols }, not both`);
5697
+ }
5698
+ const cell = checkedNumber(
5699
+ `${at}.cellPx`,
5700
+ choice.cellPx,
5701
+ CELL_PX[0],
5702
+ CELL_PX[1],
5703
+ false,
5704
+ BakeError
5705
+ );
5706
+ const count = (Math.max(1, Math.round(box.w / cell)) + 1) * (Math.max(1, Math.round(box.h / cell)) + 1);
5707
+ if (count > MAX_VERTICES) {
5708
+ throw new BakeError(
5709
+ `${at}.cellPx ${cell} makes ${count} vertices, over the format's ${MAX_VERTICES} limit`
5710
+ );
5711
+ }
5712
+ return { cellPx: cell };
5713
+ }
5714
+ const cols = checkedNumber(
5715
+ `${at}.cols`,
5716
+ choice.cols,
5717
+ 1,
5718
+ MAX_MESH_COLS,
5719
+ false,
5720
+ BakeError
5721
+ );
5722
+ if (!Number.isInteger(cols)) {
5723
+ throw new BakeError(`${at}.cols must be an integer, got ${cols}`);
5724
+ }
5725
+ const ys = choice.rows;
5726
+ if (!Array.isArray(ys) || ys.length < 2 || ys.length > MAX_MESH_ROWS) {
5727
+ throw new BakeError(
5728
+ `${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)}`
5729
+ );
5730
+ }
5731
+ ys.forEach((y, k) => {
5732
+ if (!finite(y)) {
5733
+ throw new BakeError(
5734
+ `${at}.rows[${k}] must be a finite number, got ${String(y)}`
5735
+ );
5736
+ }
5737
+ if (k > 0 && !(y > ys[k - 1])) {
5738
+ throw new BakeError(
5739
+ `${at}.rows[${k}] must be below the row before it (rows are strictly increasing), got ${y} after ${String(ys[k - 1])}`
5740
+ );
5741
+ }
5742
+ });
5743
+ const last = ys.length - 1;
5744
+ if (Math.abs(ys[0] - box.y) > 0.5) {
5745
+ throw new BakeError(
5746
+ `${at}.rows[0] must be the part's box top ${r1(box.y)} (within 0.5 px), got ${String(ys[0])}`
5747
+ );
5748
+ }
5749
+ if (Math.abs(ys[last] - (box.y + box.h)) > 0.5) {
5750
+ throw new BakeError(
5751
+ `${at}.rows[${last}] must be the part's box bottom ${r1(box.y + box.h)} (within 0.5 px), got ${String(ys[last])}`
5752
+ );
5753
+ }
5754
+ return { rows: ys, cols };
5755
+ }
5756
+ function parseBakes(bakes, customParts, parameters) {
5757
+ if (customParts === void 0 || customParts.length === 0) {
5758
+ throw new BakeError(
5759
+ "bakes needs customParts: a bake moves custom parts only, and none were given"
5760
+ );
5761
+ }
5762
+ if (!isObject(bakes)) {
5763
+ throw new BakeError(`bakes must be a plain object, got ${show(bakes)}`);
5764
+ }
5765
+ checkedKeys("bakes", bakes, ["targets", "meshes"], BakeError);
5766
+ const { targets, meshes } = bakes;
5767
+ if (!Array.isArray(targets) || targets.length === 0) {
5768
+ throw new BakeError("bakes.targets must be a non-empty array");
5769
+ }
5770
+ const paramById = new Map(parameters.map((p) => [p.id, p]));
5771
+ const parsedTargets = targets.map(
5772
+ (t, i) => checkTarget(t, i, customParts, paramById)
5773
+ );
5774
+ const parsedMeshes = /* @__PURE__ */ new Map();
5775
+ if (meshes !== void 0) {
5776
+ if (!isObject(meshes)) {
5777
+ throw new BakeError(
5778
+ `bakes.meshes must be an object keyed by part id, got ${show(meshes)}`
5779
+ );
5780
+ }
5781
+ for (const [id, choice] of Object.entries(meshes)) {
5782
+ const at = `bakes.meshes.${id}`;
5783
+ const part = customParts.find((c) => c.id === id);
5784
+ if (part === void 0) {
5785
+ throw new BakeError(
5786
+ `${at}: "${id}" is not a custom part (${customParts.map((c) => c.id).join(", ")}): only a custom part is re-meshed`
5787
+ );
5788
+ }
5789
+ parsedMeshes.set(id, checkMesh(at, choice, part.layer.bbox));
5790
+ }
5791
+ }
5792
+ return { targets: parsedTargets, meshes: parsedMeshes };
5793
+ }
5794
+ var OFFSET_STEP = 1e-6;
5795
+ var RIGID_BEND = 25e-4;
5796
+ function rigidResidual(rest, moved) {
5797
+ const n = rest.length;
5798
+ if (moved.length !== n) {
5799
+ throw new Error(
5800
+ `rigidResidual: ${n} rest points but ${moved.length} moved points`
5801
+ );
5802
+ }
5803
+ if (n === 0) return 0;
5804
+ let pcx = 0;
5805
+ let pcy = 0;
5806
+ let qcx = 0;
5807
+ let qcy = 0;
5808
+ for (let i = 0; i < n; i++) {
5809
+ pcx += rest[i][0];
5810
+ pcy += rest[i][1];
5811
+ qcx += moved[i][0];
5812
+ qcy += moved[i][1];
5813
+ }
5814
+ pcx /= n;
5815
+ pcy /= n;
5816
+ qcx /= n;
5817
+ qcy /= n;
5818
+ let dot = 0;
5819
+ let cross = 0;
5820
+ for (let i = 0; i < n; i++) {
5821
+ const px = rest[i][0] - pcx;
5822
+ const py = rest[i][1] - pcy;
5823
+ const qx = moved[i][0] - qcx;
5824
+ const qy = moved[i][1] - qcy;
5825
+ dot += px * qx + py * qy;
5826
+ cross += px * qy - py * qx;
5827
+ }
5828
+ const theta = Math.atan2(cross, dot);
5829
+ const c = Math.cos(theta);
5830
+ const s = Math.sin(theta);
5831
+ let worst = 0;
5832
+ for (let i = 0; i < n; i++) {
5833
+ const px = rest[i][0] - pcx;
5834
+ const py = rest[i][1] - pcy;
5835
+ const ex = qcx + c * px - s * py - moved[i][0];
5836
+ const ey = qcy + s * px + c * py - moved[i][1];
5837
+ worst = Math.max(worst, Math.hypot(ex, ey));
5838
+ }
5839
+ return worst;
5840
+ }
5841
+ function latticeAt(t, lattice2, x, y) {
5842
+ const { cols, rows, box } = t;
5843
+ const u = clamp((x - box.x) / box.w, 0, 1) * cols;
5844
+ const v = clamp((y - box.y) / box.h, 0, 1) * rows;
5845
+ const c = Math.min(Math.floor(u), cols - 1);
5846
+ const r = Math.min(Math.floor(v), rows - 1);
5847
+ const fx = u - c;
5848
+ const fy = v - r;
5849
+ const at = (rr, cc) => lattice2[rr * (cols + 1) + cc];
5850
+ const mix = (k) => {
5851
+ const top = at(r, c)[k] + (at(r, c + 1)[k] - at(r, c)[k]) * fx;
5852
+ const bot = at(r + 1, c)[k] + (at(r + 1, c + 1)[k] - at(r + 1, c)[k]) * fx;
5853
+ return top + (bot - top) * fy;
5854
+ };
5855
+ return [mix(0), mix(1)];
5856
+ }
5857
+ function targetField(t, v) {
5858
+ const side = Math.sign(v - t.rest);
5859
+ 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));
5860
+ const a = Math.abs(v - t.rest);
5861
+ return (x, y) => {
5862
+ if (knots.length === 0) return [0, 0];
5863
+ const at = (k) => latticeAt(t, t.stops.get(k), x, y);
5864
+ let lo = 0;
5865
+ let loVal = [0, 0];
5866
+ for (const k of knots) {
5867
+ const hi = Math.abs(k - t.rest);
5868
+ const hiVal = at(k);
5869
+ if (a <= hi + SAME_STOP) {
5870
+ const f = hi - lo > 0 ? Math.min(1, (a - lo) / (hi - lo)) : 1;
5871
+ return [
5872
+ loVal[0] + (hiVal[0] - loVal[0]) * f,
5873
+ loVal[1] + (hiVal[1] - loVal[1]) * f
5874
+ ];
5875
+ }
5876
+ lo = hi;
5877
+ loVal = hiVal;
5878
+ }
5879
+ return loVal;
5880
+ };
5881
+ }
5882
+ var apply = (m, x, y) => [
5883
+ m[0] * x + m[2] * y + m[4],
5884
+ m[1] * x + m[3] * y + m[5]
5885
+ ];
5886
+ function invert(m) {
5887
+ const det = m[0] * m[3] - m[1] * m[2];
5888
+ const a = m[3] / det;
5889
+ const b = -m[1] / det;
5890
+ const c = -m[2] / det;
5891
+ const d = m[0] / det;
5892
+ return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
5893
+ }
5894
+ function remesh(part, entry, choice, canvas) {
5895
+ const b = boxOfPixels(entry.layer.bbox, canvas.width, canvas.height);
5896
+ if ("cellPx" in choice) {
5897
+ part.mesh = gridMesh(
5898
+ Math.max(1, Math.round(bw(b) / choice.cellPx)),
5899
+ Math.max(1, Math.round(bh(b) / choice.cellPx))
5900
+ );
5901
+ return;
5902
+ }
5903
+ const xs = Array.from(
5904
+ { length: choice.cols + 1 },
5905
+ (_, c) => b.x0 + bw(b) * c / choice.cols
5906
+ );
5907
+ part.mesh = columnMesh(b, {
5908
+ xs,
5909
+ ys: choice.rows.map((y) => canvas.height / 2 - y)
5910
+ });
5911
+ }
5912
+ function applyBakes(parsed, parts, entries, ctx) {
5913
+ const { canvas, deformers: deformers2, parameters, faceWidth } = ctx;
5914
+ const partOf = (id) => parts.find((p) => p.id === id);
5915
+ const entryOf = (id) => entries.find((e) => e.id === id);
5916
+ for (const [id, choice] of parsed.meshes) {
5917
+ remesh(partOf(id), entryOf(id), choice, canvas);
5918
+ }
5919
+ const ids = [...new Set(parsed.targets.flatMap((t) => t.parts))];
5920
+ const reports = [];
5921
+ for (const id of ids) {
5922
+ const part = partOf(id);
5923
+ const n = part.mesh.vertices.length / 2;
5924
+ const own = parsed.targets.filter((t) => t.parts.includes(id));
5925
+ const w0 = matrixWorldAt(deformers2, parameters, part.deformer);
5926
+ const local = [];
5927
+ const restModel = [];
5928
+ const restCanvas = [];
5929
+ for (let i = 0; i < n; i++) {
5930
+ const p = [
5931
+ part.transform.x + part.mesh.vertices[i * 2] * part.width,
5932
+ part.transform.y + part.mesh.vertices[i * 2 + 1] * part.height
5933
+ ];
5934
+ const r = apply(w0, p[0], p[1]);
5935
+ local.push(p);
5936
+ restModel.push(r);
5937
+ restCanvas.push([r[0] + canvas.width / 2, canvas.height / 2 - r[1]]);
5938
+ }
5939
+ for (const param of [...new Set(own.map((t) => t.param))]) {
5940
+ const targets = own.filter((t) => t.param === param);
5941
+ const rest = targets[0].rest;
5942
+ const stops = [];
5943
+ for (const v of targets.flatMap((t) => [...t.stops.keys()]).sort((a, b) => a - b)) {
5944
+ if (stops.length === 0 || v - stops[stops.length - 1] > SAME_STOP) {
5945
+ stops.push(v);
5946
+ }
5947
+ }
5948
+ const range = parameters.find((p) => p.id === param);
5949
+ const ends = [];
5950
+ for (const [outer, end] of [
5951
+ [stops.filter((v) => v < rest).shift(), range.min],
5952
+ [stops.filter((v) => v > rest).pop(), range.max]
5953
+ ]) {
5954
+ if (outer !== void 0 && Math.abs(end - outer) > SAME_STOP) {
5955
+ ends.push(end);
5956
+ }
5957
+ }
5958
+ const keyforms = [{ value: rest, offsets: new Array(2 * n).fill(0) }];
5959
+ const wanted = [];
5960
+ const endWanted = [];
5961
+ for (const v of [...stops, ...ends]) {
5962
+ const disp = new Array(2 * n).fill(0);
5963
+ const fields2 = targets.map((t) => targetField(t, v));
5964
+ for (let i = 0; i < n; i++) {
5965
+ for (const field of fields2) {
5966
+ const [dx, dy] = field(restCanvas[i][0], restCanvas[i][1]);
5967
+ disp[i * 2] += dx;
5968
+ disp[i * 2 + 1] += dy;
5969
+ }
5970
+ }
5971
+ (ends.includes(v) ? endWanted : wanted).push(disp);
5972
+ if (entryOf(id).rigid === true) {
5973
+ const moved = restCanvas.map(
5974
+ (p, i) => [p[0] + disp[i * 2], p[1] + disp[i * 2 + 1]]
5975
+ );
5976
+ const bend = rigidResidual(restCanvas, moved);
5977
+ if (bend > RIGID_BEND * faceWidth) {
5978
+ const names = targets.map(
5979
+ (t) => `bakes.targets[${parsed.targets.indexOf(t)}] ("${t.id}")`
5980
+ ).join(", ");
5981
+ throw new BakeError(
5982
+ `${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`
5983
+ );
5984
+ }
5985
+ }
5986
+ const inv = invert(
5987
+ matrixWorldAt(deformers2, parameters, part.deformer, { [param]: v })
5988
+ );
5989
+ const offsets = [];
5990
+ for (let i = 0; i < n; i++) {
5991
+ const [px, py] = apply(
5992
+ inv,
5993
+ restModel[i][0] + disp[i * 2],
5994
+ restModel[i][1] - disp[i * 2 + 1]
5995
+ );
5996
+ offsets.push(
5997
+ roundTo((px - local[i][0]) / part.width, OFFSET_STEP),
5998
+ roundTo((py - local[i][1]) / part.height, OFFSET_STEP)
5999
+ );
6000
+ }
6001
+ keyforms.push({ value: v, offsets });
6002
+ }
6003
+ keyforms.sort((a, b) => a.value - b.value);
6004
+ part.warps = [...part.warps ?? [], { parameter: param, keyforms }];
6005
+ reports.push({
6006
+ part: id,
6007
+ param,
6008
+ stops,
6009
+ vertexCount: n,
6010
+ wanted,
6011
+ ends: ends.map((value, i) => ({ value, wanted: endWanted[i] }))
6012
+ });
6013
+ }
6014
+ }
6015
+ return reports;
6016
+ }
6017
+
5328
6018
  // src/auto-rig/generate.ts
5329
6019
  import {
5330
6020
  IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
@@ -5533,8 +6223,8 @@ function defaultMotions(declared) {
5533
6223
  }
5534
6224
 
5535
6225
  // src/auto-rig/checks.ts
5536
- var finite = (v) => typeof v === "number" && Number.isFinite(v);
5537
- var isObject = (v) => typeof v === "object" && v !== null;
6226
+ var finite2 = (v) => typeof v === "number" && Number.isFinite(v);
6227
+ var isObject2 = (v) => typeof v === "object" && v !== null;
5538
6228
  var MIN_FACE_PX = 24;
5539
6229
  function checkLayers(layers, canvas) {
5540
6230
  if (!Array.isArray(layers) || layers.length === 0) {
@@ -5542,10 +6232,10 @@ function checkLayers(layers, canvas) {
5542
6232
  "auto-rig: generateIkiFromLayerSet: layers must be a non-empty array"
5543
6233
  );
5544
6234
  }
5545
- if (!(finite(canvas?.width) && canvas.width > 0)) {
6235
+ if (!(finite2(canvas?.width) && canvas.width > 0)) {
5546
6236
  throw new Error("auto-rig: canvas.width must be a positive number");
5547
6237
  }
5548
- if (!(finite(canvas?.height) && canvas.height > 0)) {
6238
+ if (!(finite2(canvas?.height) && canvas.height > 0)) {
5549
6239
  throw new Error("auto-rig: canvas.height must be a positive number");
5550
6240
  }
5551
6241
  const seen = /* @__PURE__ */ new Set();
@@ -5557,7 +6247,7 @@ function checkLayers(layers, canvas) {
5557
6247
  seen.add(l.role);
5558
6248
  const at = `auto-rig: layer "${l.fileName}"`;
5559
6249
  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) {
6250
+ if (![x, y, w, h].every(finite2) || w <= 0 || h <= 0 || x < 0 || y < 0 || x + w > canvas.width || y + h > canvas.height) {
5561
6251
  throw new Error(
5562
6252
  `${at} has a bbox outside its ${canvas.width}x${canvas.height} canvas`
5563
6253
  );
@@ -5585,7 +6275,7 @@ function checkLayers(layers, canvas) {
5585
6275
  );
5586
6276
  }
5587
6277
  rows.forEach((v, i) => {
5588
- if (!finite(v) || v < 0 || v > w / 2 + 0.5) {
6278
+ if (!finite2(v) || v < 0 || v > w / 2 + 0.5) {
5589
6279
  throw new Error(
5590
6280
  `${at}: rowHalfWidths[${i}] ${v} is not a half-width inside its crop`
5591
6281
  );
@@ -5626,7 +6316,7 @@ function checkLayers(layers, canvas) {
5626
6316
  }
5627
6317
  if (l.denseCore !== void 0) {
5628
6318
  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) {
6319
+ 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
6320
  throw new Error(`${at}: denseCore is not a box inside its crop`);
5631
6321
  }
5632
6322
  }
@@ -5649,7 +6339,7 @@ function checkLayers(layers, canvas) {
5649
6339
  }
5650
6340
  function checkHeadEdges(edges, byRole) {
5651
6341
  if (edges === void 0) return;
5652
- if (!isObject(edges))
6342
+ if (!isObject2(edges))
5653
6343
  throw new TurnTargetError("headEdges must be an object");
5654
6344
  for (const side of ["left", "right"]) {
5655
6345
  const list = edges[side];
@@ -5657,12 +6347,12 @@ function checkHeadEdges(edges, byRole) {
5657
6347
  throw new TurnTargetError(`headEdges.${side} must be an array`);
5658
6348
  }
5659
6349
  list.forEach((e, i) => {
5660
- if (!isObject(e) || typeof e.role !== "string" || !byRole.has(e.role)) {
6350
+ if (!isObject2(e) || typeof e.role !== "string" || !byRole.has(e.role)) {
5661
6351
  throw new TurnTargetError(
5662
6352
  `headEdges.${side}[${i}].role is not a layer in this set`
5663
6353
  );
5664
6354
  }
5665
- if (!finite(e.x)) {
6355
+ if (!finite2(e.x)) {
5666
6356
  throw new TurnTargetError(
5667
6357
  `headEdges.${side}[${i}].x must be a finite number`
5668
6358
  );
@@ -5682,25 +6372,25 @@ function checkHeadEdges(edges, byRole) {
5682
6372
  }
5683
6373
  function checkStrandEdges(strand, irises, hair) {
5684
6374
  if (strand === void 0) return;
5685
- if (!isObject(strand)) {
6375
+ if (!isObject2(strand)) {
5686
6376
  throw new TurnTargetError("strandEdges must be an object");
5687
6377
  }
5688
6378
  for (const side of ["left", "right"]) {
5689
6379
  const s = strand[side];
5690
6380
  if (s === void 0) continue;
5691
6381
  const at = `strandEdges.${side}`;
5692
- if (!isObject(s)) throw new TurnTargetError(`${at} must be an object`);
6382
+ if (!isObject2(s)) throw new TurnTargetError(`${at} must be an object`);
5693
6383
  if (hair === void 0) {
5694
6384
  throw new TurnTargetError(
5695
6385
  `${at} names a bangs' run, but this layer set has no hair_front`
5696
6386
  );
5697
6387
  }
5698
6388
  for (const k of ["y", "irisOuter", "irisInner", "runOuter"]) {
5699
- if (!finite(s[k])) {
6389
+ if (!finite2(s[k])) {
5700
6390
  throw new TurnTargetError(`${at}.${k} must be a finite number`);
5701
6391
  }
5702
6392
  }
5703
- if (s.runFace !== null && !finite(s.runFace)) {
6393
+ if (s.runFace !== null && !finite2(s.runFace)) {
5704
6394
  throw new TurnTargetError(
5705
6395
  `${at}.runFace must be a finite number or null`
5706
6396
  );
@@ -6171,9 +6861,9 @@ function replay(op, i, ctx, sums) {
6171
6861
  const x = span2(`${name}.region.x`, region.x);
6172
6862
  const y = span2(`${name}.region.y`, region.y);
6173
6863
  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);
6864
+ const dx = finite3(`${name}.by.dx`, by.dx);
6865
+ const dy = finite3(`${name}.by.dy`, by.dy);
6866
+ const falloff = o.falloff === void 0 ? 0 : finite3(`${name}.falloff`, o.falloff, 0);
6177
6867
  if (o.why !== void 0) text(`${name}.why`, o.why);
6178
6868
  const box = ctx.boxOf(role);
6179
6869
  if (box === void 0) {
@@ -6341,10 +7031,10 @@ function fields(name, v, known) {
6341
7031
  function numbers(name, v) {
6342
7032
  const o = fields(name, v, Object.keys(v ?? {}));
6343
7033
  return Object.fromEntries(
6344
- Object.entries(o).map(([k, n]) => [k, finite2(`${name}.${k}`, n)])
7034
+ Object.entries(o).map(([k, n]) => [k, finite3(`${name}.${k}`, n)])
6345
7035
  );
6346
7036
  }
6347
- var finite2 = (name, v, lo = -Infinity) => asPatch(() => checkedNumber(name, v, lo, Infinity));
7037
+ var finite3 = (name, v, lo = -Infinity) => asPatch(() => checkedNumber(name, v, lo, Infinity));
6348
7038
  function text(name, v) {
6349
7039
  if (typeof v !== "string") {
6350
7040
  throw new PatchError(`${name} must be a string, got ${String(v)}`);
@@ -6357,8 +7047,8 @@ function span2(name, v) {
6357
7047
  `${name} must be [from, to], got ${JSON.stringify(v)}`
6358
7048
  );
6359
7049
  }
6360
- const lo = finite2(`${name}[0]`, v[0]);
6361
- const hi = finite2(`${name}[1]`, v[1]);
7050
+ const lo = finite3(`${name}[0]`, v[0]);
7051
+ const hi = finite3(`${name}[1]`, v[1]);
6362
7052
  if (lo > hi) {
6363
7053
  throw new PatchError(
6364
7054
  `${name} [${lo}, ${hi}] runs backwards: ${name}[0] must not exceed ${name}[1]`
@@ -6367,6 +7057,112 @@ function span2(name, v) {
6367
7057
  return [lo, hi];
6368
7058
  }
6369
7059
 
7060
+ // src/auto-rig/custom-parts.ts
7061
+ var KEYS = ["id", "layer", "anchor", "tie", "after", "deformer", "rigid"];
7062
+ var LAYER_KEYS = ["fileName", "bbox"];
7063
+ var finite4 = (v) => typeof v === "number" && Number.isFinite(v);
7064
+ var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7065
+ var inside = (b, [x, y]) => x >= b.x && x < b.x + b.w && y >= b.y && y < b.y + b.h;
7066
+ var RESERVED = new Set(
7067
+ ROLE_TABLE.flatMap((r) => [r.role, ...partIdsOfRole(r.role)])
7068
+ );
7069
+ function checkCustomParts(customParts, byRole, canvas) {
7070
+ if (!Array.isArray(customParts)) {
7071
+ throw new CustomPartError(
7072
+ `customParts must be an array, got ${customParts === null ? "null" : typeof customParts}`
7073
+ );
7074
+ }
7075
+ const present = [...byRole.keys()];
7076
+ const roleField = (at, v) => {
7077
+ const layer = typeof v === "string" ? byRole.get(v) : void 0;
7078
+ if (layer === void 0) {
7079
+ throw new CustomPartError(
7080
+ `${at} must be a role of the set, one of ${present.join(", ")}; got ${JSON.stringify(v)}`
7081
+ );
7082
+ }
7083
+ return layer;
7084
+ };
7085
+ const ids = /* @__PURE__ */ new Set();
7086
+ customParts.forEach((entry, i) => {
7087
+ const at = `customParts[${i}]`;
7088
+ if (!isObject3(entry)) {
7089
+ throw new CustomPartError(`${at} must be a plain object`);
7090
+ }
7091
+ checkedKeys(at, entry, KEYS, CustomPartError);
7092
+ const { id, layer, anchor, tie, after, deformer, rigid } = entry;
7093
+ if (typeof id !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(id)) {
7094
+ throw new CustomPartError(
7095
+ `${at}.id must be a plain identifier (letters, digits, underscore; not starting with a digit), got ${JSON.stringify(id)}`
7096
+ );
7097
+ }
7098
+ if (RESERVED.has(id)) {
7099
+ throw new CustomPartError(
7100
+ `${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`
7101
+ );
7102
+ }
7103
+ if (ids.has(id)) {
7104
+ throw new CustomPartError(
7105
+ `${at}.id "${id}" repeats an earlier entry's id`
7106
+ );
7107
+ }
7108
+ ids.add(id);
7109
+ if (!isObject3(layer)) {
7110
+ throw new CustomPartError(`${at}.layer must be a plain object`);
7111
+ }
7112
+ checkedKeys(`${at}.layer`, layer, LAYER_KEYS, CustomPartError);
7113
+ if (typeof layer.fileName !== "string" || layer.fileName === "") {
7114
+ throw new CustomPartError(
7115
+ `${at}.layer.fileName must be a non-empty string`
7116
+ );
7117
+ }
7118
+ const b = isObject3(layer.bbox) ? layer.bbox : {};
7119
+ const { x, y, w, h } = b;
7120
+ 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) {
7121
+ throw new CustomPartError(
7122
+ `${at}.layer.bbox must be a finite, non-empty box inside the ${canvas.width}x${canvas.height} canvas, got ${JSON.stringify(layer.bbox)}`
7123
+ );
7124
+ }
7125
+ const box = { x, y, w, h };
7126
+ const anchorLayer = roleField(`${at}.anchor`, anchor);
7127
+ roleField(`${at}.after`, after);
7128
+ if (!Array.isArray(tie) || tie.length !== 2 || !tie.every(finite4) || !inside(box, tie) || !inside(anchorLayer.bbox, tie)) {
7129
+ throw new CustomPartError(
7130
+ `${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)}`
7131
+ );
7132
+ }
7133
+ if (deformer !== void 0 && deformer !== "headDeformer") {
7134
+ throw new CustomPartError(
7135
+ `${at}.deformer "${String(deformer)}" is not accepted: headDeformer is the only deformer a custom part hangs on`
7136
+ );
7137
+ }
7138
+ if (rigid !== void 0 && typeof rigid !== "boolean") {
7139
+ throw new CustomPartError(
7140
+ `${at}.rigid must be true or false, got ${JSON.stringify(rigid)}`
7141
+ );
7142
+ }
7143
+ });
7144
+ return customParts;
7145
+ }
7146
+ var CELL_PX2 = 20;
7147
+ function buildCustomParts(customParts, canvas) {
7148
+ return customParts.map(({ id, layer }) => {
7149
+ const b = boxOfPixels(layer.bbox, canvas.width, canvas.height);
7150
+ return {
7151
+ id,
7152
+ color: [1, 1, 1, 1],
7153
+ width: bw(b),
7154
+ height: bh(b),
7155
+ transform: { x: cx(b), y: cy(b) },
7156
+ order: 0,
7157
+ deformer: "headDeformer",
7158
+ mesh: gridMesh(
7159
+ Math.max(1, Math.round(bw(b) / CELL_PX2)),
7160
+ Math.max(1, Math.round(bh(b) / CELL_PX2))
7161
+ )
7162
+ };
7163
+ });
7164
+ }
7165
+
6370
7166
  // src/auto-rig/generate.ts
6371
7167
  var MESH_CELLS = {
6372
7168
  face: { px: 28, min: 4, max: 24 },
@@ -6408,6 +7204,9 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
6408
7204
  const hasNose = has("nose");
6409
7205
  const plateHalf = byRole.get("face").cropW / 2;
6410
7206
  checkHeadEdges(options.headEdges, byRole);
7207
+ const entries = options.customParts !== void 0 && !(Array.isArray(options.customParts) && options.customParts.length === 0) ? checkCustomParts(options.customParts, byRole, canvas) : [];
7208
+ const parameters = declareParameters(new Set(byRole.keys()));
7209
+ const bakes = options.bakes === void 0 ? void 0 : parseBakes(options.bakes, entries, parameters);
6411
7210
  checkStrandEdges(
6412
7211
  options.strandEdges,
6413
7212
  sideIrises(byRole),
@@ -6557,7 +7356,6 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
6557
7356
  ...part.warps ?? []
6558
7357
  ];
6559
7358
  }
6560
- const parameters = declareParameters(new Set(byRole.keys()));
6561
7359
  const reports = options.patches === void 0 ? void 0 : applyPatches(options.patches, {
6562
7360
  grids: new Map(
6563
7361
  [...grids].map(([family, g]) => [WARP_ID[family], g])
@@ -6567,19 +7365,50 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
6567
7365
  faceWidth: byRole.get("face").cropW,
6568
7366
  parameters
6569
7367
  });
7368
+ const deformerList = deformers(
7369
+ frame,
7370
+ grids,
7371
+ bodyWarp?.deformer,
7372
+ arms,
7373
+ byRole
7374
+ );
7375
+ let bakeReports;
7376
+ if (entries.length > 0) {
7377
+ const built = buildCustomParts(entries, canvas);
7378
+ entries.forEach((entry, i) => {
7379
+ const group = new Set(partIdsOfRole(entry.after));
7380
+ for (let j = 0; j < i; j++) {
7381
+ if (entries[j].after === entry.after) group.add(entries[j].id);
7382
+ }
7383
+ const last = parts.map((p) => group.has(p.id)).lastIndexOf(true);
7384
+ parts.splice(last + 1, 0, built[i]);
7385
+ });
7386
+ parts.forEach((p, i) => {
7387
+ p.order = i;
7388
+ });
7389
+ if (bakes !== void 0) {
7390
+ bakeReports = applyBakes(bakes, built, entries, {
7391
+ canvas,
7392
+ deformers: deformerList,
7393
+ parameters,
7394
+ faceWidth: byRole.get("face").cropW
7395
+ });
7396
+ }
7397
+ }
6570
7398
  const model = {
6571
7399
  version: IKI_FORMAT_VERSION2,
6572
7400
  name: "auto-rigged",
6573
7401
  canvas: { width: canvas.width, height: canvas.height },
6574
7402
  parameters,
6575
7403
  parts,
6576
- deformers: deformers(frame, grids, bodyWarp?.deformer, arms, byRole),
7404
+ deformers: deformerList,
6577
7405
  ...has("hair_front") ? { physics: hairPhysics(has("hair_back")) } : {},
6578
7406
  expressions: defaultExpressions(new Set(parameters.map((p) => p.id))),
6579
7407
  motions: defaultMotions(new Set(parameters.map((p) => p.id)))
6580
7408
  };
6581
7409
  const valid = parseIkiModel3(model);
6582
7410
  if (reports !== void 0) options.onPatchesApplied?.(reports);
7411
+ if (bakeReports !== void 0) options.onBakesApplied?.(bakeReports);
6583
7412
  options.onRigPlanResolved?.(
6584
7413
  rigPlanReport(options.rigPlan ?? {}, plan, targets, fit)
6585
7414
  );
@@ -7061,7 +7890,9 @@ export {
7061
7890
  AddDeformer,
7062
7891
  AddPart,
7063
7892
  AddPhysicsRig,
7893
+ BakeError,
7064
7894
  CaptureGridKeyform,
7895
+ CustomPartError,
7065
7896
  DEFAULT_TURN_TARGETS,
7066
7897
  DeleteDeformer,
7067
7898
  DeletePart,
@@ -7117,6 +7948,7 @@ export {
7117
7948
  packAtlas,
7118
7949
  parseLayerRoles,
7119
7950
  partIdsOfRole,
7951
+ rigidResidual,
7120
7952
  styleRemovedMessage,
7121
7953
  upsertGridKeyform,
7122
7954
  uvRectFor,