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