@ikijs/editor 0.21.0 → 0.22.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 +13 -13
- package/dist/index.d.mts +122 -1
- package/dist/index.d.ts +122 -1
- package/dist/index.js +266 -198
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +263 -200
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3018,6 +3018,11 @@ function forearmPoseDeformer(role, arm) {
|
|
|
3018
3018
|
};
|
|
3019
3019
|
}
|
|
3020
3020
|
|
|
3021
|
+
// src/auto-rig/mouth.ts
|
|
3022
|
+
import {
|
|
3023
|
+
StandardParameter as P5
|
|
3024
|
+
} from "@ikijs/format";
|
|
3025
|
+
|
|
3021
3026
|
// src/auto-rig/roles.ts
|
|
3022
3027
|
var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
|
|
3023
3028
|
var isLipRole = (role) => LIP_ROLES.includes(role);
|
|
@@ -3159,6 +3164,234 @@ function parseLayerRoles(fileNames) {
|
|
|
3159
3164
|
return out;
|
|
3160
3165
|
}
|
|
3161
3166
|
|
|
3167
|
+
// src/auto-rig/mouth.ts
|
|
3168
|
+
var UPPER_SHARE = 0.4;
|
|
3169
|
+
var MOUTH_KNOT_PX = 4;
|
|
3170
|
+
var isLipSet = (byRole) => LIP_ROLES.every((r) => byRole.has(r));
|
|
3171
|
+
function columnRuns(layer) {
|
|
3172
|
+
const cols = /* @__PURE__ */ new Map();
|
|
3173
|
+
(layer.rowRuns ?? []).forEach((runs, k) => {
|
|
3174
|
+
for (let i = 0; i < runs.length; i += 2) {
|
|
3175
|
+
for (let c = runs[i]; c < runs[i + 1]; c++) {
|
|
3176
|
+
const list = cols.get(c) ?? [];
|
|
3177
|
+
const last = list[list.length - 1];
|
|
3178
|
+
if (last !== void 0 && last[1] === k) last[1] = k + 1;
|
|
3179
|
+
else list.push([k, k + 1]);
|
|
3180
|
+
cols.set(c, list);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
});
|
|
3184
|
+
return cols;
|
|
3185
|
+
}
|
|
3186
|
+
var edgesOf = (layer, run) => ({
|
|
3187
|
+
top: layer.canvasH / 2 - (layer.bbox.y + run[0]),
|
|
3188
|
+
bottom: layer.canvasH / 2 - (layer.bbox.y + run[1])
|
|
3189
|
+
});
|
|
3190
|
+
function mouthOpening(byRole) {
|
|
3191
|
+
const inner = byRole.get("mouth_inner");
|
|
3192
|
+
const upper = byRole.get("lip_upper");
|
|
3193
|
+
const lower = byRole.get("lip_lower");
|
|
3194
|
+
const innerCols = columnRuns(inner);
|
|
3195
|
+
if (innerCols.size === 0) {
|
|
3196
|
+
throw new LayerGeometryError(
|
|
3197
|
+
`auto-rig: layer "${inner.fileName}": mouth_inner has no opaque pixel (alpha \u2265 128) to read the opening off`
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
3200
|
+
const upperCols = columnRuns(upper);
|
|
3201
|
+
const lowerCols = columnRuns(lower);
|
|
3202
|
+
const cols = [...innerCols.keys()];
|
|
3203
|
+
const x0 = Math.min(...cols);
|
|
3204
|
+
const x1 = Math.max(...cols);
|
|
3205
|
+
const read = (col) => {
|
|
3206
|
+
const runs = innerCols.get(col);
|
|
3207
|
+
if (runs === void 0) return void 0;
|
|
3208
|
+
const T = edgesOf(inner, runs[0]).top;
|
|
3209
|
+
const Bb = edgesOf(inner, runs[runs.length - 1]).bottom;
|
|
3210
|
+
const line = [...upperCols.get(col) ?? []].reverse().map((r) => edgesOf(upper, r)).find((e) => e.top >= T);
|
|
3211
|
+
const Tu = line?.bottom ?? T;
|
|
3212
|
+
const lineTop = line?.top ?? T;
|
|
3213
|
+
const skin = lowerCols.get(col)?.[0];
|
|
3214
|
+
return {
|
|
3215
|
+
T,
|
|
3216
|
+
Bb,
|
|
3217
|
+
Tu,
|
|
3218
|
+
lineTop,
|
|
3219
|
+
lineH: lineTop - Tu,
|
|
3220
|
+
Bl: skin === void 0 ? Bb : edgesOf(lower, skin).top,
|
|
3221
|
+
H: T - Bb
|
|
3222
|
+
};
|
|
3223
|
+
};
|
|
3224
|
+
const reads = [];
|
|
3225
|
+
for (let c = x0; c <= x1; c++) {
|
|
3226
|
+
let r;
|
|
3227
|
+
for (let d = 0; (r = read(c - d) ?? read(c + d)) === void 0; d++) ;
|
|
3228
|
+
reads.push(r);
|
|
3229
|
+
}
|
|
3230
|
+
const m = (x0 + x1) / 2;
|
|
3231
|
+
let p = 0;
|
|
3232
|
+
let q = 0;
|
|
3233
|
+
if (reads.length >= 4) {
|
|
3234
|
+
let s11 = 0;
|
|
3235
|
+
let s12 = 0;
|
|
3236
|
+
let s22 = 0;
|
|
3237
|
+
let t1 = 0;
|
|
3238
|
+
let t2 = 0;
|
|
3239
|
+
reads.forEach((r, i) => {
|
|
3240
|
+
const x = x0 + i;
|
|
3241
|
+
const b1 = (x - x0) * (x - x1);
|
|
3242
|
+
const b2 = b1 * (x - m);
|
|
3243
|
+
const d = r.T - UPPER_SHARE * r.H - r.Tu;
|
|
3244
|
+
s11 += b1 * b1;
|
|
3245
|
+
s12 += b1 * b2;
|
|
3246
|
+
s22 += b2 * b2;
|
|
3247
|
+
t1 += b1 * d;
|
|
3248
|
+
t2 += b2 * d;
|
|
3249
|
+
});
|
|
3250
|
+
const det = s11 * s22 - s12 * s12;
|
|
3251
|
+
p = (t1 * s22 - t2 * s12) / det;
|
|
3252
|
+
q = (s11 * t2 - s12 * t1) / det;
|
|
3253
|
+
}
|
|
3254
|
+
const columns = reads.map((r, i) => {
|
|
3255
|
+
const x = x0 + i;
|
|
3256
|
+
return { ...r, seam: r.Tu + (x - x0) * (x - x1) * (p + q * (x - m)) };
|
|
3257
|
+
});
|
|
3258
|
+
const base = (col) => columns[Math.min(x1, Math.max(x0, col)) - x0];
|
|
3259
|
+
const centre = Math.round((x0 + x1) / 2);
|
|
3260
|
+
const w = base(centre).lineH;
|
|
3261
|
+
return {
|
|
3262
|
+
x0,
|
|
3263
|
+
x1,
|
|
3264
|
+
centre,
|
|
3265
|
+
canvasW: inner.canvasW,
|
|
3266
|
+
w,
|
|
3267
|
+
at(col) {
|
|
3268
|
+
const b = base(col);
|
|
3269
|
+
return { ...b, overlap: Math.max(0, Math.min(w, b.lineH)) };
|
|
3270
|
+
}
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
function mouthFold(role, opening) {
|
|
3274
|
+
return (col, y, v) => {
|
|
3275
|
+
if (v === 1) return 0;
|
|
3276
|
+
if (role !== "mouth_inner" && (col < opening.x0 || col > opening.x1))
|
|
3277
|
+
return 0;
|
|
3278
|
+
const c = opening.at(col);
|
|
3279
|
+
const k = 1 - v;
|
|
3280
|
+
switch (role) {
|
|
3281
|
+
case "lip_upper":
|
|
3282
|
+
return (c.seam - c.Tu) * k;
|
|
3283
|
+
case "lip_lower":
|
|
3284
|
+
return (c.seam + c.overlap - c.Bl) * k;
|
|
3285
|
+
case "mouth_inner":
|
|
3286
|
+
return (c.seam + c.overlap - y) * k;
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
function columnOfKnot(knot, opening, role) {
|
|
3291
|
+
const boundary = Math.round(knot + opening.canvasW / 2);
|
|
3292
|
+
const col = boundary === opening.x1 + 1 ? opening.x1 : boundary;
|
|
3293
|
+
if (col >= opening.x0 && col <= opening.x1) return col;
|
|
3294
|
+
return role === "mouth_inner" ? Math.min(opening.x1, Math.max(opening.x0, col)) : void 0;
|
|
3295
|
+
}
|
|
3296
|
+
function mouthFoldWarp(role, mesh, xs, box, opening) {
|
|
3297
|
+
const fold = mouthFold(role, opening);
|
|
3298
|
+
return localWarp(P5.MouthOpen, mesh, box, [0, 1], (p, v, i) => {
|
|
3299
|
+
const col = columnOfKnot(xs[i % xs.length], opening, role);
|
|
3300
|
+
return [0, col === void 0 ? 0 : fold(col, p[1], v)];
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
function mouthRestShift(byRole) {
|
|
3304
|
+
const rig = buildMouthRig(byRole);
|
|
3305
|
+
const fields = /* @__PURE__ */ new Map();
|
|
3306
|
+
for (const role of ["lip_upper", "lip_lower"]) {
|
|
3307
|
+
const box = boxOfLayer(byRole.get(role));
|
|
3308
|
+
const { mesh, xs } = mouthMesh(box, rig.knots);
|
|
3309
|
+
const warp = mouthFoldWarp(role, mesh, xs, box, rig.opening);
|
|
3310
|
+
const offsets = warp.keyforms[0].offsets;
|
|
3311
|
+
fields.set(role, {
|
|
3312
|
+
box,
|
|
3313
|
+
xs,
|
|
3314
|
+
dy: xs.map((_, i) => offsets[2 * i + 1] * bh(box))
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
const half = rig.opening.canvasW / 2;
|
|
3318
|
+
return (role, col) => {
|
|
3319
|
+
const { box, xs, dy } = fields.get(role);
|
|
3320
|
+
const x = col + 0.5 - half;
|
|
3321
|
+
if (x < box.x0 || x > box.x1) return 0;
|
|
3322
|
+
let k = 1;
|
|
3323
|
+
while (k < xs.length - 1 && xs[k] < x) k++;
|
|
3324
|
+
const t = (x - xs[k - 1]) / (xs[k] - xs[k - 1]);
|
|
3325
|
+
return dy[k - 1] + (dy[k] - dy[k - 1]) * t;
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
function mouthKnots(union, opening) {
|
|
3329
|
+
const half = opening.canvasW / 2;
|
|
3330
|
+
const step = MOUTH_KNOT_PX;
|
|
3331
|
+
const xs = [union.x0];
|
|
3332
|
+
while (xs[xs.length - 1] + step < union.x1 - 0.3 * step)
|
|
3333
|
+
xs.push(xs[xs.length - 1] + step);
|
|
3334
|
+
xs.push(union.x1);
|
|
3335
|
+
const fixed = /* @__PURE__ */ new Set([union.x0, union.x1]);
|
|
3336
|
+
for (const x of [
|
|
3337
|
+
opening.x0 - half,
|
|
3338
|
+
opening.x1 - half,
|
|
3339
|
+
opening.x1 + 1 - half
|
|
3340
|
+
]) {
|
|
3341
|
+
fixed.add(x);
|
|
3342
|
+
if (xs.includes(x)) continue;
|
|
3343
|
+
const near = xs.findIndex((v) => Math.abs(v - x) < 0.3 * step);
|
|
3344
|
+
if (near >= 0 && !fixed.has(xs[near])) xs[near] = x;
|
|
3345
|
+
else xs.push(x);
|
|
3346
|
+
}
|
|
3347
|
+
xs.sort((p, q) => p - q);
|
|
3348
|
+
for (const x of xs) {
|
|
3349
|
+
if (!Number.isInteger(x + half)) {
|
|
3350
|
+
throw new Error(
|
|
3351
|
+
`auto-rig: mouth knot ${x} is not an integer canvas boundary`
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
return xs;
|
|
3356
|
+
}
|
|
3357
|
+
function mouthMesh(box, knots) {
|
|
3358
|
+
const xs = [box.x0, ...knots.filter((k) => k > box.x0 && k < box.x1), box.x1];
|
|
3359
|
+
const rows = cellsFor(bh(box), MOUTH_KNOT_PX, 2, 40);
|
|
3360
|
+
const ys = Array.from(
|
|
3361
|
+
{ length: rows + 1 },
|
|
3362
|
+
(_, r) => box.y1 - bh(box) * r / rows
|
|
3363
|
+
);
|
|
3364
|
+
return { mesh: columnMesh(box, { xs, ys }), xs };
|
|
3365
|
+
}
|
|
3366
|
+
function buildMouthRig(byRole) {
|
|
3367
|
+
const opening = mouthOpening(byRole);
|
|
3368
|
+
const union = unionBoxes(LIP_ROLES.map((r) => boxOfLayer(byRole.get(r))));
|
|
3369
|
+
return {
|
|
3370
|
+
opening,
|
|
3371
|
+
frame: mouthFrameOf(union),
|
|
3372
|
+
knots: mouthKnots(union, opening),
|
|
3373
|
+
union
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
function mouthAnchor(byRole) {
|
|
3377
|
+
const box = (role) => boxOfLayer(byRole.get(role));
|
|
3378
|
+
if (isLipSet(byRole)) {
|
|
3379
|
+
const opening = mouthOpening(byRole);
|
|
3380
|
+
return {
|
|
3381
|
+
at: {
|
|
3382
|
+
x: cx(box("mouth_inner")),
|
|
3383
|
+
y: opening.at(opening.centre).seam
|
|
3384
|
+
},
|
|
3385
|
+
box: unionBoxes(LIP_ROLES.map(box))
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
const m = box("mouth");
|
|
3389
|
+
return {
|
|
3390
|
+
at: { x: cx(m), y: cy(m) },
|
|
3391
|
+
box: byRole.has("mouth_open") ? unionBoxes([m, box("mouth_open")]) : m
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3162
3395
|
// src/auto-rig/generate.ts
|
|
3163
3396
|
import {
|
|
3164
3397
|
IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
|
|
@@ -3168,30 +3401,30 @@ import {
|
|
|
3168
3401
|
|
|
3169
3402
|
// src/auto-rig/animations.ts
|
|
3170
3403
|
import {
|
|
3171
|
-
StandardParameter as
|
|
3404
|
+
StandardParameter as P6
|
|
3172
3405
|
} from "@ikijs/format";
|
|
3173
3406
|
function browsY(y) {
|
|
3174
3407
|
return [
|
|
3175
|
-
{ parameter:
|
|
3176
|
-
{ parameter:
|
|
3408
|
+
{ parameter: P6.BrowLeftY, value: y, blend: "add" },
|
|
3409
|
+
{ parameter: P6.BrowRightY, value: y, blend: "add" }
|
|
3177
3410
|
];
|
|
3178
3411
|
}
|
|
3179
3412
|
function browsTilt(innerDown) {
|
|
3180
3413
|
return [
|
|
3181
|
-
{ parameter:
|
|
3182
|
-
{ parameter:
|
|
3414
|
+
{ parameter: P6.BrowLeftAngle, value: innerDown, blend: "add" },
|
|
3415
|
+
{ parameter: P6.BrowRightAngle, value: -innerDown, blend: "add" }
|
|
3183
3416
|
];
|
|
3184
3417
|
}
|
|
3185
3418
|
function cheek(value) {
|
|
3186
|
-
return { parameter:
|
|
3419
|
+
return { parameter: P6.Cheek, value, blend: "add" };
|
|
3187
3420
|
}
|
|
3188
|
-
var BROWS = [
|
|
3421
|
+
var BROWS = [P6.BrowLeftY, P6.BrowRightY, P6.BrowLeftAngle, P6.BrowRightAngle];
|
|
3189
3422
|
var EXPRESSIONS = [
|
|
3190
3423
|
{
|
|
3191
3424
|
id: "smile",
|
|
3192
3425
|
description: "Smiling and pleased: warm, friendly, content. For greetings, thanks, agreement and gentle happiness.",
|
|
3193
3426
|
parameters: [
|
|
3194
|
-
{ parameter:
|
|
3427
|
+
{ parameter: P6.MouthForm, value: 1, blend: "add" },
|
|
3195
3428
|
...browsY(0.25),
|
|
3196
3429
|
cheek(0.4)
|
|
3197
3430
|
]
|
|
@@ -3200,10 +3433,10 @@ var EXPRESSIONS = [
|
|
|
3200
3433
|
id: "laugh",
|
|
3201
3434
|
description: "Laughing, eyes shut and mouth open: delighted, amused. For jokes, playfulness and big joy.",
|
|
3202
3435
|
parameters: [
|
|
3203
|
-
{ parameter:
|
|
3204
|
-
{ parameter:
|
|
3205
|
-
{ parameter:
|
|
3206
|
-
{ parameter:
|
|
3436
|
+
{ parameter: P6.EyeOpenLeft, value: 0, blend: "multiply" },
|
|
3437
|
+
{ parameter: P6.EyeOpenRight, value: 0, blend: "multiply" },
|
|
3438
|
+
{ parameter: P6.MouthForm, value: 1, blend: "add" },
|
|
3439
|
+
{ parameter: P6.MouthOpen, value: 0.8, blend: "overwrite" },
|
|
3207
3440
|
...browsY(0.4),
|
|
3208
3441
|
cheek(0.65)
|
|
3209
3442
|
]
|
|
@@ -3215,7 +3448,7 @@ var EXPRESSIONS = [
|
|
|
3215
3448
|
parameters: [
|
|
3216
3449
|
...browsY(-0.5),
|
|
3217
3450
|
...browsTilt(0.8),
|
|
3218
|
-
{ parameter:
|
|
3451
|
+
{ parameter: P6.MouthForm, value: -0.9, blend: "add" }
|
|
3219
3452
|
]
|
|
3220
3453
|
},
|
|
3221
3454
|
{
|
|
@@ -3225,7 +3458,7 @@ var EXPRESSIONS = [
|
|
|
3225
3458
|
parameters: [
|
|
3226
3459
|
...browsY(0.35),
|
|
3227
3460
|
...browsTilt(-0.8),
|
|
3228
|
-
{ parameter:
|
|
3461
|
+
{ parameter: P6.MouthForm, value: -0.8, blend: "add" }
|
|
3229
3462
|
]
|
|
3230
3463
|
},
|
|
3231
3464
|
{
|
|
@@ -3233,18 +3466,18 @@ var EXPRESSIONS = [
|
|
|
3233
3466
|
description: "Surprised, mouth dropped open: startled, amazed. For shock, sudden news or disbelief.",
|
|
3234
3467
|
parameters: [
|
|
3235
3468
|
...browsY(0.8),
|
|
3236
|
-
{ parameter:
|
|
3469
|
+
{ parameter: P6.MouthOpen, value: 0.9, blend: "overwrite" }
|
|
3237
3470
|
]
|
|
3238
3471
|
},
|
|
3239
3472
|
{
|
|
3240
3473
|
id: "shy",
|
|
3241
3474
|
description: "Shy or embarrassed: bashful, flustered. For being praised, teased or caught off guard.",
|
|
3242
|
-
needs: [
|
|
3475
|
+
needs: [P6.Cheek],
|
|
3243
3476
|
parameters: [
|
|
3244
3477
|
cheek(1),
|
|
3245
3478
|
// Gaze down.
|
|
3246
|
-
{ parameter:
|
|
3247
|
-
{ parameter:
|
|
3479
|
+
{ parameter: P6.EyeballY, value: -0.65, blend: "add" },
|
|
3480
|
+
{ parameter: P6.MouthForm, value: 0.4, blend: "add" },
|
|
3248
3481
|
...browsTilt(-0.25)
|
|
3249
3482
|
]
|
|
3250
3483
|
}
|
|
@@ -3264,7 +3497,7 @@ var HEAD_MOTIONS = {
|
|
|
3264
3497
|
duration: 1,
|
|
3265
3498
|
curves: [
|
|
3266
3499
|
{
|
|
3267
|
-
parameter:
|
|
3500
|
+
parameter: P6.AngleY,
|
|
3268
3501
|
keys: [
|
|
3269
3502
|
[0, 0],
|
|
3270
3503
|
[0.3, -18],
|
|
@@ -3285,7 +3518,7 @@ var HEAD_MOTIONS = {
|
|
|
3285
3518
|
fadeIn: 0.15,
|
|
3286
3519
|
curves: [
|
|
3287
3520
|
{
|
|
3288
|
-
parameter:
|
|
3521
|
+
parameter: P6.AngleX,
|
|
3289
3522
|
keys: [
|
|
3290
3523
|
[0, 0],
|
|
3291
3524
|
[0.2, -18],
|
|
@@ -3304,7 +3537,7 @@ var HEAD_MOTIONS = {
|
|
|
3304
3537
|
duration: 1.4,
|
|
3305
3538
|
curves: [
|
|
3306
3539
|
{
|
|
3307
|
-
parameter:
|
|
3540
|
+
parameter: P6.AngleZ,
|
|
3308
3541
|
keys: [
|
|
3309
3542
|
[0, 0],
|
|
3310
3543
|
[0.35, 20],
|
|
@@ -3325,7 +3558,7 @@ var WAVE = {
|
|
|
3325
3558
|
fadeOut: 0.15,
|
|
3326
3559
|
curves: [
|
|
3327
3560
|
{
|
|
3328
|
-
parameter:
|
|
3561
|
+
parameter: P6.ArmPoseRight,
|
|
3329
3562
|
interpolation: "linear",
|
|
3330
3563
|
keys: [
|
|
3331
3564
|
[0, 0],
|
|
@@ -3335,7 +3568,7 @@ var WAVE = {
|
|
|
3335
3568
|
]
|
|
3336
3569
|
},
|
|
3337
3570
|
{
|
|
3338
|
-
parameter:
|
|
3571
|
+
parameter: P6.ArmRight,
|
|
3339
3572
|
keys: [
|
|
3340
3573
|
[0, 0],
|
|
3341
3574
|
[0.4, 10],
|
|
@@ -3344,7 +3577,7 @@ var WAVE = {
|
|
|
3344
3577
|
]
|
|
3345
3578
|
},
|
|
3346
3579
|
{
|
|
3347
|
-
parameter:
|
|
3580
|
+
parameter: P6.ArmPoseAngleRight,
|
|
3348
3581
|
keys: [
|
|
3349
3582
|
[0, 0],
|
|
3350
3583
|
[0.5, 0],
|
|
@@ -3360,7 +3593,7 @@ var WAVE = {
|
|
|
3360
3593
|
]
|
|
3361
3594
|
};
|
|
3362
3595
|
function defaultMotions(declared) {
|
|
3363
|
-
const waves = [
|
|
3596
|
+
const waves = [P6.ArmRight, P6.ArmPoseRight, P6.ArmPoseAngleRight].every(
|
|
3364
3597
|
(id) => declared.has(id)
|
|
3365
3598
|
);
|
|
3366
3599
|
return waves ? { ...HEAD_MOTIONS, ...WAVE } : HEAD_MOTIONS;
|
|
@@ -3598,181 +3831,6 @@ function checkStrandEdges(strand, irises, hair) {
|
|
|
3598
3831
|
// src/auto-rig/context.ts
|
|
3599
3832
|
import { StandardParameter as P7 } from "@ikijs/format";
|
|
3600
3833
|
|
|
3601
|
-
// src/auto-rig/mouth.ts
|
|
3602
|
-
import {
|
|
3603
|
-
StandardParameter as P6
|
|
3604
|
-
} from "@ikijs/format";
|
|
3605
|
-
var UPPER_SHARE = 0.3;
|
|
3606
|
-
var MOUTH_KNOT_PX = 4;
|
|
3607
|
-
var isLipSet = (byRole) => LIP_ROLES.every((r) => byRole.has(r));
|
|
3608
|
-
function columnRuns(layer) {
|
|
3609
|
-
const cols = /* @__PURE__ */ new Map();
|
|
3610
|
-
(layer.rowRuns ?? []).forEach((runs, k) => {
|
|
3611
|
-
for (let i = 0; i < runs.length; i += 2) {
|
|
3612
|
-
for (let c = runs[i]; c < runs[i + 1]; c++) {
|
|
3613
|
-
const list = cols.get(c) ?? [];
|
|
3614
|
-
const last = list[list.length - 1];
|
|
3615
|
-
if (last !== void 0 && last[1] === k) last[1] = k + 1;
|
|
3616
|
-
else list.push([k, k + 1]);
|
|
3617
|
-
cols.set(c, list);
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
});
|
|
3621
|
-
return cols;
|
|
3622
|
-
}
|
|
3623
|
-
var edgesOf = (layer, run) => ({
|
|
3624
|
-
top: layer.canvasH / 2 - (layer.bbox.y + run[0]),
|
|
3625
|
-
bottom: layer.canvasH / 2 - (layer.bbox.y + run[1])
|
|
3626
|
-
});
|
|
3627
|
-
function mouthOpening(byRole) {
|
|
3628
|
-
const inner = byRole.get("mouth_inner");
|
|
3629
|
-
const upper = byRole.get("lip_upper");
|
|
3630
|
-
const lower = byRole.get("lip_lower");
|
|
3631
|
-
const innerCols = columnRuns(inner);
|
|
3632
|
-
if (innerCols.size === 0) {
|
|
3633
|
-
throw new LayerGeometryError(
|
|
3634
|
-
`auto-rig: layer "${inner.fileName}": mouth_inner has no opaque pixel (alpha \u2265 128) to read the opening off`
|
|
3635
|
-
);
|
|
3636
|
-
}
|
|
3637
|
-
const upperCols = columnRuns(upper);
|
|
3638
|
-
const lowerCols = columnRuns(lower);
|
|
3639
|
-
const cols = [...innerCols.keys()];
|
|
3640
|
-
const x0 = Math.min(...cols);
|
|
3641
|
-
const x1 = Math.max(...cols);
|
|
3642
|
-
const read = (col) => {
|
|
3643
|
-
const runs = innerCols.get(col);
|
|
3644
|
-
if (runs === void 0) return void 0;
|
|
3645
|
-
const T = edgesOf(inner, runs[0]).top;
|
|
3646
|
-
const Bb = edgesOf(inner, runs[runs.length - 1]).bottom;
|
|
3647
|
-
const line = [...upperCols.get(col) ?? []].reverse().map((r) => edgesOf(upper, r)).find((e) => e.top >= T);
|
|
3648
|
-
const Tu = line?.bottom ?? T;
|
|
3649
|
-
const lineTop = line?.top ?? T;
|
|
3650
|
-
const skin = lowerCols.get(col)?.[0];
|
|
3651
|
-
return {
|
|
3652
|
-
T,
|
|
3653
|
-
Bb,
|
|
3654
|
-
Tu,
|
|
3655
|
-
lineTop,
|
|
3656
|
-
lineH: lineTop - Tu,
|
|
3657
|
-
Bl: skin === void 0 ? Bb : edgesOf(lower, skin).top,
|
|
3658
|
-
H: T - Bb,
|
|
3659
|
-
seam: T - UPPER_SHARE * (T - Bb)
|
|
3660
|
-
};
|
|
3661
|
-
};
|
|
3662
|
-
const reads = [];
|
|
3663
|
-
for (let c = x0; c <= x1; c++) {
|
|
3664
|
-
let r;
|
|
3665
|
-
for (let d = 0; (r = read(c - d) ?? read(c + d)) === void 0; d++) ;
|
|
3666
|
-
reads.push(r);
|
|
3667
|
-
}
|
|
3668
|
-
const base = (col) => reads[Math.min(x1, Math.max(x0, col)) - x0];
|
|
3669
|
-
const centre = Math.round((x0 + x1) / 2);
|
|
3670
|
-
const w = base(centre).lineH;
|
|
3671
|
-
return {
|
|
3672
|
-
x0,
|
|
3673
|
-
x1,
|
|
3674
|
-
centre,
|
|
3675
|
-
canvasW: inner.canvasW,
|
|
3676
|
-
w,
|
|
3677
|
-
at(col) {
|
|
3678
|
-
const b = base(col);
|
|
3679
|
-
return { ...b, overlap: Math.max(0, Math.min(w, b.lineH)) };
|
|
3680
|
-
}
|
|
3681
|
-
};
|
|
3682
|
-
}
|
|
3683
|
-
function mouthFold(role, opening) {
|
|
3684
|
-
return (col, y, v) => {
|
|
3685
|
-
if (v === 1) return 0;
|
|
3686
|
-
if (role !== "mouth_inner" && (col < opening.x0 || col > opening.x1))
|
|
3687
|
-
return 0;
|
|
3688
|
-
const c = opening.at(col);
|
|
3689
|
-
const k = 1 - v;
|
|
3690
|
-
switch (role) {
|
|
3691
|
-
case "lip_upper":
|
|
3692
|
-
return (c.seam - c.Tu) * k;
|
|
3693
|
-
case "lip_lower":
|
|
3694
|
-
return (c.seam + c.overlap - c.Bl) * k;
|
|
3695
|
-
case "mouth_inner":
|
|
3696
|
-
return (c.seam + c.overlap - y) * k;
|
|
3697
|
-
}
|
|
3698
|
-
};
|
|
3699
|
-
}
|
|
3700
|
-
function columnOfKnot(knot, opening, role) {
|
|
3701
|
-
const boundary = Math.round(knot + opening.canvasW / 2);
|
|
3702
|
-
const col = boundary === opening.x1 + 1 ? opening.x1 : boundary;
|
|
3703
|
-
if (col >= opening.x0 && col <= opening.x1) return col;
|
|
3704
|
-
return role === "mouth_inner" ? Math.min(opening.x1, Math.max(opening.x0, col)) : void 0;
|
|
3705
|
-
}
|
|
3706
|
-
function mouthFoldWarp(role, mesh, xs, box, opening) {
|
|
3707
|
-
const fold = mouthFold(role, opening);
|
|
3708
|
-
return localWarp(P6.MouthOpen, mesh, box, [0, 1], (p, v, i) => {
|
|
3709
|
-
const col = columnOfKnot(xs[i % xs.length], opening, role);
|
|
3710
|
-
return [0, col === void 0 ? 0 : fold(col, p[1], v)];
|
|
3711
|
-
});
|
|
3712
|
-
}
|
|
3713
|
-
function mouthKnots(union, opening) {
|
|
3714
|
-
const half = opening.canvasW / 2;
|
|
3715
|
-
const step = MOUTH_KNOT_PX;
|
|
3716
|
-
const xs = [union.x0];
|
|
3717
|
-
while (xs[xs.length - 1] + step < union.x1 - 0.3 * step)
|
|
3718
|
-
xs.push(xs[xs.length - 1] + step);
|
|
3719
|
-
xs.push(union.x1);
|
|
3720
|
-
const fixed = /* @__PURE__ */ new Set([union.x0, union.x1]);
|
|
3721
|
-
for (const x of [opening.x0 - half, opening.x1 + 1 - half]) {
|
|
3722
|
-
fixed.add(x);
|
|
3723
|
-
if (xs.includes(x)) continue;
|
|
3724
|
-
const near = xs.findIndex((v) => Math.abs(v - x) < 0.3 * step);
|
|
3725
|
-
if (near >= 0 && !fixed.has(xs[near])) xs[near] = x;
|
|
3726
|
-
else xs.push(x);
|
|
3727
|
-
}
|
|
3728
|
-
xs.sort((p, q) => p - q);
|
|
3729
|
-
for (const x of xs) {
|
|
3730
|
-
if (!Number.isInteger(x + half)) {
|
|
3731
|
-
throw new Error(
|
|
3732
|
-
`auto-rig: mouth knot ${x} is not an integer canvas boundary`
|
|
3733
|
-
);
|
|
3734
|
-
}
|
|
3735
|
-
}
|
|
3736
|
-
return xs;
|
|
3737
|
-
}
|
|
3738
|
-
function mouthMesh(box, knots) {
|
|
3739
|
-
const xs = [box.x0, ...knots.filter((k) => k > box.x0 && k < box.x1), box.x1];
|
|
3740
|
-
const rows = cellsFor(bh(box), MOUTH_KNOT_PX, 2, 40);
|
|
3741
|
-
const ys = Array.from(
|
|
3742
|
-
{ length: rows + 1 },
|
|
3743
|
-
(_, r) => box.y1 - bh(box) * r / rows
|
|
3744
|
-
);
|
|
3745
|
-
return { mesh: columnMesh(box, { xs, ys }), xs };
|
|
3746
|
-
}
|
|
3747
|
-
function buildMouthRig(byRole) {
|
|
3748
|
-
const opening = mouthOpening(byRole);
|
|
3749
|
-
const union = unionBoxes(LIP_ROLES.map((r) => boxOfLayer(byRole.get(r))));
|
|
3750
|
-
return {
|
|
3751
|
-
opening,
|
|
3752
|
-
frame: mouthFrameOf(union),
|
|
3753
|
-
knots: mouthKnots(union, opening),
|
|
3754
|
-
union
|
|
3755
|
-
};
|
|
3756
|
-
}
|
|
3757
|
-
function mouthAnchor(byRole) {
|
|
3758
|
-
const box = (role) => boxOfLayer(byRole.get(role));
|
|
3759
|
-
if (isLipSet(byRole)) {
|
|
3760
|
-
const opening = mouthOpening(byRole);
|
|
3761
|
-
return {
|
|
3762
|
-
at: {
|
|
3763
|
-
x: cx(box("mouth_inner")),
|
|
3764
|
-
y: opening.at(opening.centre).seam
|
|
3765
|
-
},
|
|
3766
|
-
box: unionBoxes(LIP_ROLES.map(box))
|
|
3767
|
-
};
|
|
3768
|
-
}
|
|
3769
|
-
const m = box("mouth");
|
|
3770
|
-
return {
|
|
3771
|
-
at: { x: cx(m), y: cy(m) },
|
|
3772
|
-
box: byRole.has("mouth_open") ? unionBoxes([m, box("mouth_open")]) : m
|
|
3773
|
-
};
|
|
3774
|
-
}
|
|
3775
|
-
|
|
3776
3834
|
// src/auto-rig/head.ts
|
|
3777
3835
|
function outerEdge(runs, axisX, side) {
|
|
3778
3836
|
if (runs.length === 0) return void 0;
|
|
@@ -6184,6 +6242,7 @@ export {
|
|
|
6184
6242
|
DeletePhysicsRig,
|
|
6185
6243
|
EditorDocument,
|
|
6186
6244
|
HEAD_BAND,
|
|
6245
|
+
LIP_ROLES,
|
|
6187
6246
|
LayerGeometryError,
|
|
6188
6247
|
SPECK_CORE_FRACTION,
|
|
6189
6248
|
SetDeformerBindings,
|
|
@@ -6205,6 +6264,7 @@ export {
|
|
|
6205
6264
|
UV_INSET_PX,
|
|
6206
6265
|
armGeometry,
|
|
6207
6266
|
captureBindingEndpoint,
|
|
6267
|
+
columnRuns,
|
|
6208
6268
|
computeGridOffsets,
|
|
6209
6269
|
createDefaultMatrixDeformer,
|
|
6210
6270
|
createDefaultPart,
|
|
@@ -6219,6 +6279,9 @@ export {
|
|
|
6219
6279
|
headHalfOf,
|
|
6220
6280
|
interpolateGridOffsets,
|
|
6221
6281
|
isSpeckCore,
|
|
6282
|
+
mouthFold,
|
|
6283
|
+
mouthOpening,
|
|
6284
|
+
mouthRestShift,
|
|
6222
6285
|
packAtlas,
|
|
6223
6286
|
parseLayerRoles,
|
|
6224
6287
|
partIdsOfRole,
|