@ikijs/editor 0.22.0 → 0.23.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.js CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  DeletePhysicsRig: () => DeletePhysicsRig,
34
34
  EditorDocument: () => EditorDocument,
35
35
  HEAD_BAND: () => HEAD_BAND,
36
+ LIP_INSIDE_ROLES: () => LIP_INSIDE_ROLES,
36
37
  LIP_ROLES: () => LIP_ROLES,
37
38
  LayerGeometryError: () => LayerGeometryError,
38
39
  SPECK_CORE_FRACTION: () => SPECK_CORE_FRACTION,
@@ -69,6 +70,7 @@ __export(index_exports, {
69
70
  generateIkiFromLayerSet: () => generateIkiFromLayerSet,
70
71
  headHalfOf: () => headHalfOf,
71
72
  interpolateGridOffsets: () => interpolateGridOffsets,
73
+ isLipInsideRole: () => isLipInsideRole,
72
74
  isSpeckCore: () => isSpeckCore,
73
75
  mouthFold: () => mouthFold,
74
76
  mouthOpening: () => mouthOpening,
@@ -3098,6 +3100,8 @@ var import_format7 = require("@ikijs/format");
3098
3100
  // src/auto-rig/roles.ts
3099
3101
  var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
3100
3102
  var isLipRole = (role) => LIP_ROLES.includes(role);
3103
+ var LIP_INSIDE_ROLES = ["mouth_tongue", "mouth_teeth"];
3104
+ var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
3101
3105
  var ROLE_TABLE = [
3102
3106
  { role: "hair_back", family: "hair_back" },
3103
3107
  { role: "body", family: "body" },
@@ -3109,10 +3113,13 @@ var ROLE_TABLE = [
3109
3113
  { role: "nose", family: "nose" },
3110
3114
  { role: "mouth", family: "mouth" },
3111
3115
  { role: "mouth_open", family: "mouth" },
3112
- // The lip set, back to front: the interior, the lower lip's skin, the upper
3113
- // lip and its line over both. It folds open instead of crossfading
3114
- // (`mouth.ts`), so it stands in for `mouth`, never beside it.
3116
+ // The lip set, back to front: the interior, the tongue and the teeth clipped
3117
+ // to it, the lower lip's skin, the upper lip and its line over both. It
3118
+ // folds open instead of crossfading (`mouth.ts`), so it stands in for
3119
+ // `mouth`, never beside it.
3115
3120
  { role: "mouth_inner", family: "mouth" },
3121
+ { role: "mouth_tongue", family: "mouth" },
3122
+ { role: "mouth_teeth", family: "mouth" },
3116
3123
  { role: "lip_lower", family: "mouth" },
3117
3124
  { role: "lip_upper", family: "mouth" },
3118
3125
  { role: "eye_L", family: "eye_L", required: true },
@@ -3175,6 +3182,12 @@ function checkArmsHaveBody(roles) {
3175
3182
  function checkMouth(roles) {
3176
3183
  const has = new Set(roles);
3177
3184
  const present = LIP_ROLES.filter((r) => has.has(r));
3185
+ const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
3186
+ if (inside.length > 0 && present.length === 0) {
3187
+ throw new Error(
3188
+ `auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
3189
+ );
3190
+ }
3178
3191
  if (present.length === 0) {
3179
3192
  if (!has.has("mouth")) {
3180
3193
  throw new Error(
@@ -3238,6 +3251,11 @@ function parseLayerRoles(fileNames) {
3238
3251
 
3239
3252
  // src/auto-rig/mouth.ts
3240
3253
  var UPPER_SHARE = 0.4;
3254
+ var UPPER_THIN = 0.7;
3255
+ var END_TAPER = 0.35;
3256
+ var END_TAPER_FROM = 0.7;
3257
+ var FLICK_PULL = 0.5;
3258
+ var FLICK_SQUASH = 0.25;
3241
3259
  var MOUTH_KNOT_PX = 4;
3242
3260
  var isLipSet = (byRole) => LIP_ROLES.every((r) => byRole.has(r));
3243
3261
  function columnRuns(layer) {
@@ -3327,9 +3345,17 @@ function mouthOpening(byRole) {
3327
3345
  const x = x0 + i;
3328
3346
  return { ...r, seam: r.Tu + (x - x0) * (x - x1) * (p + q * (x - m)) };
3329
3347
  });
3330
- const base = (col) => columns[Math.min(x1, Math.max(x0, col)) - x0];
3348
+ const clamp2 = (col) => Math.min(x1, Math.max(x0, col));
3349
+ const base = (col) => columns[clamp2(col) - x0];
3331
3350
  const centre = Math.round((x0 + x1) / 2);
3332
3351
  const w = base(centre).lineH;
3352
+ const from = END_TAPER_FROM * ((x1 - x0 + 1) / 2);
3353
+ const squash = (col) => {
3354
+ const d = Math.abs(col - centre);
3355
+ const edge = col < centre ? centre - upper.bbox.x : upper.bbox.x + upper.bbox.w - 1 - centre;
3356
+ const t = d <= from ? 0 : d >= edge ? 1 : (d - from) / (edge - from);
3357
+ return 1 - (1 - END_TAPER) * t;
3358
+ };
3333
3359
  return {
3334
3360
  x0,
3335
3361
  x1,
@@ -3338,39 +3364,80 @@ function mouthOpening(byRole) {
3338
3364
  w,
3339
3365
  at(col) {
3340
3366
  const b = base(col);
3341
- return { ...b, overlap: Math.max(0, Math.min(w, b.lineH)) };
3367
+ const s = squash(clamp2(col));
3368
+ return {
3369
+ ...b,
3370
+ overlap: UPPER_THIN * (b.lineH * (1 - s) / 2 + Math.min(w, b.lineH) * s)
3371
+ };
3372
+ },
3373
+ line(col) {
3374
+ if (col >= x0 && col <= x1) {
3375
+ const b = base(col);
3376
+ return { bottom: b.Tu, top: b.lineTop, squash: squash(col) };
3377
+ }
3378
+ const runs = upperCols.get(col);
3379
+ if (runs === void 0) return void 0;
3380
+ return {
3381
+ bottom: edgesOf(upper, runs[runs.length - 1]).bottom,
3382
+ top: edgesOf(upper, runs[0]).top,
3383
+ squash: FLICK_SQUASH
3384
+ };
3342
3385
  }
3343
3386
  };
3344
3387
  }
3345
3388
  function mouthFold(role, opening) {
3389
+ const { x0, x1 } = opening;
3390
+ const stackThin = (c, y) => y > c.Tu ? c.Tu + UPPER_THIN * (y - c.Tu) : y;
3346
3391
  return (col, y, v) => {
3347
- if (v === 1) return 0;
3348
- if (role !== "mouth_inner" && (col < opening.x0 || col > opening.x1))
3349
- return 0;
3350
- const c = opening.at(col);
3351
3392
  const k = 1 - v;
3393
+ const inside = col >= x0 && col <= x1;
3352
3394
  switch (role) {
3353
- case "lip_upper":
3354
- return (c.seam - c.Tu) * k;
3355
- case "lip_lower":
3356
- return (c.seam + c.overlap - c.Bl) * k;
3357
- case "mouth_inner":
3358
- return (c.seam + c.overlap - y) * k;
3395
+ case "lip_lower": {
3396
+ if (!inside) return [0, 0];
3397
+ const c = opening.at(col);
3398
+ return [0, (c.seam + c.overlap - c.Bl) * k];
3399
+ }
3400
+ case "mouth_inner": {
3401
+ const c = opening.at(col);
3402
+ const S = c.seam + c.overlap;
3403
+ return [0, S + v * (stackThin(c, y) - S) - y];
3404
+ }
3405
+ case "mouth_teeth": {
3406
+ const c = opening.at(col);
3407
+ return [0, stackThin(c, y) - y + (c.seam - c.Tu) * k];
3408
+ }
3409
+ case "mouth_tongue": {
3410
+ const c = opening.at(col);
3411
+ return [0, (c.seam + c.overlap - c.Bb) * k];
3412
+ }
3413
+ case "lip_upper": {
3414
+ let c = col;
3415
+ let line = opening.line(c);
3416
+ while (line === void 0)
3417
+ line = opening.line(c += col < x0 ? 1 : -1);
3418
+ const thin = line.bottom + UPPER_THIN * (y - line.bottom);
3419
+ const mid = line.bottom + UPPER_THIN * (line.top - line.bottom) / 2;
3420
+ const travel = inside ? opening.at(col).seam - line.bottom : 0;
3421
+ const shut = mid + line.squash * (thin - mid) + travel;
3422
+ const pull = inside ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
3423
+ return [pull * k, thin - y + (shut - thin) * k];
3424
+ }
3359
3425
  }
3360
3426
  };
3361
3427
  }
3362
- function columnOfKnot(knot, opening, role) {
3428
+ function columnOfKnot(knot, opening) {
3363
3429
  const boundary = Math.round(knot + opening.canvasW / 2);
3364
- const col = boundary === opening.x1 + 1 ? opening.x1 : boundary;
3365
- if (col >= opening.x0 && col <= opening.x1) return col;
3366
- return role === "mouth_inner" ? Math.min(opening.x1, Math.max(opening.x0, col)) : void 0;
3430
+ return boundary === opening.x1 + 1 ? opening.x1 : boundary;
3367
3431
  }
3368
3432
  function mouthFoldWarp(role, mesh, xs, box, opening) {
3369
3433
  const fold = mouthFold(role, opening);
3370
- return localWarp(import_format7.StandardParameter.MouthOpen, mesh, box, [0, 1], (p, v, i) => {
3371
- const col = columnOfKnot(xs[i % xs.length], opening, role);
3372
- return [0, col === void 0 ? 0 : fold(col, p[1], v)];
3373
- });
3434
+ return localWarp(
3435
+ import_format7.StandardParameter.MouthOpen,
3436
+ mesh,
3437
+ box,
3438
+ [0, 1],
3439
+ (p, v, i) => fold(columnOfKnot(xs[i % xs.length], opening), p[1], v)
3440
+ );
3374
3441
  }
3375
3442
  function mouthRestShift(byRole) {
3376
3443
  const rig = buildMouthRig(byRole);
@@ -3383,18 +3450,31 @@ function mouthRestShift(byRole) {
3383
3450
  fields.set(role, {
3384
3451
  box,
3385
3452
  xs,
3386
- dy: xs.map((_, i) => offsets[2 * i + 1] * bh(box))
3453
+ rows: offsets.length / 2 / xs.length - 1,
3454
+ dy: offsets.filter((_, i) => i % 2 === 1).map((o) => o * bh(box))
3387
3455
  });
3388
3456
  }
3389
- const half = rig.opening.canvasW / 2;
3390
- return (role, col) => {
3391
- const { box, xs, dy } = fields.get(role);
3392
- const x = col + 0.5 - half;
3393
- if (x < box.x0 || x > box.x1) return 0;
3457
+ const { canvasW, canvasH } = byRole.get("lip_upper");
3458
+ return (role, col, row) => {
3459
+ const { box, xs, rows, dy } = fields.get(role);
3460
+ const x = col + 0.5 - canvasW / 2;
3461
+ const y = canvasH / 2 - (row + 0.5);
3462
+ if (x < box.x0 || x > box.x1 || y < box.y0 || y > box.y1) return void 0;
3394
3463
  let k = 1;
3395
3464
  while (k < xs.length - 1 && xs[k] < x) k++;
3396
- const t = (x - xs[k - 1]) / (xs[k] - xs[k - 1]);
3397
- return dy[k - 1] + (dy[k] - dy[k - 1]) * t;
3465
+ const down = (box.y1 - y) / bh(box) * rows;
3466
+ const r = Math.min(rows - 1, Math.floor(down));
3467
+ const fx = (x - xs[k - 1]) / (xs[k] - xs[k - 1]);
3468
+ const fy = down - r;
3469
+ const at = (i, j) => dy[i * xs.length + j];
3470
+ const tl = at(r, k - 1);
3471
+ const br = at(r + 1, k);
3472
+ if (fx <= fy) {
3473
+ const bl = at(r + 1, k - 1);
3474
+ return tl + fy * (bl - tl) + fx * (br - bl);
3475
+ }
3476
+ const tr = at(r, k);
3477
+ return tl + fx * (tr - tl) + fy * (br - tr);
3398
3478
  };
3399
3479
  }
3400
3480
  function mouthKnots(union, opening) {
@@ -5983,7 +6063,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
5983
6063
  headStart = fm.headStart;
5984
6064
  } else if (spec.role === "hair_front") {
5985
6065
  mesh = columnMesh(b, frontGrid);
5986
- } else if (lips !== void 0 && isLipRole(spec.role)) {
6066
+ } else if (lips !== void 0 && (isLipRole(spec.role) || isLipInsideRole(spec.role))) {
5987
6067
  ({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
5988
6068
  } else if (spec.family !== "body") {
5989
6069
  const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
@@ -6100,6 +6180,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
6100
6180
  extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
6101
6181
  break;
6102
6182
  case "mouth_inner":
6183
+ case "mouth_tongue":
6184
+ case "mouth_teeth":
6103
6185
  case "lip_lower":
6104
6186
  case "lip_upper":
6105
6187
  warps.push(
@@ -6107,6 +6189,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
6107
6189
  mouthForm(mesh, b, lips.frame),
6108
6190
  mouthWiden(mesh, b, lips.frame)
6109
6191
  );
6192
+ if (isLipInsideRole(spec.role)) part.clip = { masks: ["mouth_inner"] };
6110
6193
  break;
6111
6194
  case "blush_L":
6112
6195
  case "blush_R":
@@ -6309,6 +6392,7 @@ function declareParameters(roles) {
6309
6392
  DeletePhysicsRig,
6310
6393
  EditorDocument,
6311
6394
  HEAD_BAND,
6395
+ LIP_INSIDE_ROLES,
6312
6396
  LIP_ROLES,
6313
6397
  LayerGeometryError,
6314
6398
  SPECK_CORE_FRACTION,
@@ -6345,6 +6429,7 @@ function declareParameters(roles) {
6345
6429
  generateIkiFromLayerSet,
6346
6430
  headHalfOf,
6347
6431
  interpolateGridOffsets,
6432
+ isLipInsideRole,
6348
6433
  isSpeckCore,
6349
6434
  mouthFold,
6350
6435
  mouthOpening,