@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.mjs CHANGED
@@ -1761,7 +1761,10 @@ function captureBindingEndpoint(channel, restValue, posedValue) {
1761
1761
  // src/atlas.ts
1762
1762
  var ATLAS_PADDING = 2;
1763
1763
  var UV_INSET_PX = 0.5;
1764
- function packAtlas(sources, padding = ATLAS_PADDING) {
1764
+ function packAtlas(sources, padding = ATLAS_PADDING, maxSide) {
1765
+ if (maxSide !== void 0 && !(isFinite(maxSide) && maxSide > 0)) {
1766
+ throw new Error(`packAtlas: invalid maxSide ${maxSide}`);
1767
+ }
1765
1768
  for (const src of sources) {
1766
1769
  if (!isFinite(src.width) || src.width <= 0) {
1767
1770
  throw new Error(
@@ -1782,42 +1785,50 @@ function packAtlas(sources, padding = ATLAS_PADDING) {
1782
1785
  (sum, s) => sum + (s.width + padding) * (s.height + padding),
1783
1786
  0
1784
1787
  );
1785
- const targetWidth = Math.max(
1786
- Math.ceil(Math.sqrt(totalArea)),
1787
- // Ensure at least the widest single source fits.
1788
- Math.max(...sorted.map((s) => s.width + padding))
1789
- );
1790
- const placements = [];
1791
- let shelfX = 0;
1792
- let shelfY = 0;
1793
- let shelfHeight = 0;
1794
- for (const src of sorted) {
1795
- const paddedW = src.width + padding;
1796
- const paddedH = src.height + padding;
1797
- if (shelfX > 0 && shelfX + paddedW > targetWidth) {
1798
- shelfY += shelfHeight;
1799
- shelfX = 0;
1800
- shelfHeight = 0;
1801
- }
1802
- placements.push({
1803
- id: src.id,
1804
- x: shelfX,
1805
- y: shelfY,
1806
- width: src.width,
1807
- height: src.height
1808
- });
1809
- shelfX += paddedW;
1810
- if (paddedH > shelfHeight) shelfHeight = paddedH;
1788
+ const widest = Math.max(...sorted.map((s) => s.width + padding));
1789
+ const firstWidth = Math.max(Math.ceil(Math.sqrt(totalArea)), widest);
1790
+ const shelve = (targetWidth) => {
1791
+ const placements = [];
1792
+ let shelfX = 0;
1793
+ let shelfY = 0;
1794
+ let shelfHeight = 0;
1795
+ for (const src of sorted) {
1796
+ const paddedW = src.width + padding;
1797
+ const paddedH = src.height + padding;
1798
+ if (shelfX > 0 && shelfX + paddedW > targetWidth) {
1799
+ shelfY += shelfHeight;
1800
+ shelfX = 0;
1801
+ shelfHeight = 0;
1802
+ }
1803
+ placements.push({
1804
+ id: src.id,
1805
+ x: shelfX,
1806
+ y: shelfY,
1807
+ width: src.width,
1808
+ height: src.height
1809
+ });
1810
+ shelfX += paddedW;
1811
+ if (paddedH > shelfHeight) shelfHeight = paddedH;
1812
+ }
1813
+ let pageWidth = 0;
1814
+ let pageHeight = 0;
1815
+ for (const p of placements) {
1816
+ const right = p.x + p.width + padding;
1817
+ const bottom = p.y + p.height + padding;
1818
+ if (right > pageWidth) pageWidth = right;
1819
+ if (bottom > pageHeight) pageHeight = bottom;
1820
+ }
1821
+ return { pageWidth, pageHeight, placements };
1822
+ };
1823
+ const first = shelve(firstWidth);
1824
+ if (maxSide === void 0 || first.pageHeight <= maxSide || first.pageWidth > maxSide) {
1825
+ return { ...first, padding };
1811
1826
  }
1812
- let pageWidth = 0;
1813
- let pageHeight = 0;
1814
- for (const p of placements) {
1815
- const right = p.x + p.width + padding;
1816
- const bottom = p.y + p.height + padding;
1817
- if (right > pageWidth) pageWidth = right;
1818
- if (bottom > pageHeight) pageHeight = bottom;
1827
+ for (let w = widest; w <= maxSide; w++) {
1828
+ const layout = shelve(w);
1829
+ if (layout.pageHeight <= maxSide) return { ...layout, padding };
1819
1830
  }
1820
- return { pageWidth, pageHeight, placements, padding };
1831
+ return { ...first, padding };
1821
1832
  }
1822
1833
  function uvRectFor(placement, page, insetPx = UV_INSET_PX) {
1823
1834
  const px = placement.x + insetPx;
@@ -2926,14 +2937,14 @@ function armParts(role, layer, g, order, poseSwitch) {
2926
2937
  },
2927
2938
  true
2928
2939
  ),
2929
- part(role, ids.armDeformer, order + 1, meshOf(band(0.5, seam), quad)),
2930
2940
  part(
2931
2941
  ids.forearm,
2932
2942
  ids.forearmDeformer,
2933
- order + 2,
2943
+ order + 1,
2934
2944
  meshOf(band(seam, -0.5), quad),
2935
2945
  true
2936
- )
2946
+ ),
2947
+ part(role, ids.armDeformer, order + 2, meshOf(band(0.5, seam), quad))
2937
2948
  ];
2938
2949
  }
2939
2950
  var rotateBinding = (parameter, range, sign) => ({
@@ -3026,21 +3037,26 @@ import {
3026
3037
  // src/auto-rig/roles.ts
3027
3038
  var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
3028
3039
  var isLipRole = (role) => LIP_ROLES.includes(role);
3040
+ var LIP_INSIDE_ROLES = ["mouth_tongue", "mouth_teeth"];
3041
+ var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
3029
3042
  var ROLE_TABLE = [
3030
3043
  { role: "hair_back", family: "hair_back" },
3031
3044
  { role: "body", family: "body" },
3032
- { role: "arm_L", family: "arm", parts: ["elbow_L", "arm_L", "forearm_L"] },
3033
- { role: "arm_R", family: "arm", parts: ["elbow_R", "arm_R", "forearm_R"] },
3045
+ { role: "arm_L", family: "arm", parts: ["elbow_L", "forearm_L", "arm_L"] },
3046
+ { role: "arm_R", family: "arm", parts: ["elbow_R", "forearm_R", "arm_R"] },
3034
3047
  { role: "face", family: "face", required: true },
3035
3048
  { role: "blush_L", family: "face" },
3036
3049
  { role: "blush_R", family: "face" },
3037
3050
  { role: "nose", family: "nose" },
3038
3051
  { role: "mouth", family: "mouth" },
3039
3052
  { 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.
3053
+ // The lip set, back to front: the interior, the tongue and the teeth clipped
3054
+ // to it, the lower lip's skin, the upper lip and its line over both. It
3055
+ // folds open instead of crossfading (`mouth.ts`), so it stands in for
3056
+ // `mouth`, never beside it.
3043
3057
  { role: "mouth_inner", family: "mouth" },
3058
+ { role: "mouth_tongue", family: "mouth" },
3059
+ { role: "mouth_teeth", family: "mouth" },
3044
3060
  { role: "lip_lower", family: "mouth" },
3045
3061
  { role: "lip_upper", family: "mouth" },
3046
3062
  { role: "eye_L", family: "eye_L", required: true },
@@ -3103,6 +3119,12 @@ function checkArmsHaveBody(roles) {
3103
3119
  function checkMouth(roles) {
3104
3120
  const has = new Set(roles);
3105
3121
  const present = LIP_ROLES.filter((r) => has.has(r));
3122
+ const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
3123
+ if (inside.length > 0 && present.length === 0) {
3124
+ throw new Error(
3125
+ `auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
3126
+ );
3127
+ }
3106
3128
  if (present.length === 0) {
3107
3129
  if (!has.has("mouth")) {
3108
3130
  throw new Error(
@@ -3166,6 +3188,12 @@ function parseLayerRoles(fileNames) {
3166
3188
 
3167
3189
  // src/auto-rig/mouth.ts
3168
3190
  var UPPER_SHARE = 0.4;
3191
+ var CLOSED_SAG_MAX = 0.1;
3192
+ var UPPER_THIN = 0.7;
3193
+ var END_TAPER = 0.35;
3194
+ var END_TAPER_FROM = 0.7;
3195
+ var FLICK_PULL = 0.5;
3196
+ var FLICK_SQUASH = 0.25;
3169
3197
  var MOUTH_KNOT_PX = 4;
3170
3198
  var isLipSet = (byRole) => LIP_ROLES.every((r) => byRole.has(r));
3171
3199
  function columnRuns(layer) {
@@ -3227,10 +3255,15 @@ function mouthOpening(byRole) {
3227
3255
  for (let d = 0; (r = read(c - d) ?? read(c + d)) === void 0; d++) ;
3228
3256
  reads.push(r);
3229
3257
  }
3258
+ const n = reads.length;
3230
3259
  const m = (x0 + x1) / 2;
3231
- let p = 0;
3232
- let q = 0;
3233
- if (reads.length >= 4) {
3260
+ const centre = Math.round(m);
3261
+ const smooth = reads.map((_, i) => {
3262
+ const near = reads.slice(Math.max(0, i - 3), i + 4);
3263
+ return near.reduce((s, r) => s + r.Tu, 0) / near.length;
3264
+ });
3265
+ let sag = 0;
3266
+ if (n >= 4) {
3234
3267
  let s11 = 0;
3235
3268
  let s12 = 0;
3236
3269
  let s22 = 0;
@@ -3248,16 +3281,32 @@ function mouthOpening(byRole) {
3248
3281
  t2 += b2 * d;
3249
3282
  });
3250
3283
  const det = s11 * s22 - s12 * s12;
3251
- p = (t1 * s22 - t2 * s12) / det;
3252
- q = (s11 * t2 - s12 * t1) / det;
3284
+ const p = (t1 * s22 - t2 * s12) / det;
3285
+ const q = (s11 * t2 - s12 * t1) / det;
3286
+ const c = centre - x0;
3287
+ const D = c * (centre - x1) * (p + q * (centre - m));
3288
+ const arc = (reads[0].Tu + reads[n - 1].Tu) / 2 - reads[c].Tu;
3289
+ const t = c / (n - 1);
3290
+ const k = 4 * t * (1 - t);
3291
+ const offset = arc - (smooth[0] + (smooth[n - 1] - smooth[0]) * t - smooth[c]);
3292
+ sag = Math.min(arc - D, CLOSED_SAG_MAX * n - Math.max(0, offset) / k);
3253
3293
  }
3254
3294
  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)) };
3295
+ if (n < 4) return { ...r, seam: r.Tu };
3296
+ const t = i / (x1 - x0);
3297
+ const parabola = smooth[0] + (smooth[n - 1] - smooth[0]) * t - sag * 4 * t * (1 - t);
3298
+ return { ...r, seam: r.Tu + parabola - smooth[i] };
3257
3299
  });
3258
- const base = (col) => columns[Math.min(x1, Math.max(x0, col)) - x0];
3259
- const centre = Math.round((x0 + x1) / 2);
3300
+ const clamp2 = (col) => Math.min(x1, Math.max(x0, col));
3301
+ const base = (col) => columns[clamp2(col) - x0];
3260
3302
  const w = base(centre).lineH;
3303
+ const from = END_TAPER_FROM * ((x1 - x0 + 1) / 2);
3304
+ const squash = (col) => {
3305
+ const d = Math.abs(col - centre);
3306
+ const edge = col < centre ? centre - upper.bbox.x : upper.bbox.x + upper.bbox.w - 1 - centre;
3307
+ const t = d <= from ? 0 : d >= edge ? 1 : (d - from) / (edge - from);
3308
+ return 1 - (1 - END_TAPER) * t;
3309
+ };
3261
3310
  return {
3262
3311
  x0,
3263
3312
  x1,
@@ -3266,39 +3315,80 @@ function mouthOpening(byRole) {
3266
3315
  w,
3267
3316
  at(col) {
3268
3317
  const b = base(col);
3269
- return { ...b, overlap: Math.max(0, Math.min(w, b.lineH)) };
3318
+ const s = squash(clamp2(col));
3319
+ return {
3320
+ ...b,
3321
+ overlap: UPPER_THIN * (b.lineH * (1 - s) / 2 + Math.min(w, b.lineH) * s)
3322
+ };
3323
+ },
3324
+ line(col) {
3325
+ if (col >= x0 && col <= x1) {
3326
+ const b = base(col);
3327
+ return { bottom: b.Tu, top: b.lineTop, squash: squash(col) };
3328
+ }
3329
+ const runs = upperCols.get(col);
3330
+ if (runs === void 0) return void 0;
3331
+ return {
3332
+ bottom: edgesOf(upper, runs[runs.length - 1]).bottom,
3333
+ top: edgesOf(upper, runs[0]).top,
3334
+ squash: FLICK_SQUASH
3335
+ };
3270
3336
  }
3271
3337
  };
3272
3338
  }
3273
3339
  function mouthFold(role, opening) {
3340
+ const { x0, x1 } = opening;
3341
+ const stackThin = (c, y) => y > c.Tu ? c.Tu + UPPER_THIN * (y - c.Tu) : y;
3274
3342
  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
3343
  const k = 1 - v;
3344
+ const inside = col >= x0 && col <= x1;
3280
3345
  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;
3346
+ case "lip_lower": {
3347
+ if (!inside) return [0, 0];
3348
+ const c = opening.at(col);
3349
+ return [0, (c.seam + c.overlap - c.Bl) * k];
3350
+ }
3351
+ case "mouth_inner": {
3352
+ const c = opening.at(col);
3353
+ const S = c.seam + c.overlap;
3354
+ return [0, S + v * (stackThin(c, y) - S) - y];
3355
+ }
3356
+ case "mouth_teeth": {
3357
+ const c = opening.at(col);
3358
+ return [0, stackThin(c, y) - y + (c.seam - c.Tu) * k];
3359
+ }
3360
+ case "mouth_tongue": {
3361
+ const c = opening.at(col);
3362
+ return [0, (c.seam + c.overlap - c.Bb) * k];
3363
+ }
3364
+ case "lip_upper": {
3365
+ let c = col;
3366
+ let line = opening.line(c);
3367
+ while (line === void 0)
3368
+ line = opening.line(c += col < x0 ? 1 : -1);
3369
+ const thin = line.bottom + UPPER_THIN * (y - line.bottom);
3370
+ const mid = line.bottom + UPPER_THIN * (line.top - line.bottom) / 2;
3371
+ const travel = inside ? opening.at(col).seam - line.bottom : 0;
3372
+ const shut = mid + line.squash * (thin - mid) + travel;
3373
+ const pull = inside ? 0 : (1 - FLICK_PULL) * ((col < x0 ? x0 : x1 + 1) - col);
3374
+ return [pull * k, thin - y + (shut - thin) * k];
3375
+ }
3287
3376
  }
3288
3377
  };
3289
3378
  }
3290
- function columnOfKnot(knot, opening, role) {
3379
+ function columnOfKnot(knot, opening) {
3291
3380
  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;
3381
+ return boundary === opening.x1 + 1 ? opening.x1 : boundary;
3295
3382
  }
3296
3383
  function mouthFoldWarp(role, mesh, xs, box, opening) {
3297
3384
  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
- });
3385
+ return localWarp(
3386
+ P5.MouthOpen,
3387
+ mesh,
3388
+ box,
3389
+ [0, 1],
3390
+ (p, v, i) => fold(columnOfKnot(xs[i % xs.length], opening), p[1], v)
3391
+ );
3302
3392
  }
3303
3393
  function mouthRestShift(byRole) {
3304
3394
  const rig = buildMouthRig(byRole);
@@ -3311,18 +3401,31 @@ function mouthRestShift(byRole) {
3311
3401
  fields.set(role, {
3312
3402
  box,
3313
3403
  xs,
3314
- dy: xs.map((_, i) => offsets[2 * i + 1] * bh(box))
3404
+ rows: offsets.length / 2 / xs.length - 1,
3405
+ dy: offsets.filter((_, i) => i % 2 === 1).map((o) => o * bh(box))
3315
3406
  });
3316
3407
  }
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;
3408
+ const { canvasW, canvasH } = byRole.get("lip_upper");
3409
+ return (role, col, row) => {
3410
+ const { box, xs, rows, dy } = fields.get(role);
3411
+ const x = col + 0.5 - canvasW / 2;
3412
+ const y = canvasH / 2 - (row + 0.5);
3413
+ if (x < box.x0 || x > box.x1 || y < box.y0 || y > box.y1) return void 0;
3322
3414
  let k = 1;
3323
3415
  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;
3416
+ const down = (box.y1 - y) / bh(box) * rows;
3417
+ const r = Math.min(rows - 1, Math.floor(down));
3418
+ const fx = (x - xs[k - 1]) / (xs[k] - xs[k - 1]);
3419
+ const fy = down - r;
3420
+ const at = (i, j) => dy[i * xs.length + j];
3421
+ const tl = at(r, k - 1);
3422
+ const br = at(r + 1, k);
3423
+ if (fx <= fy) {
3424
+ const bl = at(r + 1, k - 1);
3425
+ return tl + fy * (bl - tl) + fx * (br - bl);
3426
+ }
3427
+ const tr = at(r, k);
3428
+ return tl + fx * (tr - tl) + fy * (br - tr);
3326
3429
  };
3327
3430
  }
3328
3431
  function mouthKnots(union, opening) {
@@ -5917,7 +6020,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
5917
6020
  headStart = fm.headStart;
5918
6021
  } else if (spec.role === "hair_front") {
5919
6022
  mesh = columnMesh(b, frontGrid);
5920
- } else if (lips !== void 0 && isLipRole(spec.role)) {
6023
+ } else if (lips !== void 0 && (isLipRole(spec.role) || isLipInsideRole(spec.role))) {
5921
6024
  ({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
5922
6025
  } else if (spec.family !== "body") {
5923
6026
  const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
@@ -6034,6 +6137,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
6034
6137
  extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
6035
6138
  break;
6036
6139
  case "mouth_inner":
6140
+ case "mouth_tongue":
6141
+ case "mouth_teeth":
6037
6142
  case "lip_lower":
6038
6143
  case "lip_upper":
6039
6144
  warps.push(
@@ -6041,6 +6146,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
6041
6146
  mouthForm(mesh, b, lips.frame),
6042
6147
  mouthWiden(mesh, b, lips.frame)
6043
6148
  );
6149
+ if (isLipInsideRole(spec.role)) part.clip = { masks: ["mouth_inner"] };
6044
6150
  break;
6045
6151
  case "blush_L":
6046
6152
  case "blush_R":
@@ -6242,6 +6348,7 @@ export {
6242
6348
  DeletePhysicsRig,
6243
6349
  EditorDocument,
6244
6350
  HEAD_BAND,
6351
+ LIP_INSIDE_ROLES,
6245
6352
  LIP_ROLES,
6246
6353
  LayerGeometryError,
6247
6354
  SPECK_CORE_FRACTION,
@@ -6278,6 +6385,7 @@ export {
6278
6385
  generateIkiFromLayerSet,
6279
6386
  headHalfOf,
6280
6387
  interpolateGridOffsets,
6388
+ isLipInsideRole,
6281
6389
  isSpeckCore,
6282
6390
  mouthFold,
6283
6391
  mouthOpening,