@ikijs/editor 0.20.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/dist/index.mjs CHANGED
@@ -1567,7 +1567,10 @@ var ROW_RUN_ROLES = /* @__PURE__ */ new Set([
1567
1567
  "arm_L",
1568
1568
  "arm_R",
1569
1569
  "forearm_pose_L",
1570
- "forearm_pose_R"
1570
+ "forearm_pose_R",
1571
+ "mouth_inner",
1572
+ "lip_lower",
1573
+ "lip_upper"
1571
1574
  ]);
1572
1575
  var REST_HIDDEN_ROLES = /* @__PURE__ */ new Set([
1573
1576
  "forearm_pose_L",
@@ -2184,6 +2187,29 @@ function gridMesh(cols, rows) {
2184
2187
  }
2185
2188
  return { vertices, uvs, indices };
2186
2189
  }
2190
+ function columnMesh(b, grid) {
2191
+ const { xs } = grid;
2192
+ const rows = grid.ys.length - 1;
2193
+ const vertices = [];
2194
+ const uvs = [];
2195
+ for (let r = 0; r <= rows; r++) {
2196
+ for (const x of xs) {
2197
+ const u = (x - b.x0) / bw(b);
2198
+ vertices.push(roundTo(u - 0.5, 1e-5), roundTo(0.5 - r / rows, 1e-5));
2199
+ uvs.push(roundTo(u, 1e-5), roundTo(r / rows, 1e-5));
2200
+ }
2201
+ }
2202
+ const n = xs.length;
2203
+ const indices = [];
2204
+ for (let r = 0; r < rows; r++) {
2205
+ for (let c = 0; c + 1 < n; c++) {
2206
+ const tl = r * n + c;
2207
+ const bl = tl + n;
2208
+ indices.push(bl, bl + 1, tl, tl, bl + 1, tl + 1);
2209
+ }
2210
+ }
2211
+ return { vertices, uvs, indices };
2212
+ }
2187
2213
  function meshPoints(mesh, box) {
2188
2214
  const out = [];
2189
2215
  for (let i = 0; i < mesh.vertices.length; i += 2) {
@@ -2309,10 +2335,13 @@ function browBindings(side, reach) {
2309
2335
  ];
2310
2336
  }
2311
2337
  var BROW_TILT = 15;
2312
- function mouthForm(mesh, box) {
2313
- const a = 0.3 * bh(box);
2314
- const c = cx(box);
2315
- const half = bw(box) / 2;
2338
+ var mouthFrameOf = (box) => ({
2339
+ c: cx(box),
2340
+ half: bw(box) / 2,
2341
+ amp: 0.3 * bh(box)
2342
+ });
2343
+ function mouthForm(mesh, box, frame = mouthFrameOf(box)) {
2344
+ const { c, half, amp: a } = frame;
2316
2345
  return localWarp(P.MouthForm, mesh, box, [-1, 0, 1], ([x], v) => {
2317
2346
  if (v === 0) return [0, 0];
2318
2347
  const u = (x - c) / half;
@@ -2330,8 +2359,8 @@ function mouthOpenGrow(mesh, box) {
2330
2359
  ([x, y], v) => v === 1 ? [(AMPLITUDE.mouthOpenWidth - 1) * (x - c), 0] : [0, box.y1 + (y - box.y1) * 0.2 - y]
2331
2360
  );
2332
2361
  }
2333
- function mouthWiden(mesh, box) {
2334
- const c = cx(box);
2362
+ function mouthWiden(mesh, box, frame = mouthFrameOf(box)) {
2363
+ const c = frame.c;
2335
2364
  return localWarp(P.MouthOpen, mesh, box, [0, 1], ([x], v) => [
2336
2365
  v * (AMPLITUDE.mouthOpenWidth - 1) * (x - c),
2337
2366
  0
@@ -2989,7 +3018,14 @@ function forearmPoseDeformer(role, arm) {
2989
3018
  };
2990
3019
  }
2991
3020
 
3021
+ // src/auto-rig/mouth.ts
3022
+ import {
3023
+ StandardParameter as P5
3024
+ } from "@ikijs/format";
3025
+
2992
3026
  // src/auto-rig/roles.ts
3027
+ var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
3028
+ var isLipRole = (role) => LIP_ROLES.includes(role);
2993
3029
  var ROLE_TABLE = [
2994
3030
  { role: "hair_back", family: "hair_back" },
2995
3031
  { role: "body", family: "body" },
@@ -2999,8 +3035,14 @@ var ROLE_TABLE = [
2999
3035
  { role: "blush_L", family: "face" },
3000
3036
  { role: "blush_R", family: "face" },
3001
3037
  { role: "nose", family: "nose" },
3002
- { role: "mouth", family: "mouth", required: true },
3038
+ { role: "mouth", family: "mouth" },
3003
3039
  { role: "mouth_open", family: "mouth" },
3040
+ // The lip set, back to front: the interior, the lower lip's skin, the upper
3041
+ // lip and its line over both. It folds open instead of crossfading
3042
+ // (`mouth.ts`), so it stands in for `mouth`, never beside it.
3043
+ { role: "mouth_inner", family: "mouth" },
3044
+ { role: "lip_lower", family: "mouth" },
3045
+ { role: "lip_upper", family: "mouth" },
3004
3046
  { role: "eye_L", family: "eye_L", required: true },
3005
3047
  { role: "eye_R", family: "eye_R", required: true },
3006
3048
  { role: "iris_L", family: "eye_L" },
@@ -3058,6 +3100,29 @@ function checkArmsHaveBody(roles) {
3058
3100
  );
3059
3101
  }
3060
3102
  }
3103
+ function checkMouth(roles) {
3104
+ const has = new Set(roles);
3105
+ const present = LIP_ROLES.filter((r) => has.has(r));
3106
+ if (present.length === 0) {
3107
+ if (!has.has("mouth")) {
3108
+ throw new Error(
3109
+ `auto-rig: missing required role mouth (or the lip set ${LIP_ROLES.join(", ")})`
3110
+ );
3111
+ }
3112
+ return;
3113
+ }
3114
+ if (present.length < LIP_ROLES.length) {
3115
+ throw new Error(
3116
+ `auto-rig: the lip set is partial: missing ${LIP_ROLES.filter((r) => !has.has(r)).join(", ")}`
3117
+ );
3118
+ }
3119
+ const mixed = ["mouth", "mouth_open"].filter((r) => has.has(r));
3120
+ if (mixed.length > 0) {
3121
+ throw new Error(
3122
+ `auto-rig: the lip set cannot be mixed with ${mixed.join(", ")} (the lip set replaces the mouth drawings; use one or the other)`
3123
+ );
3124
+ }
3125
+ }
3061
3126
  function checkPosesHaveArms(roles) {
3062
3127
  const has = new Set(roles);
3063
3128
  for (const side of ["L", "R"]) {
@@ -3093,44 +3158,273 @@ function parseLayerRoles(fileNames) {
3093
3158
  `auto-rig: missing required role${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`
3094
3159
  );
3095
3160
  }
3161
+ checkMouth(seen.keys());
3096
3162
  checkArmsHaveBody(seen.keys());
3097
3163
  checkPosesHaveArms(seen.keys());
3098
3164
  return out;
3099
3165
  }
3100
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
+
3101
3395
  // src/auto-rig/generate.ts
3102
3396
  import {
3103
3397
  IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
3104
- StandardParameter as P7,
3398
+ StandardParameter as P8,
3105
3399
  parseIkiModel as parseIkiModel3
3106
3400
  } from "@ikijs/format";
3107
3401
 
3108
3402
  // src/auto-rig/animations.ts
3109
3403
  import {
3110
- StandardParameter as P5
3404
+ StandardParameter as P6
3111
3405
  } from "@ikijs/format";
3112
3406
  function browsY(y) {
3113
3407
  return [
3114
- { parameter: P5.BrowLeftY, value: y, blend: "add" },
3115
- { parameter: P5.BrowRightY, value: y, blend: "add" }
3408
+ { parameter: P6.BrowLeftY, value: y, blend: "add" },
3409
+ { parameter: P6.BrowRightY, value: y, blend: "add" }
3116
3410
  ];
3117
3411
  }
3118
3412
  function browsTilt(innerDown) {
3119
3413
  return [
3120
- { parameter: P5.BrowLeftAngle, value: innerDown, blend: "add" },
3121
- { parameter: P5.BrowRightAngle, value: -innerDown, blend: "add" }
3414
+ { parameter: P6.BrowLeftAngle, value: innerDown, blend: "add" },
3415
+ { parameter: P6.BrowRightAngle, value: -innerDown, blend: "add" }
3122
3416
  ];
3123
3417
  }
3124
3418
  function cheek(value) {
3125
- return { parameter: P5.Cheek, value, blend: "add" };
3419
+ return { parameter: P6.Cheek, value, blend: "add" };
3126
3420
  }
3127
- var BROWS = [P5.BrowLeftY, P5.BrowRightY, P5.BrowLeftAngle, P5.BrowRightAngle];
3421
+ var BROWS = [P6.BrowLeftY, P6.BrowRightY, P6.BrowLeftAngle, P6.BrowRightAngle];
3128
3422
  var EXPRESSIONS = [
3129
3423
  {
3130
3424
  id: "smile",
3131
3425
  description: "Smiling and pleased: warm, friendly, content. For greetings, thanks, agreement and gentle happiness.",
3132
3426
  parameters: [
3133
- { parameter: P5.MouthForm, value: 1, blend: "add" },
3427
+ { parameter: P6.MouthForm, value: 1, blend: "add" },
3134
3428
  ...browsY(0.25),
3135
3429
  cheek(0.4)
3136
3430
  ]
@@ -3139,10 +3433,10 @@ var EXPRESSIONS = [
3139
3433
  id: "laugh",
3140
3434
  description: "Laughing, eyes shut and mouth open: delighted, amused. For jokes, playfulness and big joy.",
3141
3435
  parameters: [
3142
- { parameter: P5.EyeOpenLeft, value: 0, blend: "multiply" },
3143
- { parameter: P5.EyeOpenRight, value: 0, blend: "multiply" },
3144
- { parameter: P5.MouthForm, value: 1, blend: "add" },
3145
- { parameter: P5.MouthOpen, value: 0.8, blend: "overwrite" },
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" },
3146
3440
  ...browsY(0.4),
3147
3441
  cheek(0.65)
3148
3442
  ]
@@ -3154,7 +3448,7 @@ var EXPRESSIONS = [
3154
3448
  parameters: [
3155
3449
  ...browsY(-0.5),
3156
3450
  ...browsTilt(0.8),
3157
- { parameter: P5.MouthForm, value: -0.9, blend: "add" }
3451
+ { parameter: P6.MouthForm, value: -0.9, blend: "add" }
3158
3452
  ]
3159
3453
  },
3160
3454
  {
@@ -3164,7 +3458,7 @@ var EXPRESSIONS = [
3164
3458
  parameters: [
3165
3459
  ...browsY(0.35),
3166
3460
  ...browsTilt(-0.8),
3167
- { parameter: P5.MouthForm, value: -0.8, blend: "add" }
3461
+ { parameter: P6.MouthForm, value: -0.8, blend: "add" }
3168
3462
  ]
3169
3463
  },
3170
3464
  {
@@ -3172,18 +3466,18 @@ var EXPRESSIONS = [
3172
3466
  description: "Surprised, mouth dropped open: startled, amazed. For shock, sudden news or disbelief.",
3173
3467
  parameters: [
3174
3468
  ...browsY(0.8),
3175
- { parameter: P5.MouthOpen, value: 0.9, blend: "overwrite" }
3469
+ { parameter: P6.MouthOpen, value: 0.9, blend: "overwrite" }
3176
3470
  ]
3177
3471
  },
3178
3472
  {
3179
3473
  id: "shy",
3180
3474
  description: "Shy or embarrassed: bashful, flustered. For being praised, teased or caught off guard.",
3181
- needs: [P5.Cheek],
3475
+ needs: [P6.Cheek],
3182
3476
  parameters: [
3183
3477
  cheek(1),
3184
3478
  // Gaze down.
3185
- { parameter: P5.EyeballY, value: -0.65, blend: "add" },
3186
- { parameter: P5.MouthForm, value: 0.4, blend: "add" },
3479
+ { parameter: P6.EyeballY, value: -0.65, blend: "add" },
3480
+ { parameter: P6.MouthForm, value: 0.4, blend: "add" },
3187
3481
  ...browsTilt(-0.25)
3188
3482
  ]
3189
3483
  }
@@ -3203,7 +3497,7 @@ var HEAD_MOTIONS = {
3203
3497
  duration: 1,
3204
3498
  curves: [
3205
3499
  {
3206
- parameter: P5.AngleY,
3500
+ parameter: P6.AngleY,
3207
3501
  keys: [
3208
3502
  [0, 0],
3209
3503
  [0.3, -18],
@@ -3224,7 +3518,7 @@ var HEAD_MOTIONS = {
3224
3518
  fadeIn: 0.15,
3225
3519
  curves: [
3226
3520
  {
3227
- parameter: P5.AngleX,
3521
+ parameter: P6.AngleX,
3228
3522
  keys: [
3229
3523
  [0, 0],
3230
3524
  [0.2, -18],
@@ -3243,7 +3537,7 @@ var HEAD_MOTIONS = {
3243
3537
  duration: 1.4,
3244
3538
  curves: [
3245
3539
  {
3246
- parameter: P5.AngleZ,
3540
+ parameter: P6.AngleZ,
3247
3541
  keys: [
3248
3542
  [0, 0],
3249
3543
  [0.35, 20],
@@ -3264,7 +3558,7 @@ var WAVE = {
3264
3558
  fadeOut: 0.15,
3265
3559
  curves: [
3266
3560
  {
3267
- parameter: P5.ArmPoseRight,
3561
+ parameter: P6.ArmPoseRight,
3268
3562
  interpolation: "linear",
3269
3563
  keys: [
3270
3564
  [0, 0],
@@ -3274,7 +3568,7 @@ var WAVE = {
3274
3568
  ]
3275
3569
  },
3276
3570
  {
3277
- parameter: P5.ArmRight,
3571
+ parameter: P6.ArmRight,
3278
3572
  keys: [
3279
3573
  [0, 0],
3280
3574
  [0.4, 10],
@@ -3283,7 +3577,7 @@ var WAVE = {
3283
3577
  ]
3284
3578
  },
3285
3579
  {
3286
- parameter: P5.ArmPoseAngleRight,
3580
+ parameter: P6.ArmPoseAngleRight,
3287
3581
  keys: [
3288
3582
  [0, 0],
3289
3583
  [0.5, 0],
@@ -3299,7 +3593,7 @@ var WAVE = {
3299
3593
  ]
3300
3594
  };
3301
3595
  function defaultMotions(declared) {
3302
- const waves = [P5.ArmRight, P5.ArmPoseRight, P5.ArmPoseAngleRight].every(
3596
+ const waves = [P6.ArmRight, P6.ArmPoseRight, P6.ArmPoseAngleRight].every(
3303
3597
  (id) => declared.has(id)
3304
3598
  );
3305
3599
  return waves ? { ...HEAD_MOTIONS, ...WAVE } : HEAD_MOTIONS;
@@ -3408,6 +3702,14 @@ function checkLayers(layers, canvas) {
3408
3702
  if (missing.length > 0) {
3409
3703
  throw new Error(`auto-rig: missing required role(s) ${missing.join(", ")}`);
3410
3704
  }
3705
+ checkMouth(seen);
3706
+ for (const l of layers) {
3707
+ if (l.rowRuns === void 0 && isLipRole(l.role)) {
3708
+ throw new Error(
3709
+ `auto-rig: layer "${l.fileName}": the lip set needs rowRuns (the fold reads the opening's rows off them; measure with createLayerSetMeasurer)`
3710
+ );
3711
+ }
3712
+ }
3411
3713
  checkArmsHaveBody(seen);
3412
3714
  checkPosesHaveArms(seen);
3413
3715
  }
@@ -3527,7 +3829,7 @@ function checkStrandEdges(strand, irises, hair) {
3527
3829
  }
3528
3830
 
3529
3831
  // src/auto-rig/context.ts
3530
- import { StandardParameter as P6 } from "@ikijs/format";
3832
+ import { StandardParameter as P7 } from "@ikijs/format";
3531
3833
 
3532
3834
  // src/auto-rig/head.ts
3533
3835
  function outerEdge(runs, axisX, side) {
@@ -3607,7 +3909,7 @@ function buildHeadFrame(layers, opts) {
3607
3909
  return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
3608
3910
  };
3609
3911
  const eyeY = pair("iris_L", "iris_R") ?? pair("eye_L", "eye_R");
3610
- const mouthY = centreY("mouth");
3912
+ const mouthY = mouthAnchor(byRole).at.y;
3611
3913
  const rowY = (r) => canvasH / 2 - (faceLayer.bbox.y + r + 0.5);
3612
3914
  const raw = filledProfile(faceLayer.rowHalfWidths);
3613
3915
  const profile = raw === null ? null : smoothProfile(raw);
@@ -4580,7 +4882,6 @@ function sideIrises(byRole) {
4580
4882
  return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
4581
4883
  }
4582
4884
  function solveContext(frame, byRole, options, hasNose) {
4583
- const box = (role) => boxOfLayer(byRole.get(role));
4584
4885
  const row = (s, strand2, sideSign) => {
4585
4886
  if (strand2 !== void 0) {
4586
4887
  const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
@@ -4629,8 +4930,7 @@ function solveContext(frame, byRole, options, hasNose) {
4629
4930
  noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
4630
4931
  noseAt = { x: cx(noseBox), y: cy(noseBox) };
4631
4932
  }
4632
- const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
4633
- const m = box("mouth");
4933
+ const mouth = mouthAnchor(byRole);
4634
4934
  return {
4635
4935
  frame,
4636
4936
  left,
@@ -4642,8 +4942,8 @@ function solveContext(frame, byRole, options, hasNose) {
4642
4942
  eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
4643
4943
  noseAt,
4644
4944
  noseBox,
4645
- mouthAt: { x: cx(m), y: cy(m) },
4646
- mouthBox
4945
+ mouthAt: mouth.at,
4946
+ mouthBox: mouth.box
4647
4947
  };
4648
4948
  }
4649
4949
  function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
@@ -4659,7 +4959,7 @@ function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
4659
4959
  const family = roleSpec(role).family;
4660
4960
  const part = parts.find((p) => p.id === role);
4661
4961
  const g = grids.get(family);
4662
- const warp = part?.warps?.find((w) => w.parameter === P6.AngleX);
4962
+ const warp = part?.warps?.find((w) => w.parameter === P7.AngleX);
4663
4963
  if (part === void 0 || part.mesh === void 0 || !g && !warp) {
4664
4964
  return x + field(role)(x, y, ax, 0)[0];
4665
4965
  }
@@ -5456,6 +5756,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
5456
5756
  hh: frame.hh,
5457
5757
  pivots: [...arms.values()].map((g) => g.shoulder)
5458
5758
  }) : void 0;
5759
+ const lips = isLipSet(byRole) ? buildMouthRig(byRole) : void 0;
5459
5760
  const parts = [];
5460
5761
  const reach = /* @__PURE__ */ new Map();
5461
5762
  let regionOf = [];
@@ -5487,7 +5788,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
5487
5788
  frame,
5488
5789
  byRole,
5489
5790
  style,
5490
- landmarks.hairFrontGrid
5791
+ landmarks.hairFrontGrid,
5792
+ lips
5491
5793
  );
5492
5794
  if (built.region !== void 0) {
5493
5795
  regionOf = built.region;
@@ -5569,14 +5871,14 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
5569
5871
  function turnWarps(mesh, b, fieldOf) {
5570
5872
  return [
5571
5873
  localWarp(
5572
- P7.AngleX,
5874
+ P8.AngleX,
5573
5875
  mesh,
5574
5876
  b,
5575
5877
  X_STOPS,
5576
5878
  ([x, y], v, i) => v === 0 ? [0, 0] : [fieldOf(i)(x, y, v, 0)[0], 0]
5577
5879
  ),
5578
5880
  localWarp(
5579
- P7.AngleY,
5881
+ P8.AngleY,
5580
5882
  mesh,
5581
5883
  b,
5582
5884
  Y_STOPS,
@@ -5589,7 +5891,7 @@ function plateFields(turn, region, head) {
5589
5891
  const neck = neckField(turn);
5590
5892
  return (i) => region[i] === "ear" ? ear : region[i] === "neck" ? neck : head;
5591
5893
  }
5592
- function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5894
+ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
5593
5895
  const box = (role) => boxOfLayer(byRole.get(role));
5594
5896
  const part = {
5595
5897
  id: spec.role,
@@ -5604,6 +5906,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5604
5906
  const bindings = [];
5605
5907
  let extra = [0, 0];
5606
5908
  let mesh;
5909
+ let knotXs;
5607
5910
  let region;
5608
5911
  let headStart = 0;
5609
5912
  if (spec.role === "face") {
@@ -5614,6 +5917,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5614
5917
  headStart = fm.headStart;
5615
5918
  } else if (spec.role === "hair_front") {
5616
5919
  mesh = columnMesh(b, frontGrid);
5920
+ } else if (lips !== void 0 && isLipRole(spec.role)) {
5921
+ ({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
5617
5922
  } else if (spec.family !== "body") {
5618
5923
  const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
5619
5924
  const px = Math.min(cells.px, meshScale(spec.role, frame));
@@ -5624,7 +5929,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5624
5929
  }
5625
5930
  if (mesh !== void 0) part.mesh = mesh;
5626
5931
  const side = spec.role.endsWith("_L") ? "L" : "R";
5627
- const eyeOpen = side === "L" ? P7.EyeOpenLeft : P7.EyeOpenRight;
5932
+ const eyeOpen = side === "L" ? P8.EyeOpenLeft : P8.EyeOpenRight;
5628
5933
  const hh = frame.hh;
5629
5934
  switch (spec.role) {
5630
5935
  case "face":
@@ -5639,7 +5944,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5639
5944
  headOwnRoll(byRole.has("body"))
5640
5945
  ),
5641
5946
  localWarp(
5642
- P7.Breath,
5947
+ P8.Breath,
5643
5948
  mesh,
5644
5949
  b,
5645
5950
  [0, 1],
@@ -5686,8 +5991,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5686
5991
  if (byRole.has("mouth_open")) {
5687
5992
  warps.push(mouthWiden(mesh, b));
5688
5993
  bindings.push(
5689
- { parameter: P7.MouthOpen, channel: "opacity", from: 1, to: 0 },
5690
- { parameter: P7.MouthOpen, channel: "opacity", from: 1, to: 0 }
5994
+ { parameter: P8.MouthOpen, channel: "opacity", from: 1, to: 0 },
5995
+ { parameter: P8.MouthOpen, channel: "opacity", from: 1, to: 0 }
5691
5996
  );
5692
5997
  extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
5693
5998
  } else {
@@ -5697,19 +6002,19 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5697
6002
  );
5698
6003
  bindings.push(
5699
6004
  {
5700
- parameter: P7.MouthOpen,
6005
+ parameter: P8.MouthOpen,
5701
6006
  channel: "scaleY",
5702
6007
  from: 0,
5703
6008
  to: roundTo(grow, 0.01)
5704
6009
  },
5705
6010
  {
5706
- parameter: P7.MouthOpen,
6011
+ parameter: P8.MouthOpen,
5707
6012
  channel: "scaleX",
5708
6013
  from: 0,
5709
6014
  to: roundTo(AMPLITUDE.mouthOpenWidth - 1, 0.01)
5710
6015
  },
5711
6016
  {
5712
- parameter: P7.MouthOpen,
6017
+ parameter: P8.MouthOpen,
5713
6018
  channel: "translateY",
5714
6019
  from: 0,
5715
6020
  to: -roundTo(grow * bh(b) / 2, 0.01)
@@ -5721,17 +6026,26 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5721
6026
  case "mouth_open":
5722
6027
  warps.push(mouthForm(mesh, b), mouthOpenGrow(mesh, b));
5723
6028
  bindings.push({
5724
- parameter: P7.MouthOpen,
6029
+ parameter: P8.MouthOpen,
5725
6030
  channel: "opacity",
5726
6031
  from: 0,
5727
6032
  to: 1
5728
6033
  });
5729
6034
  extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
5730
6035
  break;
6036
+ case "mouth_inner":
6037
+ case "lip_lower":
6038
+ case "lip_upper":
6039
+ warps.push(
6040
+ mouthFoldWarp(spec.role, mesh, knotXs, b, lips.opening),
6041
+ mouthForm(mesh, b, lips.frame),
6042
+ mouthWiden(mesh, b, lips.frame)
6043
+ );
6044
+ break;
5731
6045
  case "blush_L":
5732
6046
  case "blush_R":
5733
6047
  bindings.push({
5734
- parameter: P7.Cheek,
6048
+ parameter: P8.Cheek,
5735
6049
  channel: "opacity",
5736
6050
  from: AMPLITUDE.blushRest,
5737
6051
  to: 1
@@ -5742,8 +6056,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5742
6056
  if (byRole.has("hair_front")) {
5743
6057
  const amp = style.sway * AMPLITUDE.sway * hh;
5744
6058
  warps.push(
5745
- hairSway(P7.HairSwayX, mesh, b, amp),
5746
- hairSway(P7.HairSwayZ, mesh, b, amp)
6059
+ hairSway(P8.HairSwayX, mesh, b, amp),
6060
+ hairSway(P8.HairSwayZ, mesh, b, amp)
5747
6061
  );
5748
6062
  }
5749
6063
  warps.push(tiltHang(mesh, b, pivot, hairHangWeight(frame.chinY, b.y0)));
@@ -5784,29 +6098,6 @@ function hairFrontGrid(b, frame, boxes) {
5784
6098
  );
5785
6099
  return { xs, ys };
5786
6100
  }
5787
- function columnMesh(b, grid) {
5788
- const { xs } = grid;
5789
- const rows = grid.ys.length - 1;
5790
- const vertices = [];
5791
- const uvs = [];
5792
- for (let r = 0; r <= rows; r++) {
5793
- for (const x of xs) {
5794
- const u = (x - b.x0) / bw(b);
5795
- vertices.push(roundTo(u - 0.5, 1e-5), roundTo(0.5 - r / rows, 1e-5));
5796
- uvs.push(roundTo(u, 1e-5), roundTo(r / rows, 1e-5));
5797
- }
5798
- }
5799
- const n = xs.length;
5800
- const indices = [];
5801
- for (let r = 0; r < rows; r++) {
5802
- for (let c = 0; c + 1 < n; c++) {
5803
- const tl = r * n + c;
5804
- const bl = tl + n;
5805
- indices.push(bl, bl + 1, tl, tl, bl + 1, tl + 1);
5806
- }
5807
- }
5808
- return { vertices, uvs, indices };
5809
- }
5810
6101
  function meshScale(role, frame) {
5811
6102
  switch (role) {
5812
6103
  case "hair_front":
@@ -5832,9 +6123,9 @@ function deformers(frame, grids, bodyWarp, arms, byRole) {
5832
6123
  pivot: { x: roundTo(frame.axisX, 0.01), y: roundTo(frame.chinY, 0.01) },
5833
6124
  bindings: [
5834
6125
  // AngleZ is clockwise-positive (Live2D's); a rotation is CCW-positive.
5835
- { parameter: P7.AngleZ, channel: "rotate", from: roll, to: -roll },
6126
+ { parameter: P8.AngleZ, channel: "rotate", from: roll, to: -roll },
5836
6127
  {
5837
- parameter: P7.Breath,
6128
+ parameter: P8.Breath,
5838
6129
  channel: "translateY",
5839
6130
  from: 0,
5840
6131
  to: roundTo(headOwnBreath(hasBody) * frame.hh, 0.01)
@@ -5852,8 +6143,8 @@ function deformers(frame, grids, bodyWarp, arms, byRole) {
5852
6143
  points: g.lattice.points
5853
6144
  },
5854
6145
  warp2d: {
5855
- parameter: P7.AngleX,
5856
- parameterY: P7.AngleY,
6146
+ parameter: P8.AngleX,
6147
+ parameterY: P8.AngleY,
5857
6148
  valuesX: X_STOPS,
5858
6149
  valuesY: Y_STOPS,
5859
6150
  keyforms2d: g.keyforms.map((offsets) => ({ offsets }))
@@ -5866,16 +6157,16 @@ function hairPhysics() {
5866
6157
  return [
5867
6158
  {
5868
6159
  id: "hairSway",
5869
- input: { parameter: P7.AngleX, weight: 1 },
5870
- output: { parameter: P7.HairSwayX, scale: 5 },
6160
+ input: { parameter: P8.AngleX, weight: 1 },
6161
+ output: { parameter: P8.HairSwayX, scale: 5 },
5871
6162
  mass: 1,
5872
6163
  stiffness: 30,
5873
6164
  damping: 3
5874
6165
  },
5875
6166
  {
5876
6167
  id: "hairTilt",
5877
- input: { parameter: P7.AngleZ, weight: 1 },
5878
- output: { parameter: P7.HairSwayZ, scale: 8 },
6168
+ input: { parameter: P8.AngleZ, weight: 1 },
6169
+ output: { parameter: P8.HairSwayZ, scale: 8 },
5879
6170
  mass: 1,
5880
6171
  stiffness: 25,
5881
6172
  damping: 2.5
@@ -5885,13 +6176,13 @@ function hairPhysics() {
5885
6176
  function declareParameters(roles) {
5886
6177
  const out = [];
5887
6178
  const add = (id, name, min, max, def) => out.push({ id, name, min, max, default: def });
5888
- add(P7.AngleX, "Head Angle", -30, 30, 0);
5889
- add(P7.AngleY, "Head Angle Y", -30, 30, 0);
5890
- add(P7.AngleZ, "Head Angle Z", -30, 30, 0);
6179
+ add(P8.AngleX, "Head Angle", -30, 30, 0);
6180
+ add(P8.AngleY, "Head Angle Y", -30, 30, 0);
6181
+ add(P8.AngleZ, "Head Angle Z", -30, 30, 0);
5891
6182
  if (roles.has("body")) {
5892
- add(P7.BodyAngleX, "Body Angle X", -10, 10, 0);
5893
- add(P7.BodyAngleY, "Body Angle Y", -10, 10, 0);
5894
- add(P7.BodyAngleZ, "Body Angle Z", -10, 10, 0);
6183
+ add(P8.BodyAngleX, "Body Angle X", -10, 10, 0);
6184
+ add(P8.BodyAngleY, "Body Angle Y", -10, 10, 0);
6185
+ add(P8.BodyAngleZ, "Body Angle Z", -10, 10, 0);
5895
6186
  }
5896
6187
  const addPose = (side, pose, angle) => {
5897
6188
  add(pose, `Arm Pose ${side}`, 0, 1, 0);
@@ -5900,40 +6191,40 @@ function declareParameters(roles) {
5900
6191
  const [armMin, armMax] = ARM_RANGE;
5901
6192
  const [elbowMin, elbowMax] = ELBOW_RANGE;
5902
6193
  if (roles.has("arm_L")) {
5903
- add(P7.ArmLeft, "Arm L", armMin, armMax, 0);
5904
- add(P7.ElbowLeft, "Elbow L", elbowMin, elbowMax, 0);
6194
+ add(P8.ArmLeft, "Arm L", armMin, armMax, 0);
6195
+ add(P8.ElbowLeft, "Elbow L", elbowMin, elbowMax, 0);
5905
6196
  if (roles.has("forearm_pose_L"))
5906
- addPose("L", P7.ArmPoseLeft, P7.ArmPoseAngleLeft);
6197
+ addPose("L", P8.ArmPoseLeft, P8.ArmPoseAngleLeft);
5907
6198
  }
5908
6199
  if (roles.has("arm_R")) {
5909
- add(P7.ArmRight, "Arm R", armMin, armMax, 0);
5910
- add(P7.ElbowRight, "Elbow R", elbowMin, elbowMax, 0);
6200
+ add(P8.ArmRight, "Arm R", armMin, armMax, 0);
6201
+ add(P8.ElbowRight, "Elbow R", elbowMin, elbowMax, 0);
5911
6202
  if (roles.has("forearm_pose_R"))
5912
- addPose("R", P7.ArmPoseRight, P7.ArmPoseAngleRight);
6203
+ addPose("R", P8.ArmPoseRight, P8.ArmPoseAngleRight);
5913
6204
  }
5914
- add(P7.EyeOpenLeft, "Eye L", 0, 1, 1);
5915
- add(P7.EyeOpenRight, "Eye R", 0, 1, 1);
6205
+ add(P8.EyeOpenLeft, "Eye L", 0, 1, 1);
6206
+ add(P8.EyeOpenRight, "Eye R", 0, 1, 1);
5916
6207
  if ([...roles].some((r) => /^(iris|pupil|highlight)_/.test(r))) {
5917
- add(P7.EyeballX, "Gaze X", -1, 1, 0);
5918
- add(P7.EyeballY, "Gaze Y", -1, 1, 0);
6208
+ add(P8.EyeballX, "Gaze X", -1, 1, 0);
6209
+ add(P8.EyeballY, "Gaze Y", -1, 1, 0);
5919
6210
  }
5920
- add(P7.MouthOpen, "Mouth Open", 0, 1, 0);
5921
- add(P7.MouthForm, "Mouth Form", -1, 1, 0);
6211
+ add(P8.MouthOpen, "Mouth Open", 0, 1, 0);
6212
+ add(P8.MouthForm, "Mouth Form", -1, 1, 0);
5922
6213
  if (roles.has("brow_L")) {
5923
- add(P7.BrowLeftY, "Brow L Y", -1, 1, 0);
5924
- add(P7.BrowLeftAngle, "Brow L Angle", -1, 1, 0);
6214
+ add(P8.BrowLeftY, "Brow L Y", -1, 1, 0);
6215
+ add(P8.BrowLeftAngle, "Brow L Angle", -1, 1, 0);
5925
6216
  }
5926
6217
  if (roles.has("brow_R")) {
5927
- add(P7.BrowRightY, "Brow R Y", -1, 1, 0);
5928
- add(P7.BrowRightAngle, "Brow R Angle", -1, 1, 0);
6218
+ add(P8.BrowRightY, "Brow R Y", -1, 1, 0);
6219
+ add(P8.BrowRightAngle, "Brow R Angle", -1, 1, 0);
5929
6220
  }
5930
6221
  if (roles.has("blush_L") || roles.has("blush_R")) {
5931
- add(P7.Cheek, "Cheek", 0, 1, 0);
6222
+ add(P8.Cheek, "Cheek", 0, 1, 0);
5932
6223
  }
5933
- add(P7.Breath, "Breath", 0, 1, 0);
6224
+ add(P8.Breath, "Breath", 0, 1, 0);
5934
6225
  if (roles.has("hair_front")) {
5935
- add(P7.HairSwayX, "Hair Sway X", -SWAY_RANGE, SWAY_RANGE, 0);
5936
- add(P7.HairSwayZ, "Hair Sway Z", -SWAY_RANGE, SWAY_RANGE, 0);
6226
+ add(P8.HairSwayX, "Hair Sway X", -SWAY_RANGE, SWAY_RANGE, 0);
6227
+ add(P8.HairSwayZ, "Hair Sway Z", -SWAY_RANGE, SWAY_RANGE, 0);
5937
6228
  }
5938
6229
  return out;
5939
6230
  }
@@ -5951,6 +6242,7 @@ export {
5951
6242
  DeletePhysicsRig,
5952
6243
  EditorDocument,
5953
6244
  HEAD_BAND,
6245
+ LIP_ROLES,
5954
6246
  LayerGeometryError,
5955
6247
  SPECK_CORE_FRACTION,
5956
6248
  SetDeformerBindings,
@@ -5972,6 +6264,7 @@ export {
5972
6264
  UV_INSET_PX,
5973
6265
  armGeometry,
5974
6266
  captureBindingEndpoint,
6267
+ columnRuns,
5975
6268
  computeGridOffsets,
5976
6269
  createDefaultMatrixDeformer,
5977
6270
  createDefaultPart,
@@ -5986,6 +6279,9 @@ export {
5986
6279
  headHalfOf,
5987
6280
  interpolateGridOffsets,
5988
6281
  isSpeckCore,
6282
+ mouthFold,
6283
+ mouthOpening,
6284
+ mouthRestShift,
5989
6285
  packAtlas,
5990
6286
  parseLayerRoles,
5991
6287
  partIdsOfRole,