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