@ikijs/editor 0.22.0 → 0.24.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,
@@ -1843,7 +1845,10 @@ function captureBindingEndpoint(channel, restValue, posedValue) {
1843
1845
  // src/atlas.ts
1844
1846
  var ATLAS_PADDING = 2;
1845
1847
  var UV_INSET_PX = 0.5;
1846
- function packAtlas(sources, padding = ATLAS_PADDING) {
1848
+ function packAtlas(sources, padding = ATLAS_PADDING, maxSide) {
1849
+ if (maxSide !== void 0 && !(isFinite(maxSide) && maxSide > 0)) {
1850
+ throw new Error(`packAtlas: invalid maxSide ${maxSide}`);
1851
+ }
1847
1852
  for (const src of sources) {
1848
1853
  if (!isFinite(src.width) || src.width <= 0) {
1849
1854
  throw new Error(
@@ -1864,42 +1869,50 @@ function packAtlas(sources, padding = ATLAS_PADDING) {
1864
1869
  (sum, s) => sum + (s.width + padding) * (s.height + padding),
1865
1870
  0
1866
1871
  );
1867
- const targetWidth = Math.max(
1868
- Math.ceil(Math.sqrt(totalArea)),
1869
- // Ensure at least the widest single source fits.
1870
- Math.max(...sorted.map((s) => s.width + padding))
1871
- );
1872
- const placements = [];
1873
- let shelfX = 0;
1874
- let shelfY = 0;
1875
- let shelfHeight = 0;
1876
- for (const src of sorted) {
1877
- const paddedW = src.width + padding;
1878
- const paddedH = src.height + padding;
1879
- if (shelfX > 0 && shelfX + paddedW > targetWidth) {
1880
- shelfY += shelfHeight;
1881
- shelfX = 0;
1882
- shelfHeight = 0;
1883
- }
1884
- placements.push({
1885
- id: src.id,
1886
- x: shelfX,
1887
- y: shelfY,
1888
- width: src.width,
1889
- height: src.height
1890
- });
1891
- shelfX += paddedW;
1892
- if (paddedH > shelfHeight) shelfHeight = paddedH;
1872
+ const widest = Math.max(...sorted.map((s) => s.width + padding));
1873
+ const firstWidth = Math.max(Math.ceil(Math.sqrt(totalArea)), widest);
1874
+ const shelve = (targetWidth) => {
1875
+ const placements = [];
1876
+ let shelfX = 0;
1877
+ let shelfY = 0;
1878
+ let shelfHeight = 0;
1879
+ for (const src of sorted) {
1880
+ const paddedW = src.width + padding;
1881
+ const paddedH = src.height + padding;
1882
+ if (shelfX > 0 && shelfX + paddedW > targetWidth) {
1883
+ shelfY += shelfHeight;
1884
+ shelfX = 0;
1885
+ shelfHeight = 0;
1886
+ }
1887
+ placements.push({
1888
+ id: src.id,
1889
+ x: shelfX,
1890
+ y: shelfY,
1891
+ width: src.width,
1892
+ height: src.height
1893
+ });
1894
+ shelfX += paddedW;
1895
+ if (paddedH > shelfHeight) shelfHeight = paddedH;
1896
+ }
1897
+ let pageWidth = 0;
1898
+ let pageHeight = 0;
1899
+ for (const p of placements) {
1900
+ const right = p.x + p.width + padding;
1901
+ const bottom = p.y + p.height + padding;
1902
+ if (right > pageWidth) pageWidth = right;
1903
+ if (bottom > pageHeight) pageHeight = bottom;
1904
+ }
1905
+ return { pageWidth, pageHeight, placements };
1906
+ };
1907
+ const first = shelve(firstWidth);
1908
+ if (maxSide === void 0 || first.pageHeight <= maxSide || first.pageWidth > maxSide) {
1909
+ return { ...first, padding };
1893
1910
  }
1894
- let pageWidth = 0;
1895
- let pageHeight = 0;
1896
- for (const p of placements) {
1897
- const right = p.x + p.width + padding;
1898
- const bottom = p.y + p.height + padding;
1899
- if (right > pageWidth) pageWidth = right;
1900
- if (bottom > pageHeight) pageHeight = bottom;
1911
+ for (let w = widest; w <= maxSide; w++) {
1912
+ const layout = shelve(w);
1913
+ if (layout.pageHeight <= maxSide) return { ...layout, padding };
1901
1914
  }
1902
- return { pageWidth, pageHeight, placements, padding };
1915
+ return { ...first, padding };
1903
1916
  }
1904
1917
  function uvRectFor(placement, page, insetPx = UV_INSET_PX) {
1905
1918
  const px = placement.x + insetPx;
@@ -3002,14 +3015,14 @@ function armParts(role, layer, g, order, poseSwitch) {
3002
3015
  },
3003
3016
  true
3004
3017
  ),
3005
- part(role, ids.armDeformer, order + 1, meshOf(band(0.5, seam), quad)),
3006
3018
  part(
3007
3019
  ids.forearm,
3008
3020
  ids.forearmDeformer,
3009
- order + 2,
3021
+ order + 1,
3010
3022
  meshOf(band(seam, -0.5), quad),
3011
3023
  true
3012
- )
3024
+ ),
3025
+ part(role, ids.armDeformer, order + 2, meshOf(band(0.5, seam), quad))
3013
3026
  ];
3014
3027
  }
3015
3028
  var rotateBinding = (parameter, range, sign) => ({
@@ -3098,21 +3111,26 @@ var import_format7 = require("@ikijs/format");
3098
3111
  // src/auto-rig/roles.ts
3099
3112
  var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
3100
3113
  var isLipRole = (role) => LIP_ROLES.includes(role);
3114
+ var LIP_INSIDE_ROLES = ["mouth_tongue", "mouth_teeth"];
3115
+ var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
3101
3116
  var ROLE_TABLE = [
3102
3117
  { role: "hair_back", family: "hair_back" },
3103
3118
  { role: "body", family: "body" },
3104
- { role: "arm_L", family: "arm", parts: ["elbow_L", "arm_L", "forearm_L"] },
3105
- { role: "arm_R", family: "arm", parts: ["elbow_R", "arm_R", "forearm_R"] },
3119
+ { role: "arm_L", family: "arm", parts: ["elbow_L", "forearm_L", "arm_L"] },
3120
+ { role: "arm_R", family: "arm", parts: ["elbow_R", "forearm_R", "arm_R"] },
3106
3121
  { role: "face", family: "face", required: true },
3107
3122
  { role: "blush_L", family: "face" },
3108
3123
  { role: "blush_R", family: "face" },
3109
3124
  { role: "nose", family: "nose" },
3110
3125
  { role: "mouth", family: "mouth" },
3111
3126
  { 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.
3127
+ // The lip set, back to front: the interior, the tongue and the teeth clipped
3128
+ // to it, the lower lip's skin, the upper lip and its line over both. It
3129
+ // folds open instead of crossfading (`mouth.ts`), so it stands in for
3130
+ // `mouth`, never beside it.
3115
3131
  { role: "mouth_inner", family: "mouth" },
3132
+ { role: "mouth_tongue", family: "mouth" },
3133
+ { role: "mouth_teeth", family: "mouth" },
3116
3134
  { role: "lip_lower", family: "mouth" },
3117
3135
  { role: "lip_upper", family: "mouth" },
3118
3136
  { role: "eye_L", family: "eye_L", required: true },
@@ -3175,6 +3193,12 @@ function checkArmsHaveBody(roles) {
3175
3193
  function checkMouth(roles) {
3176
3194
  const has = new Set(roles);
3177
3195
  const present = LIP_ROLES.filter((r) => has.has(r));
3196
+ const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
3197
+ if (inside.length > 0 && present.length === 0) {
3198
+ throw new Error(
3199
+ `auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
3200
+ );
3201
+ }
3178
3202
  if (present.length === 0) {
3179
3203
  if (!has.has("mouth")) {
3180
3204
  throw new Error(
@@ -3238,6 +3262,12 @@ function parseLayerRoles(fileNames) {
3238
3262
 
3239
3263
  // src/auto-rig/mouth.ts
3240
3264
  var UPPER_SHARE = 0.4;
3265
+ var CLOSED_SAG_MAX = 0.1;
3266
+ var UPPER_THIN = 0.7;
3267
+ var END_TAPER = 0.35;
3268
+ var END_TAPER_FROM = 0.7;
3269
+ var FLICK_PULL = 0.5;
3270
+ var FLICK_SQUASH = 0.25;
3241
3271
  var MOUTH_KNOT_PX = 4;
3242
3272
  var isLipSet = (byRole) => LIP_ROLES.every((r) => byRole.has(r));
3243
3273
  function columnRuns(layer) {
@@ -3299,10 +3329,15 @@ function mouthOpening(byRole) {
3299
3329
  for (let d = 0; (r = read(c - d) ?? read(c + d)) === void 0; d++) ;
3300
3330
  reads.push(r);
3301
3331
  }
3332
+ const n = reads.length;
3302
3333
  const m = (x0 + x1) / 2;
3303
- let p = 0;
3304
- let q = 0;
3305
- if (reads.length >= 4) {
3334
+ const centre = Math.round(m);
3335
+ const smooth = reads.map((_, i) => {
3336
+ const near = reads.slice(Math.max(0, i - 3), i + 4);
3337
+ return near.reduce((s, r) => s + r.Tu, 0) / near.length;
3338
+ });
3339
+ let sag = 0;
3340
+ if (n >= 4) {
3306
3341
  let s11 = 0;
3307
3342
  let s12 = 0;
3308
3343
  let s22 = 0;
@@ -3320,16 +3355,32 @@ function mouthOpening(byRole) {
3320
3355
  t2 += b2 * d;
3321
3356
  });
3322
3357
  const det = s11 * s22 - s12 * s12;
3323
- p = (t1 * s22 - t2 * s12) / det;
3324
- q = (s11 * t2 - s12 * t1) / det;
3358
+ const p = (t1 * s22 - t2 * s12) / det;
3359
+ const q = (s11 * t2 - s12 * t1) / det;
3360
+ const c = centre - x0;
3361
+ const D = c * (centre - x1) * (p + q * (centre - m));
3362
+ const arc = (reads[0].Tu + reads[n - 1].Tu) / 2 - reads[c].Tu;
3363
+ const t = c / (n - 1);
3364
+ const k = 4 * t * (1 - t);
3365
+ const offset = arc - (smooth[0] + (smooth[n - 1] - smooth[0]) * t - smooth[c]);
3366
+ sag = Math.min(arc - D, CLOSED_SAG_MAX * n - Math.max(0, offset) / k);
3325
3367
  }
3326
3368
  const columns = reads.map((r, i) => {
3327
- const x = x0 + i;
3328
- return { ...r, seam: r.Tu + (x - x0) * (x - x1) * (p + q * (x - m)) };
3369
+ if (n < 4) return { ...r, seam: r.Tu };
3370
+ const t = i / (x1 - x0);
3371
+ const parabola = smooth[0] + (smooth[n - 1] - smooth[0]) * t - sag * 4 * t * (1 - t);
3372
+ return { ...r, seam: r.Tu + parabola - smooth[i] };
3329
3373
  });
3330
- const base = (col) => columns[Math.min(x1, Math.max(x0, col)) - x0];
3331
- const centre = Math.round((x0 + x1) / 2);
3374
+ const clamp2 = (col) => Math.min(x1, Math.max(x0, col));
3375
+ const base = (col) => columns[clamp2(col) - x0];
3332
3376
  const w = base(centre).lineH;
3377
+ const from = END_TAPER_FROM * ((x1 - x0 + 1) / 2);
3378
+ const squash = (col) => {
3379
+ const d = Math.abs(col - centre);
3380
+ const edge = col < centre ? centre - upper.bbox.x : upper.bbox.x + upper.bbox.w - 1 - centre;
3381
+ const t = d <= from ? 0 : d >= edge ? 1 : (d - from) / (edge - from);
3382
+ return 1 - (1 - END_TAPER) * t;
3383
+ };
3333
3384
  return {
3334
3385
  x0,
3335
3386
  x1,
@@ -3338,39 +3389,80 @@ function mouthOpening(byRole) {
3338
3389
  w,
3339
3390
  at(col) {
3340
3391
  const b = base(col);
3341
- return { ...b, overlap: Math.max(0, Math.min(w, b.lineH)) };
3392
+ const s = squash(clamp2(col));
3393
+ return {
3394
+ ...b,
3395
+ overlap: UPPER_THIN * (b.lineH * (1 - s) / 2 + Math.min(w, b.lineH) * s)
3396
+ };
3397
+ },
3398
+ line(col) {
3399
+ if (col >= x0 && col <= x1) {
3400
+ const b = base(col);
3401
+ return { bottom: b.Tu, top: b.lineTop, squash: squash(col) };
3402
+ }
3403
+ const runs = upperCols.get(col);
3404
+ if (runs === void 0) return void 0;
3405
+ return {
3406
+ bottom: edgesOf(upper, runs[runs.length - 1]).bottom,
3407
+ top: edgesOf(upper, runs[0]).top,
3408
+ squash: FLICK_SQUASH
3409
+ };
3342
3410
  }
3343
3411
  };
3344
3412
  }
3345
3413
  function mouthFold(role, opening) {
3414
+ const { x0, x1 } = opening;
3415
+ const stackThin = (c, y) => y > c.Tu ? c.Tu + UPPER_THIN * (y - c.Tu) : y;
3346
3416
  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
3417
  const k = 1 - v;
3418
+ const inside = col >= x0 && col <= x1;
3352
3419
  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;
3420
+ case "lip_lower": {
3421
+ if (!inside) return [0, 0];
3422
+ const c = opening.at(col);
3423
+ return [0, (c.seam + c.overlap - c.Bl) * k];
3424
+ }
3425
+ case "mouth_inner": {
3426
+ const c = opening.at(col);
3427
+ const S = c.seam + c.overlap;
3428
+ return [0, S + v * (stackThin(c, y) - S) - y];
3429
+ }
3430
+ case "mouth_teeth": {
3431
+ const c = opening.at(col);
3432
+ return [0, stackThin(c, y) - y + (c.seam - c.Tu) * k];
3433
+ }
3434
+ case "mouth_tongue": {
3435
+ const c = opening.at(col);
3436
+ return [0, (c.seam + c.overlap - c.Bb) * k];
3437
+ }
3438
+ case "lip_upper": {
3439
+ let c = col;
3440
+ let line = opening.line(c);
3441
+ while (line === void 0)
3442
+ line = opening.line(c += col < x0 ? 1 : -1);
3443
+ const thin = line.bottom + UPPER_THIN * (y - line.bottom);
3444
+ const mid = line.bottom + UPPER_THIN * (line.top - line.bottom) / 2;
3445
+ const travel = inside ? opening.at(col).seam - line.bottom : 0;
3446
+ const shut = mid + line.squash * (thin - mid) + travel;
3447
+ const pull = inside ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
3448
+ return [pull * k, thin - y + (shut - thin) * k];
3449
+ }
3359
3450
  }
3360
3451
  };
3361
3452
  }
3362
- function columnOfKnot(knot, opening, role) {
3453
+ function columnOfKnot(knot, opening) {
3363
3454
  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;
3455
+ return boundary === opening.x1 + 1 ? opening.x1 : boundary;
3367
3456
  }
3368
3457
  function mouthFoldWarp(role, mesh, xs, box, opening) {
3369
3458
  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
- });
3459
+ return localWarp(
3460
+ import_format7.StandardParameter.MouthOpen,
3461
+ mesh,
3462
+ box,
3463
+ [0, 1],
3464
+ (p, v, i) => fold(columnOfKnot(xs[i % xs.length], opening), p[1], v)
3465
+ );
3374
3466
  }
3375
3467
  function mouthRestShift(byRole) {
3376
3468
  const rig = buildMouthRig(byRole);
@@ -3383,18 +3475,31 @@ function mouthRestShift(byRole) {
3383
3475
  fields.set(role, {
3384
3476
  box,
3385
3477
  xs,
3386
- dy: xs.map((_, i) => offsets[2 * i + 1] * bh(box))
3478
+ rows: offsets.length / 2 / xs.length - 1,
3479
+ dy: offsets.filter((_, i) => i % 2 === 1).map((o) => o * bh(box))
3387
3480
  });
3388
3481
  }
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;
3482
+ const { canvasW, canvasH } = byRole.get("lip_upper");
3483
+ return (role, col, row) => {
3484
+ const { box, xs, rows, dy } = fields.get(role);
3485
+ const x = col + 0.5 - canvasW / 2;
3486
+ const y = canvasH / 2 - (row + 0.5);
3487
+ if (x < box.x0 || x > box.x1 || y < box.y0 || y > box.y1) return void 0;
3394
3488
  let k = 1;
3395
3489
  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;
3490
+ const down = (box.y1 - y) / bh(box) * rows;
3491
+ const r = Math.min(rows - 1, Math.floor(down));
3492
+ const fx = (x - xs[k - 1]) / (xs[k] - xs[k - 1]);
3493
+ const fy = down - r;
3494
+ const at = (i, j) => dy[i * xs.length + j];
3495
+ const tl = at(r, k - 1);
3496
+ const br = at(r + 1, k);
3497
+ if (fx <= fy) {
3498
+ const bl = at(r + 1, k - 1);
3499
+ return tl + fy * (bl - tl) + fx * (br - bl);
3500
+ }
3501
+ const tr = at(r, k);
3502
+ return tl + fx * (tr - tl) + fy * (br - tr);
3398
3503
  };
3399
3504
  }
3400
3505
  function mouthKnots(union, opening) {
@@ -5983,7 +6088,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
5983
6088
  headStart = fm.headStart;
5984
6089
  } else if (spec.role === "hair_front") {
5985
6090
  mesh = columnMesh(b, frontGrid);
5986
- } else if (lips !== void 0 && isLipRole(spec.role)) {
6091
+ } else if (lips !== void 0 && (isLipRole(spec.role) || isLipInsideRole(spec.role))) {
5987
6092
  ({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
5988
6093
  } else if (spec.family !== "body") {
5989
6094
  const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
@@ -6100,6 +6205,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
6100
6205
  extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
6101
6206
  break;
6102
6207
  case "mouth_inner":
6208
+ case "mouth_tongue":
6209
+ case "mouth_teeth":
6103
6210
  case "lip_lower":
6104
6211
  case "lip_upper":
6105
6212
  warps.push(
@@ -6107,6 +6214,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
6107
6214
  mouthForm(mesh, b, lips.frame),
6108
6215
  mouthWiden(mesh, b, lips.frame)
6109
6216
  );
6217
+ if (isLipInsideRole(spec.role)) part.clip = { masks: ["mouth_inner"] };
6110
6218
  break;
6111
6219
  case "blush_L":
6112
6220
  case "blush_R":
@@ -6309,6 +6417,7 @@ function declareParameters(roles) {
6309
6417
  DeletePhysicsRig,
6310
6418
  EditorDocument,
6311
6419
  HEAD_BAND,
6420
+ LIP_INSIDE_ROLES,
6312
6421
  LIP_ROLES,
6313
6422
  LayerGeometryError,
6314
6423
  SPECK_CORE_FRACTION,
@@ -6345,6 +6454,7 @@ function declareParameters(roles) {
6345
6454
  generateIkiFromLayerSet,
6346
6455
  headHalfOf,
6347
6456
  interpolateGridOffsets,
6457
+ isLipInsideRole,
6348
6458
  isSpeckCore,
6349
6459
  mouthFold,
6350
6460
  mouthOpening,