@ikijs/editor 0.19.0 → 0.21.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
@@ -62,6 +62,7 @@ __export(index_exports, {
62
62
  createLayerSetMeasurer: () => createLayerSetMeasurer,
63
63
  denseCoreOf: () => denseCoreOf,
64
64
  detectAlphaBbox: () => detectAlphaBbox,
65
+ forearmPoseGeometry: () => forearmPoseGeometry,
65
66
  foregroundSpan: () => foregroundSpan,
66
67
  generateIkiFromLayerSet: () => generateIkiFromLayerSet,
67
68
  headHalfOf: () => headHalfOf,
@@ -1641,7 +1642,16 @@ var ROW_RUN_ROLES = /* @__PURE__ */ new Set([
1641
1642
  "hair_back",
1642
1643
  "body",
1643
1644
  "arm_L",
1644
- "arm_R"
1645
+ "arm_R",
1646
+ "forearm_pose_L",
1647
+ "forearm_pose_R",
1648
+ "mouth_inner",
1649
+ "lip_lower",
1650
+ "lip_upper"
1651
+ ]);
1652
+ var REST_HIDDEN_ROLES = /* @__PURE__ */ new Set([
1653
+ "forearm_pose_L",
1654
+ "forearm_pose_R"
1645
1655
  ]);
1646
1656
  function createLayerSetMeasurer(canvas) {
1647
1657
  for (const field of ["width", "height"]) {
@@ -1672,6 +1682,7 @@ function createLayerSetMeasurer(canvas) {
1672
1682
  if (bbox === null) return null;
1673
1683
  const rowLeft = new Int32Array(canvasH).fill(canvasW);
1674
1684
  const rowRight = new Int32Array(canvasH).fill(-1);
1685
+ const hidden = REST_HIDDEN_ROLES.has(role);
1675
1686
  const rowRuns = ROW_RUN_ROLES.has(role) ? [] : void 0;
1676
1687
  for (let y = 0; y < canvasH; y++) {
1677
1688
  let runs;
@@ -1687,14 +1698,14 @@ function createLayerSetMeasurer(canvas) {
1687
1698
  runs.push(x);
1688
1699
  inRun = on;
1689
1700
  }
1690
- if (!on) continue;
1701
+ if (!on || hidden) continue;
1691
1702
  opaque[p] = 1;
1692
1703
  if (x < rowLeft[y]) rowLeft[y] = x;
1693
1704
  if (x > rowRight[y]) rowRight[y] = x;
1694
1705
  }
1695
1706
  if (runs !== void 0 && inRun) runs.push(canvasW);
1696
1707
  }
1697
- rowSpansByRole.push({ role, rowLeft, rowRight });
1708
+ if (!hidden) rowSpansByRole.push({ role, rowLeft, rowRight });
1698
1709
  const layer = {
1699
1710
  role,
1700
1711
  fileName,
@@ -2249,6 +2260,29 @@ function gridMesh(cols, rows) {
2249
2260
  }
2250
2261
  return { vertices, uvs, indices };
2251
2262
  }
2263
+ function columnMesh(b, grid) {
2264
+ const { xs } = grid;
2265
+ const rows = grid.ys.length - 1;
2266
+ const vertices = [];
2267
+ const uvs = [];
2268
+ for (let r = 0; r <= rows; r++) {
2269
+ for (const x of xs) {
2270
+ const u = (x - b.x0) / bw(b);
2271
+ vertices.push(roundTo(u - 0.5, 1e-5), roundTo(0.5 - r / rows, 1e-5));
2272
+ uvs.push(roundTo(u, 1e-5), roundTo(r / rows, 1e-5));
2273
+ }
2274
+ }
2275
+ const n = xs.length;
2276
+ const indices = [];
2277
+ for (let r = 0; r < rows; r++) {
2278
+ for (let c = 0; c + 1 < n; c++) {
2279
+ const tl = r * n + c;
2280
+ const bl = tl + n;
2281
+ indices.push(bl, bl + 1, tl, tl, bl + 1, tl + 1);
2282
+ }
2283
+ }
2284
+ return { vertices, uvs, indices };
2285
+ }
2252
2286
  function meshPoints(mesh, box) {
2253
2287
  const out = [];
2254
2288
  for (let i = 0; i < mesh.vertices.length; i += 2) {
@@ -2372,10 +2406,13 @@ function browBindings(side, reach) {
2372
2406
  ];
2373
2407
  }
2374
2408
  var BROW_TILT = 15;
2375
- function mouthForm(mesh, box) {
2376
- const a = 0.3 * bh(box);
2377
- const c = cx(box);
2378
- const half = bw(box) / 2;
2409
+ var mouthFrameOf = (box) => ({
2410
+ c: cx(box),
2411
+ half: bw(box) / 2,
2412
+ amp: 0.3 * bh(box)
2413
+ });
2414
+ function mouthForm(mesh, box, frame = mouthFrameOf(box)) {
2415
+ const { c, half, amp: a } = frame;
2379
2416
  return localWarp(import_format3.StandardParameter.MouthForm, mesh, box, [-1, 0, 1], ([x], v) => {
2380
2417
  if (v === 0) return [0, 0];
2381
2418
  const u = (x - c) / half;
@@ -2393,8 +2430,8 @@ function mouthOpenGrow(mesh, box) {
2393
2430
  ([x, y], v) => v === 1 ? [(AMPLITUDE.mouthOpenWidth - 1) * (x - c), 0] : [0, box.y1 + (y - box.y1) * 0.2 - y]
2394
2431
  );
2395
2432
  }
2396
- function mouthWiden(mesh, box) {
2397
- const c = cx(box);
2433
+ function mouthWiden(mesh, box, frame = mouthFrameOf(box)) {
2434
+ const c = frame.c;
2398
2435
  return localWarp(import_format3.StandardParameter.MouthOpen, mesh, box, [0, 1], ([x], v) => [
2399
2436
  v * (AMPLITUDE.mouthOpenWidth - 1) * (x - c),
2400
2437
  0
@@ -2777,40 +2814,56 @@ var ARM_IDS = {
2777
2814
  forearmDeformer: "forearmDeformer_R"
2778
2815
  }
2779
2816
  };
2780
- function armGeometry(layer, bodyAxisX) {
2817
+ function endPivot(layer, end) {
2781
2818
  const { bbox, canvasW, canvasH, rowRuns } = layer;
2782
- const b = boxOfLayer(layer);
2783
- const span = (k) => {
2819
+ const span = runSpan(layer);
2820
+ const from = (k) => end === "top" ? k : bbox.h - 1 - k;
2821
+ const painted = rowRuns?.flatMap((runs, k) => runs.length > 0 ? [k] : []) ?? [];
2822
+ const firstK = end === "top" ? painted[0] ?? 0 : painted.at(-1) ?? bbox.h - 1;
2823
+ const lo = Math.ceil(CAP_ROWS[0] * bbox.h);
2824
+ const hi = Math.max(lo, Math.floor(CAP_ROWS[1] * bbox.h));
2825
+ const widths = [];
2826
+ for (let k = lo; k <= hi; k++) {
2827
+ const [s2, e2] = span(from(k));
2828
+ widths.push(e2 - s2);
2829
+ }
2830
+ widths.sort((p, q) => p - q);
2831
+ const radius = widths[widths.length >> 1] / 2;
2832
+ const ks = end === "top" ? firstK + radius : firstK - radius;
2833
+ const [s, e] = span(Math.round(ks));
2834
+ return {
2835
+ point: {
2836
+ x: roundTo((s + e) / 2 - canvasW / 2, 0.01),
2837
+ y: roundTo(canvasH / 2 - (bbox.y + ks + 0.5), 0.01)
2838
+ },
2839
+ radius,
2840
+ row: ks
2841
+ };
2842
+ }
2843
+ function runSpan(layer) {
2844
+ const { bbox, rowRuns } = layer;
2845
+ return (k) => {
2784
2846
  const runs = rowRuns?.[k] ?? [];
2785
2847
  if (runs.length === 0) return [bbox.x, bbox.x + bbox.w];
2786
- let [s2, e2] = [runs[0], runs[1]];
2848
+ let [s, e] = [runs[0], runs[1]];
2787
2849
  for (let i = 2; i < runs.length; i += 2) {
2788
- if (runs[i + 1] - runs[i] > e2 - s2) [s2, e2] = [runs[i], runs[i + 1]];
2850
+ if (runs[i + 1] - runs[i] > e - s) [s, e] = [runs[i], runs[i + 1]];
2789
2851
  }
2790
- return [s2, e2];
2852
+ return [s, e];
2791
2853
  };
2854
+ }
2855
+ function armGeometry(layer, bodyAxisX) {
2856
+ const { bbox, canvasW, canvasH, rowRuns } = layer;
2857
+ const b = boxOfLayer(layer);
2858
+ const span = runSpan(layer);
2792
2859
  const centreX = (k) => {
2793
2860
  const [s2, e2] = span(k);
2794
2861
  return (s2 + e2) / 2 - canvasW / 2;
2795
2862
  };
2796
2863
  const rowY = (k) => canvasH / 2 - (bbox.y + k + 0.5);
2797
2864
  const painted = rowRuns?.flatMap((runs, k) => runs.length > 0 ? [k] : []) ?? [];
2798
- const first = painted[0] ?? 0;
2799
2865
  const last = painted.at(-1) ?? bbox.h - 1;
2800
- const lo = Math.ceil(CAP_ROWS[0] * bbox.h);
2801
- const hi = Math.max(lo, Math.floor(CAP_ROWS[1] * bbox.h));
2802
- const widths = [];
2803
- for (let k = lo; k <= hi; k++) {
2804
- const [s2, e2] = span(k);
2805
- widths.push(e2 - s2);
2806
- }
2807
- widths.sort((p, q) => p - q);
2808
- const ru = widths[widths.length >> 1] / 2;
2809
- const ks = first + ru;
2810
- const shoulder = {
2811
- x: roundTo(centreX(Math.round(ks)), 0.01),
2812
- y: roundTo(rowY(ks), 0.01)
2813
- };
2866
+ const { point: shoulder, radius: ru, row: ks } = endPivot(layer, "top");
2814
2867
  const ke = Math.round(ks + ELBOW_AT * (last - ks));
2815
2868
  if (ke <= ks) {
2816
2869
  throw new LayerGeometryError(
@@ -2858,7 +2911,7 @@ function meshOf(vertices, indices) {
2858
2911
  );
2859
2912
  return { vertices, uvs, indices };
2860
2913
  }
2861
- function armParts(role, layer, g, order) {
2914
+ function armParts(role, layer, g, order, poseSwitch) {
2862
2915
  const b = boxOfLayer(layer);
2863
2916
  const lx = (x) => roundTo((x - cx(b)) / bw(b), 1e-5);
2864
2917
  const ly = (y) => roundTo((y - cy(b)) / bh(b), 1e-5);
@@ -2911,7 +2964,7 @@ function armParts(role, layer, g, order) {
2911
2964
  const [p, q] = [ringInner + j, ringOuter + j];
2912
2965
  ring.push(p, q, q + 1, p, q + 1, p + 1);
2913
2966
  }
2914
- const part = (id, deformer, z, mesh) => ({
2967
+ const part = (id, deformer, z, mesh, swaps = false) => ({
2915
2968
  id,
2916
2969
  color: [1, 1, 1, 1],
2917
2970
  width: bw(b),
@@ -2919,50 +2972,124 @@ function armParts(role, layer, g, order) {
2919
2972
  transform: { x: cx(b), y: cy(b) },
2920
2973
  order: z,
2921
2974
  deformer,
2922
- mesh
2975
+ mesh,
2976
+ ...swaps && poseSwitch !== void 0 ? {
2977
+ bindings: [
2978
+ {
2979
+ parameter: poseSwitch,
2980
+ channel: "opacity",
2981
+ from: 1,
2982
+ to: 0
2983
+ }
2984
+ ]
2985
+ } : {}
2923
2986
  });
2924
2987
  const ids = ARM_IDS[role];
2925
2988
  return [
2926
- part(ids.cap, ids.forearmDeformer, order, {
2927
- vertices,
2928
- uvs,
2929
- indices: [...fan, ...ring]
2930
- }),
2989
+ part(
2990
+ ids.cap,
2991
+ ids.forearmDeformer,
2992
+ order,
2993
+ {
2994
+ vertices,
2995
+ uvs,
2996
+ indices: [...fan, ...ring]
2997
+ },
2998
+ true
2999
+ ),
2931
3000
  part(role, ids.armDeformer, order + 1, meshOf(band(0.5, seam), quad)),
2932
3001
  part(
2933
3002
  ids.forearm,
2934
3003
  ids.forearmDeformer,
2935
3004
  order + 2,
2936
- meshOf(band(seam, -0.5), quad)
3005
+ meshOf(band(seam, -0.5), quad),
3006
+ true
2937
3007
  )
2938
3008
  ];
2939
3009
  }
3010
+ var rotateBinding = (parameter, range, sign) => ({
3011
+ parameter,
3012
+ channel: "rotate",
3013
+ from: sign * range[0],
3014
+ to: sign * range[1]
3015
+ });
2940
3016
  function armDeformers(role, g) {
2941
3017
  const ids = ARM_IDS[role];
2942
3018
  const params = ARM_PARAMS[role];
2943
- const rotate = (parameter, range, sign) => ({
2944
- parameter,
2945
- channel: "rotate",
2946
- from: sign * range[0],
2947
- to: sign * range[1]
2948
- });
2949
3019
  return [
2950
3020
  {
2951
3021
  id: ids.armDeformer,
2952
3022
  parent: BODY_WARP_ID,
2953
3023
  pivot: { ...g.shoulder },
2954
- bindings: [rotate(params.arm, ARM_RANGE, g.side)]
3024
+ bindings: [rotateBinding(params.arm, ARM_RANGE, g.side)]
2955
3025
  },
2956
3026
  {
2957
3027
  id: ids.forearmDeformer,
2958
3028
  parent: ids.armDeformer,
2959
3029
  pivot: { ...g.elbow },
2960
- bindings: [rotate(params.elbow, ELBOW_RANGE, -g.side)]
3030
+ bindings: [rotateBinding(params.elbow, ELBOW_RANGE, -g.side)]
2961
3031
  }
2962
3032
  ];
2963
3033
  }
2964
3034
 
3035
+ // src/auto-rig/forearm-pose.ts
3036
+ var import_format6 = require("@ikijs/format");
3037
+ var ARM_OF_POSE = {
3038
+ forearm_pose_L: "arm_L",
3039
+ forearm_pose_R: "arm_R"
3040
+ };
3041
+ var POSE_OF_ARM = {
3042
+ arm_L: "forearm_pose_L",
3043
+ arm_R: "forearm_pose_R"
3044
+ };
3045
+ var POSE_PARAMS = {
3046
+ forearm_pose_L: { pose: import_format6.StandardParameter.ArmPoseLeft, angle: import_format6.StandardParameter.ArmPoseAngleLeft },
3047
+ forearm_pose_R: { pose: import_format6.StandardParameter.ArmPoseRight, angle: import_format6.StandardParameter.ArmPoseAngleRight }
3048
+ };
3049
+ var POSE_IDS = {
3050
+ forearm_pose_L: "armPoseDeformer_L",
3051
+ forearm_pose_R: "armPoseDeformer_R"
3052
+ };
3053
+ var POSE_ANGLE_RANGE = [-15, 15];
3054
+ function forearmPoseGeometry(layer) {
3055
+ const { point, radius } = endPivot(layer, "bottom");
3056
+ const first = layer.rowRuns?.findIndex((runs) => runs.length > 0) ?? -1;
3057
+ const k = first < 0 ? 0 : first;
3058
+ return {
3059
+ pivot: point,
3060
+ radius,
3061
+ top: roundTo(layer.canvasH / 2 - (layer.bbox.y + k + 0.5), 0.01)
3062
+ };
3063
+ }
3064
+ function forearmPosePart(role, layer, order) {
3065
+ const b = boxOfLayer(layer);
3066
+ return {
3067
+ id: role,
3068
+ color: [1, 1, 1, 1],
3069
+ width: bw(b),
3070
+ height: bh(b),
3071
+ transform: { x: cx(b), y: cy(b) },
3072
+ order,
3073
+ deformer: POSE_IDS[role],
3074
+ bindings: [
3075
+ { parameter: POSE_PARAMS[role].pose, channel: "opacity", from: 0, to: 1 }
3076
+ ]
3077
+ };
3078
+ }
3079
+ function forearmPoseDeformer(role, arm) {
3080
+ return {
3081
+ id: POSE_IDS[role],
3082
+ parent: ARM_IDS[ARM_OF_POSE[role]].armDeformer,
3083
+ pivot: { ...arm.elbow },
3084
+ bindings: [
3085
+ rotateBinding(POSE_PARAMS[role].angle, POSE_ANGLE_RANGE, -arm.side)
3086
+ ]
3087
+ };
3088
+ }
3089
+
2965
3090
  // src/auto-rig/roles.ts
3091
+ var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
3092
+ var isLipRole = (role) => LIP_ROLES.includes(role);
2966
3093
  var ROLE_TABLE = [
2967
3094
  { role: "hair_back", family: "hair_back" },
2968
3095
  { role: "body", family: "body" },
@@ -2972,8 +3099,14 @@ var ROLE_TABLE = [
2972
3099
  { role: "blush_L", family: "face" },
2973
3100
  { role: "blush_R", family: "face" },
2974
3101
  { role: "nose", family: "nose" },
2975
- { role: "mouth", family: "mouth", required: true },
3102
+ { role: "mouth", family: "mouth" },
2976
3103
  { role: "mouth_open", family: "mouth" },
3104
+ // The lip set, back to front: the interior, the lower lip's skin, the upper
3105
+ // lip and its line over both. It folds open instead of crossfading
3106
+ // (`mouth.ts`), so it stands in for `mouth`, never beside it.
3107
+ { role: "mouth_inner", family: "mouth" },
3108
+ { role: "lip_lower", family: "mouth" },
3109
+ { role: "lip_upper", family: "mouth" },
2977
3110
  { role: "eye_L", family: "eye_L", required: true },
2978
3111
  { role: "eye_R", family: "eye_R", required: true },
2979
3112
  { role: "iris_L", family: "eye_L" },
@@ -2988,7 +3121,10 @@ var ROLE_TABLE = [
2988
3121
  { role: "lash_R", family: "eye_R" },
2989
3122
  { role: "brow_L", family: "brow_L" },
2990
3123
  { role: "brow_R", family: "brow_R" },
2991
- { role: "hair_front", family: "hair_front" }
3124
+ { role: "hair_front", family: "hair_front" },
3125
+ // Drawn above the face and the hair: a raised hand passes them.
3126
+ { role: "forearm_pose_L", family: "forearm_pose" },
3127
+ { role: "forearm_pose_R", family: "forearm_pose" }
2992
3128
  ];
2993
3129
  var REQUIRED_ROLES = ROLE_TABLE.filter(
2994
3130
  (r) => r.required
@@ -3028,6 +3164,39 @@ function checkArmsHaveBody(roles) {
3028
3164
  );
3029
3165
  }
3030
3166
  }
3167
+ function checkMouth(roles) {
3168
+ const has = new Set(roles);
3169
+ const present = LIP_ROLES.filter((r) => has.has(r));
3170
+ if (present.length === 0) {
3171
+ if (!has.has("mouth")) {
3172
+ throw new Error(
3173
+ `auto-rig: missing required role mouth (or the lip set ${LIP_ROLES.join(", ")})`
3174
+ );
3175
+ }
3176
+ return;
3177
+ }
3178
+ if (present.length < LIP_ROLES.length) {
3179
+ throw new Error(
3180
+ `auto-rig: the lip set is partial: missing ${LIP_ROLES.filter((r) => !has.has(r)).join(", ")}`
3181
+ );
3182
+ }
3183
+ const mixed = ["mouth", "mouth_open"].filter((r) => has.has(r));
3184
+ if (mixed.length > 0) {
3185
+ throw new Error(
3186
+ `auto-rig: the lip set cannot be mixed with ${mixed.join(", ")} (the lip set replaces the mouth drawings; use one or the other)`
3187
+ );
3188
+ }
3189
+ }
3190
+ function checkPosesHaveArms(roles) {
3191
+ const has = new Set(roles);
3192
+ for (const side of ["L", "R"]) {
3193
+ if (has.has(`forearm_pose_${side}`) && !has.has(`arm_${side}`)) {
3194
+ throw new Error(
3195
+ `auto-rig: forearm_pose_${side} needs an arm_${side} layer (a pose forearm hangs from its arm's elbow)`
3196
+ );
3197
+ }
3198
+ }
3199
+ }
3031
3200
  function parseLayerRoles(fileNames) {
3032
3201
  const seen = /* @__PURE__ */ new Map();
3033
3202
  const out = [];
@@ -3053,37 +3222,39 @@ function parseLayerRoles(fileNames) {
3053
3222
  `auto-rig: missing required role${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`
3054
3223
  );
3055
3224
  }
3225
+ checkMouth(seen.keys());
3056
3226
  checkArmsHaveBody(seen.keys());
3227
+ checkPosesHaveArms(seen.keys());
3057
3228
  return out;
3058
3229
  }
3059
3230
 
3060
3231
  // src/auto-rig/generate.ts
3061
- var import_format8 = require("@ikijs/format");
3232
+ var import_format10 = require("@ikijs/format");
3062
3233
 
3063
3234
  // src/auto-rig/animations.ts
3064
- var import_format6 = require("@ikijs/format");
3235
+ var import_format7 = require("@ikijs/format");
3065
3236
  function browsY(y) {
3066
3237
  return [
3067
- { parameter: import_format6.StandardParameter.BrowLeftY, value: y, blend: "add" },
3068
- { parameter: import_format6.StandardParameter.BrowRightY, value: y, blend: "add" }
3238
+ { parameter: import_format7.StandardParameter.BrowLeftY, value: y, blend: "add" },
3239
+ { parameter: import_format7.StandardParameter.BrowRightY, value: y, blend: "add" }
3069
3240
  ];
3070
3241
  }
3071
3242
  function browsTilt(innerDown) {
3072
3243
  return [
3073
- { parameter: import_format6.StandardParameter.BrowLeftAngle, value: innerDown, blend: "add" },
3074
- { parameter: import_format6.StandardParameter.BrowRightAngle, value: -innerDown, blend: "add" }
3244
+ { parameter: import_format7.StandardParameter.BrowLeftAngle, value: innerDown, blend: "add" },
3245
+ { parameter: import_format7.StandardParameter.BrowRightAngle, value: -innerDown, blend: "add" }
3075
3246
  ];
3076
3247
  }
3077
3248
  function cheek(value) {
3078
- return { parameter: import_format6.StandardParameter.Cheek, value, blend: "add" };
3249
+ return { parameter: import_format7.StandardParameter.Cheek, value, blend: "add" };
3079
3250
  }
3080
- var BROWS = [import_format6.StandardParameter.BrowLeftY, import_format6.StandardParameter.BrowRightY, import_format6.StandardParameter.BrowLeftAngle, import_format6.StandardParameter.BrowRightAngle];
3251
+ var BROWS = [import_format7.StandardParameter.BrowLeftY, import_format7.StandardParameter.BrowRightY, import_format7.StandardParameter.BrowLeftAngle, import_format7.StandardParameter.BrowRightAngle];
3081
3252
  var EXPRESSIONS = [
3082
3253
  {
3083
3254
  id: "smile",
3084
3255
  description: "Smiling and pleased: warm, friendly, content. For greetings, thanks, agreement and gentle happiness.",
3085
3256
  parameters: [
3086
- { parameter: import_format6.StandardParameter.MouthForm, value: 1, blend: "add" },
3257
+ { parameter: import_format7.StandardParameter.MouthForm, value: 1, blend: "add" },
3087
3258
  ...browsY(0.25),
3088
3259
  cheek(0.4)
3089
3260
  ]
@@ -3092,10 +3263,10 @@ var EXPRESSIONS = [
3092
3263
  id: "laugh",
3093
3264
  description: "Laughing, eyes shut and mouth open: delighted, amused. For jokes, playfulness and big joy.",
3094
3265
  parameters: [
3095
- { parameter: import_format6.StandardParameter.EyeOpenLeft, value: 0, blend: "multiply" },
3096
- { parameter: import_format6.StandardParameter.EyeOpenRight, value: 0, blend: "multiply" },
3097
- { parameter: import_format6.StandardParameter.MouthForm, value: 1, blend: "add" },
3098
- { parameter: import_format6.StandardParameter.MouthOpen, value: 0.8, blend: "overwrite" },
3266
+ { parameter: import_format7.StandardParameter.EyeOpenLeft, value: 0, blend: "multiply" },
3267
+ { parameter: import_format7.StandardParameter.EyeOpenRight, value: 0, blend: "multiply" },
3268
+ { parameter: import_format7.StandardParameter.MouthForm, value: 1, blend: "add" },
3269
+ { parameter: import_format7.StandardParameter.MouthOpen, value: 0.8, blend: "overwrite" },
3099
3270
  ...browsY(0.4),
3100
3271
  cheek(0.65)
3101
3272
  ]
@@ -3107,7 +3278,7 @@ var EXPRESSIONS = [
3107
3278
  parameters: [
3108
3279
  ...browsY(-0.5),
3109
3280
  ...browsTilt(0.8),
3110
- { parameter: import_format6.StandardParameter.MouthForm, value: -0.9, blend: "add" }
3281
+ { parameter: import_format7.StandardParameter.MouthForm, value: -0.9, blend: "add" }
3111
3282
  ]
3112
3283
  },
3113
3284
  {
@@ -3117,7 +3288,7 @@ var EXPRESSIONS = [
3117
3288
  parameters: [
3118
3289
  ...browsY(0.35),
3119
3290
  ...browsTilt(-0.8),
3120
- { parameter: import_format6.StandardParameter.MouthForm, value: -0.8, blend: "add" }
3291
+ { parameter: import_format7.StandardParameter.MouthForm, value: -0.8, blend: "add" }
3121
3292
  ]
3122
3293
  },
3123
3294
  {
@@ -3125,18 +3296,18 @@ var EXPRESSIONS = [
3125
3296
  description: "Surprised, mouth dropped open: startled, amazed. For shock, sudden news or disbelief.",
3126
3297
  parameters: [
3127
3298
  ...browsY(0.8),
3128
- { parameter: import_format6.StandardParameter.MouthOpen, value: 0.9, blend: "overwrite" }
3299
+ { parameter: import_format7.StandardParameter.MouthOpen, value: 0.9, blend: "overwrite" }
3129
3300
  ]
3130
3301
  },
3131
3302
  {
3132
3303
  id: "shy",
3133
3304
  description: "Shy or embarrassed: bashful, flustered. For being praised, teased or caught off guard.",
3134
- needs: [import_format6.StandardParameter.Cheek],
3305
+ needs: [import_format7.StandardParameter.Cheek],
3135
3306
  parameters: [
3136
3307
  cheek(1),
3137
3308
  // Gaze down.
3138
- { parameter: import_format6.StandardParameter.EyeballY, value: -0.65, blend: "add" },
3139
- { parameter: import_format6.StandardParameter.MouthForm, value: 0.4, blend: "add" },
3309
+ { parameter: import_format7.StandardParameter.EyeballY, value: -0.65, blend: "add" },
3310
+ { parameter: import_format7.StandardParameter.MouthForm, value: 0.4, blend: "add" },
3140
3311
  ...browsTilt(-0.25)
3141
3312
  ]
3142
3313
  }
@@ -3149,14 +3320,14 @@ function defaultExpressions(declared) {
3149
3320
  parameters: e.parameters.filter((t) => declared.has(t.parameter))
3150
3321
  }));
3151
3322
  }
3152
- var DEFAULT_MOTIONS = {
3323
+ var HEAD_MOTIONS = {
3153
3324
  Nod: [
3154
3325
  {
3155
3326
  description: "Nods yes: agreement, acknowledgement, understanding.",
3156
3327
  duration: 1,
3157
3328
  curves: [
3158
3329
  {
3159
- parameter: import_format6.StandardParameter.AngleY,
3330
+ parameter: import_format7.StandardParameter.AngleY,
3160
3331
  keys: [
3161
3332
  [0, 0],
3162
3333
  [0.3, -18],
@@ -3177,7 +3348,7 @@ var DEFAULT_MOTIONS = {
3177
3348
  fadeIn: 0.15,
3178
3349
  curves: [
3179
3350
  {
3180
- parameter: import_format6.StandardParameter.AngleX,
3351
+ parameter: import_format7.StandardParameter.AngleX,
3181
3352
  keys: [
3182
3353
  [0, 0],
3183
3354
  [0.2, -18],
@@ -3196,7 +3367,7 @@ var DEFAULT_MOTIONS = {
3196
3367
  duration: 1.4,
3197
3368
  curves: [
3198
3369
  {
3199
- parameter: import_format6.StandardParameter.AngleZ,
3370
+ parameter: import_format7.StandardParameter.AngleZ,
3200
3371
  keys: [
3201
3372
  [0, 0],
3202
3373
  [0.35, 20],
@@ -3208,6 +3379,55 @@ var DEFAULT_MOTIONS = {
3208
3379
  }
3209
3380
  ]
3210
3381
  };
3382
+ var WAVE = {
3383
+ Wave: [
3384
+ {
3385
+ description: "Waves hello with the right hand: greeting, goodbye, getting attention.",
3386
+ duration: 2.2,
3387
+ fadeIn: 0.15,
3388
+ fadeOut: 0.15,
3389
+ curves: [
3390
+ {
3391
+ parameter: import_format7.StandardParameter.ArmPoseRight,
3392
+ interpolation: "linear",
3393
+ keys: [
3394
+ [0, 0],
3395
+ [0.15, 1],
3396
+ [2.05, 1],
3397
+ [2.2, 0]
3398
+ ]
3399
+ },
3400
+ {
3401
+ parameter: import_format7.StandardParameter.ArmRight,
3402
+ keys: [
3403
+ [0, 0],
3404
+ [0.4, 10],
3405
+ [1.8, 10],
3406
+ [2.2, 0]
3407
+ ]
3408
+ },
3409
+ {
3410
+ parameter: import_format7.StandardParameter.ArmPoseAngleRight,
3411
+ keys: [
3412
+ [0, 0],
3413
+ [0.5, 0],
3414
+ [0.7, 12],
3415
+ [1, -12],
3416
+ [1.3, 12],
3417
+ [1.7, 0],
3418
+ [2.2, 0]
3419
+ ]
3420
+ }
3421
+ ]
3422
+ }
3423
+ ]
3424
+ };
3425
+ function defaultMotions(declared) {
3426
+ const waves = [import_format7.StandardParameter.ArmRight, import_format7.StandardParameter.ArmPoseRight, import_format7.StandardParameter.ArmPoseAngleRight].every(
3427
+ (id) => declared.has(id)
3428
+ );
3429
+ return waves ? { ...HEAD_MOTIONS, ...WAVE } : HEAD_MOTIONS;
3430
+ }
3211
3431
 
3212
3432
  // src/auto-rig/checks.ts
3213
3433
  var finite = (v) => typeof v === "number" && Number.isFinite(v);
@@ -3312,7 +3532,16 @@ function checkLayers(layers, canvas) {
3312
3532
  if (missing.length > 0) {
3313
3533
  throw new Error(`auto-rig: missing required role(s) ${missing.join(", ")}`);
3314
3534
  }
3535
+ checkMouth(seen);
3536
+ for (const l of layers) {
3537
+ if (l.rowRuns === void 0 && isLipRole(l.role)) {
3538
+ throw new Error(
3539
+ `auto-rig: layer "${l.fileName}": the lip set needs rowRuns (the fold reads the opening's rows off them; measure with createLayerSetMeasurer)`
3540
+ );
3541
+ }
3542
+ }
3315
3543
  checkArmsHaveBody(seen);
3544
+ checkPosesHaveArms(seen);
3316
3545
  }
3317
3546
  function checkHeadEdges(edges, byRole) {
3318
3547
  if (edges === void 0) return;
@@ -3430,7 +3659,180 @@ function checkStrandEdges(strand, irises, hair) {
3430
3659
  }
3431
3660
 
3432
3661
  // src/auto-rig/context.ts
3433
- var import_format7 = require("@ikijs/format");
3662
+ var import_format9 = require("@ikijs/format");
3663
+
3664
+ // src/auto-rig/mouth.ts
3665
+ var import_format8 = require("@ikijs/format");
3666
+ var UPPER_SHARE = 0.3;
3667
+ var MOUTH_KNOT_PX = 4;
3668
+ var isLipSet = (byRole) => LIP_ROLES.every((r) => byRole.has(r));
3669
+ function columnRuns(layer) {
3670
+ const cols = /* @__PURE__ */ new Map();
3671
+ (layer.rowRuns ?? []).forEach((runs, k) => {
3672
+ for (let i = 0; i < runs.length; i += 2) {
3673
+ for (let c = runs[i]; c < runs[i + 1]; c++) {
3674
+ const list = cols.get(c) ?? [];
3675
+ const last = list[list.length - 1];
3676
+ if (last !== void 0 && last[1] === k) last[1] = k + 1;
3677
+ else list.push([k, k + 1]);
3678
+ cols.set(c, list);
3679
+ }
3680
+ }
3681
+ });
3682
+ return cols;
3683
+ }
3684
+ var edgesOf = (layer, run) => ({
3685
+ top: layer.canvasH / 2 - (layer.bbox.y + run[0]),
3686
+ bottom: layer.canvasH / 2 - (layer.bbox.y + run[1])
3687
+ });
3688
+ function mouthOpening(byRole) {
3689
+ const inner = byRole.get("mouth_inner");
3690
+ const upper = byRole.get("lip_upper");
3691
+ const lower = byRole.get("lip_lower");
3692
+ const innerCols = columnRuns(inner);
3693
+ if (innerCols.size === 0) {
3694
+ throw new LayerGeometryError(
3695
+ `auto-rig: layer "${inner.fileName}": mouth_inner has no opaque pixel (alpha \u2265 128) to read the opening off`
3696
+ );
3697
+ }
3698
+ const upperCols = columnRuns(upper);
3699
+ const lowerCols = columnRuns(lower);
3700
+ const cols = [...innerCols.keys()];
3701
+ const x0 = Math.min(...cols);
3702
+ const x1 = Math.max(...cols);
3703
+ const read = (col) => {
3704
+ const runs = innerCols.get(col);
3705
+ if (runs === void 0) return void 0;
3706
+ const T = edgesOf(inner, runs[0]).top;
3707
+ const Bb = edgesOf(inner, runs[runs.length - 1]).bottom;
3708
+ const line = [...upperCols.get(col) ?? []].reverse().map((r) => edgesOf(upper, r)).find((e) => e.top >= T);
3709
+ const Tu = line?.bottom ?? T;
3710
+ const lineTop = line?.top ?? T;
3711
+ const skin = lowerCols.get(col)?.[0];
3712
+ return {
3713
+ T,
3714
+ Bb,
3715
+ Tu,
3716
+ lineTop,
3717
+ lineH: lineTop - Tu,
3718
+ Bl: skin === void 0 ? Bb : edgesOf(lower, skin).top,
3719
+ H: T - Bb,
3720
+ seam: T - UPPER_SHARE * (T - Bb)
3721
+ };
3722
+ };
3723
+ const reads = [];
3724
+ for (let c = x0; c <= x1; c++) {
3725
+ let r;
3726
+ for (let d = 0; (r = read(c - d) ?? read(c + d)) === void 0; d++) ;
3727
+ reads.push(r);
3728
+ }
3729
+ const base = (col) => reads[Math.min(x1, Math.max(x0, col)) - x0];
3730
+ const centre = Math.round((x0 + x1) / 2);
3731
+ const w = base(centre).lineH;
3732
+ return {
3733
+ x0,
3734
+ x1,
3735
+ centre,
3736
+ canvasW: inner.canvasW,
3737
+ w,
3738
+ at(col) {
3739
+ const b = base(col);
3740
+ return { ...b, overlap: Math.max(0, Math.min(w, b.lineH)) };
3741
+ }
3742
+ };
3743
+ }
3744
+ function mouthFold(role, opening) {
3745
+ return (col, y, v) => {
3746
+ if (v === 1) return 0;
3747
+ if (role !== "mouth_inner" && (col < opening.x0 || col > opening.x1))
3748
+ return 0;
3749
+ const c = opening.at(col);
3750
+ const k = 1 - v;
3751
+ switch (role) {
3752
+ case "lip_upper":
3753
+ return (c.seam - c.Tu) * k;
3754
+ case "lip_lower":
3755
+ return (c.seam + c.overlap - c.Bl) * k;
3756
+ case "mouth_inner":
3757
+ return (c.seam + c.overlap - y) * k;
3758
+ }
3759
+ };
3760
+ }
3761
+ function columnOfKnot(knot, opening, role) {
3762
+ const boundary = Math.round(knot + opening.canvasW / 2);
3763
+ const col = boundary === opening.x1 + 1 ? opening.x1 : boundary;
3764
+ if (col >= opening.x0 && col <= opening.x1) return col;
3765
+ return role === "mouth_inner" ? Math.min(opening.x1, Math.max(opening.x0, col)) : void 0;
3766
+ }
3767
+ function mouthFoldWarp(role, mesh, xs, box, opening) {
3768
+ const fold = mouthFold(role, opening);
3769
+ return localWarp(import_format8.StandardParameter.MouthOpen, mesh, box, [0, 1], (p, v, i) => {
3770
+ const col = columnOfKnot(xs[i % xs.length], opening, role);
3771
+ return [0, col === void 0 ? 0 : fold(col, p[1], v)];
3772
+ });
3773
+ }
3774
+ function mouthKnots(union, opening) {
3775
+ const half = opening.canvasW / 2;
3776
+ const step = MOUTH_KNOT_PX;
3777
+ const xs = [union.x0];
3778
+ while (xs[xs.length - 1] + step < union.x1 - 0.3 * step)
3779
+ xs.push(xs[xs.length - 1] + step);
3780
+ xs.push(union.x1);
3781
+ const fixed = /* @__PURE__ */ new Set([union.x0, union.x1]);
3782
+ for (const x of [opening.x0 - half, opening.x1 + 1 - half]) {
3783
+ fixed.add(x);
3784
+ if (xs.includes(x)) continue;
3785
+ const near = xs.findIndex((v) => Math.abs(v - x) < 0.3 * step);
3786
+ if (near >= 0 && !fixed.has(xs[near])) xs[near] = x;
3787
+ else xs.push(x);
3788
+ }
3789
+ xs.sort((p, q) => p - q);
3790
+ for (const x of xs) {
3791
+ if (!Number.isInteger(x + half)) {
3792
+ throw new Error(
3793
+ `auto-rig: mouth knot ${x} is not an integer canvas boundary`
3794
+ );
3795
+ }
3796
+ }
3797
+ return xs;
3798
+ }
3799
+ function mouthMesh(box, knots) {
3800
+ const xs = [box.x0, ...knots.filter((k) => k > box.x0 && k < box.x1), box.x1];
3801
+ const rows = cellsFor(bh(box), MOUTH_KNOT_PX, 2, 40);
3802
+ const ys = Array.from(
3803
+ { length: rows + 1 },
3804
+ (_, r) => box.y1 - bh(box) * r / rows
3805
+ );
3806
+ return { mesh: columnMesh(box, { xs, ys }), xs };
3807
+ }
3808
+ function buildMouthRig(byRole) {
3809
+ const opening = mouthOpening(byRole);
3810
+ const union = unionBoxes(LIP_ROLES.map((r) => boxOfLayer(byRole.get(r))));
3811
+ return {
3812
+ opening,
3813
+ frame: mouthFrameOf(union),
3814
+ knots: mouthKnots(union, opening),
3815
+ union
3816
+ };
3817
+ }
3818
+ function mouthAnchor(byRole) {
3819
+ const box = (role) => boxOfLayer(byRole.get(role));
3820
+ if (isLipSet(byRole)) {
3821
+ const opening = mouthOpening(byRole);
3822
+ return {
3823
+ at: {
3824
+ x: cx(box("mouth_inner")),
3825
+ y: opening.at(opening.centre).seam
3826
+ },
3827
+ box: unionBoxes(LIP_ROLES.map(box))
3828
+ };
3829
+ }
3830
+ const m = box("mouth");
3831
+ return {
3832
+ at: { x: cx(m), y: cy(m) },
3833
+ box: byRole.has("mouth_open") ? unionBoxes([m, box("mouth_open")]) : m
3834
+ };
3835
+ }
3434
3836
 
3435
3837
  // src/auto-rig/head.ts
3436
3838
  function outerEdge(runs, axisX, side) {
@@ -3510,7 +3912,7 @@ function buildHeadFrame(layers, opts) {
3510
3912
  return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
3511
3913
  };
3512
3914
  const eyeY = pair("iris_L", "iris_R") ?? pair("eye_L", "eye_R");
3513
- const mouthY = centreY("mouth");
3915
+ const mouthY = mouthAnchor(byRole).at.y;
3514
3916
  const rowY = (r) => canvasH / 2 - (faceLayer.bbox.y + r + 0.5);
3515
3917
  const raw = filledProfile(faceLayer.rowHalfWidths);
3516
3918
  const profile = raw === null ? null : smoothProfile(raw);
@@ -4462,6 +4864,7 @@ function familyField(m, family) {
4462
4864
  return hairField(m, m.hairBack, NOD.hairBack);
4463
4865
  case "body":
4464
4866
  case "arm":
4867
+ case "forearm_pose":
4465
4868
  return () => [0, 0];
4466
4869
  }
4467
4870
  }
@@ -4482,7 +4885,6 @@ function sideIrises(byRole) {
4482
4885
  return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
4483
4886
  }
4484
4887
  function solveContext(frame, byRole, options, hasNose) {
4485
- const box = (role) => boxOfLayer(byRole.get(role));
4486
4888
  const row = (s, strand2, sideSign) => {
4487
4889
  if (strand2 !== void 0) {
4488
4890
  const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
@@ -4531,8 +4933,7 @@ function solveContext(frame, byRole, options, hasNose) {
4531
4933
  noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
4532
4934
  noseAt = { x: cx(noseBox), y: cy(noseBox) };
4533
4935
  }
4534
- const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
4535
- const m = box("mouth");
4936
+ const mouth = mouthAnchor(byRole);
4536
4937
  return {
4537
4938
  frame,
4538
4939
  left,
@@ -4544,8 +4945,8 @@ function solveContext(frame, byRole, options, hasNose) {
4544
4945
  eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
4545
4946
  noseAt,
4546
4947
  noseBox,
4547
- mouthAt: { x: cx(m), y: cy(m) },
4548
- mouthBox
4948
+ mouthAt: mouth.at,
4949
+ mouthBox: mouth.box
4549
4950
  };
4550
4951
  }
4551
4952
  function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
@@ -4561,7 +4962,7 @@ function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
4561
4962
  const family = roleSpec(role).family;
4562
4963
  const part = parts.find((p) => p.id === role);
4563
4964
  const g = grids.get(family);
4564
- const warp = part?.warps?.find((w) => w.parameter === import_format7.StandardParameter.AngleX);
4965
+ const warp = part?.warps?.find((w) => w.parameter === import_format9.StandardParameter.AngleX);
4565
4966
  if (part === void 0 || part.mesh === void 0 || !g && !warp) {
4566
4967
  return x + field(role)(x, y, ax, 0)[0];
4567
4968
  }
@@ -5297,6 +5698,7 @@ var WARP_ID = {
5297
5698
  };
5298
5699
  var isGridFamily = (f) => f in WARP_ID;
5299
5700
  var isArm = (spec) => spec.family === "arm";
5701
+ var isPose = (spec) => spec.family === "forearm_pose";
5300
5702
  var GRID_PAD = 6;
5301
5703
  function generateIkiFromLayerSet(layers, canvas, options = {}) {
5302
5704
  checkLayers(layers, canvas);
@@ -5357,6 +5759,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
5357
5759
  hh: frame.hh,
5358
5760
  pivots: [...arms.values()].map((g) => g.shoulder)
5359
5761
  }) : void 0;
5762
+ const lips = isLipSet(byRole) ? buildMouthRig(byRole) : void 0;
5360
5763
  const parts = [];
5361
5764
  const reach = /* @__PURE__ */ new Map();
5362
5765
  let regionOf = [];
@@ -5365,11 +5768,22 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
5365
5768
  const layer = byRole.get(spec.role);
5366
5769
  if (layer === void 0) continue;
5367
5770
  if (isArm(spec)) {
5771
+ const pose = POSE_OF_ARM[spec.role];
5368
5772
  parts.push(
5369
- ...armParts(spec.role, layer, arms.get(spec.role), parts.length)
5773
+ ...armParts(
5774
+ spec.role,
5775
+ layer,
5776
+ arms.get(spec.role),
5777
+ parts.length,
5778
+ has(pose) ? POSE_PARAMS[pose].pose : void 0
5779
+ )
5370
5780
  );
5371
5781
  continue;
5372
5782
  }
5783
+ if (isPose(spec)) {
5784
+ parts.push(forearmPosePart(spec.role, layer, parts.length));
5785
+ continue;
5786
+ }
5373
5787
  const built = buildPart(
5374
5788
  spec,
5375
5789
  boxOfLayer(layer),
@@ -5377,7 +5791,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
5377
5791
  frame,
5378
5792
  byRole,
5379
5793
  style,
5380
- landmarks.hairFrontGrid
5794
+ landmarks.hairFrontGrid,
5795
+ lips
5381
5796
  );
5382
5797
  if (built.region !== void 0) {
5383
5798
  regionOf = built.region;
@@ -5422,7 +5837,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
5422
5837
  part.mesh = bodyWarp.mesh;
5423
5838
  continue;
5424
5839
  }
5425
- if (family === "arm") continue;
5840
+ if (family === "arm" || family === "forearm_pose") continue;
5426
5841
  part.deformer = "headDeformer";
5427
5842
  if (part.mesh === void 0) continue;
5428
5843
  const own = familyField(turn, family);
@@ -5434,17 +5849,17 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
5434
5849
  }
5435
5850
  const parameters = declareParameters(new Set(byRole.keys()));
5436
5851
  const model = {
5437
- version: import_format8.IKI_FORMAT_VERSION,
5852
+ version: import_format10.IKI_FORMAT_VERSION,
5438
5853
  name: "auto-rigged",
5439
5854
  canvas: { width: canvas.width, height: canvas.height },
5440
5855
  parameters,
5441
5856
  parts,
5442
- deformers: deformers(frame, grids, bodyWarp?.deformer, arms),
5857
+ deformers: deformers(frame, grids, bodyWarp?.deformer, arms, byRole),
5443
5858
  ...has("hair_front") ? { physics: hairPhysics() } : {},
5444
5859
  expressions: defaultExpressions(new Set(parameters.map((p) => p.id))),
5445
- motions: DEFAULT_MOTIONS
5860
+ motions: defaultMotions(new Set(parameters.map((p) => p.id)))
5446
5861
  };
5447
- const valid = (0, import_format8.parseIkiModel)(model);
5862
+ const valid = (0, import_format10.parseIkiModel)(model);
5448
5863
  if (hasNose && options.onTurnSolved) {
5449
5864
  options.onTurnSolved(
5450
5865
  turnReport(
@@ -5459,14 +5874,14 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
5459
5874
  function turnWarps(mesh, b, fieldOf) {
5460
5875
  return [
5461
5876
  localWarp(
5462
- import_format8.StandardParameter.AngleX,
5877
+ import_format10.StandardParameter.AngleX,
5463
5878
  mesh,
5464
5879
  b,
5465
5880
  X_STOPS,
5466
5881
  ([x, y], v, i) => v === 0 ? [0, 0] : [fieldOf(i)(x, y, v, 0)[0], 0]
5467
5882
  ),
5468
5883
  localWarp(
5469
- import_format8.StandardParameter.AngleY,
5884
+ import_format10.StandardParameter.AngleY,
5470
5885
  mesh,
5471
5886
  b,
5472
5887
  Y_STOPS,
@@ -5479,7 +5894,7 @@ function plateFields(turn, region, head) {
5479
5894
  const neck = neckField(turn);
5480
5895
  return (i) => region[i] === "ear" ? ear : region[i] === "neck" ? neck : head;
5481
5896
  }
5482
- function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5897
+ function buildPart(spec, b, order, frame, byRole, style, frontGrid, lips) {
5483
5898
  const box = (role) => boxOfLayer(byRole.get(role));
5484
5899
  const part = {
5485
5900
  id: spec.role,
@@ -5494,6 +5909,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5494
5909
  const bindings = [];
5495
5910
  let extra = [0, 0];
5496
5911
  let mesh;
5912
+ let knotXs;
5497
5913
  let region;
5498
5914
  let headStart = 0;
5499
5915
  if (spec.role === "face") {
@@ -5504,6 +5920,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5504
5920
  headStart = fm.headStart;
5505
5921
  } else if (spec.role === "hair_front") {
5506
5922
  mesh = columnMesh(b, frontGrid);
5923
+ } else if (lips !== void 0 && isLipRole(spec.role)) {
5924
+ ({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
5507
5925
  } else if (spec.family !== "body") {
5508
5926
  const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
5509
5927
  const px = Math.min(cells.px, meshScale(spec.role, frame));
@@ -5514,7 +5932,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5514
5932
  }
5515
5933
  if (mesh !== void 0) part.mesh = mesh;
5516
5934
  const side = spec.role.endsWith("_L") ? "L" : "R";
5517
- const eyeOpen = side === "L" ? import_format8.StandardParameter.EyeOpenLeft : import_format8.StandardParameter.EyeOpenRight;
5935
+ const eyeOpen = side === "L" ? import_format10.StandardParameter.EyeOpenLeft : import_format10.StandardParameter.EyeOpenRight;
5518
5936
  const hh = frame.hh;
5519
5937
  switch (spec.role) {
5520
5938
  case "face":
@@ -5529,7 +5947,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5529
5947
  headOwnRoll(byRole.has("body"))
5530
5948
  ),
5531
5949
  localWarp(
5532
- import_format8.StandardParameter.Breath,
5950
+ import_format10.StandardParameter.Breath,
5533
5951
  mesh,
5534
5952
  b,
5535
5953
  [0, 1],
@@ -5576,8 +5994,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5576
5994
  if (byRole.has("mouth_open")) {
5577
5995
  warps.push(mouthWiden(mesh, b));
5578
5996
  bindings.push(
5579
- { parameter: import_format8.StandardParameter.MouthOpen, channel: "opacity", from: 1, to: 0 },
5580
- { parameter: import_format8.StandardParameter.MouthOpen, channel: "opacity", from: 1, to: 0 }
5997
+ { parameter: import_format10.StandardParameter.MouthOpen, channel: "opacity", from: 1, to: 0 },
5998
+ { parameter: import_format10.StandardParameter.MouthOpen, channel: "opacity", from: 1, to: 0 }
5581
5999
  );
5582
6000
  extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
5583
6001
  } else {
@@ -5587,19 +6005,19 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5587
6005
  );
5588
6006
  bindings.push(
5589
6007
  {
5590
- parameter: import_format8.StandardParameter.MouthOpen,
6008
+ parameter: import_format10.StandardParameter.MouthOpen,
5591
6009
  channel: "scaleY",
5592
6010
  from: 0,
5593
6011
  to: roundTo(grow, 0.01)
5594
6012
  },
5595
6013
  {
5596
- parameter: import_format8.StandardParameter.MouthOpen,
6014
+ parameter: import_format10.StandardParameter.MouthOpen,
5597
6015
  channel: "scaleX",
5598
6016
  from: 0,
5599
6017
  to: roundTo(AMPLITUDE.mouthOpenWidth - 1, 0.01)
5600
6018
  },
5601
6019
  {
5602
- parameter: import_format8.StandardParameter.MouthOpen,
6020
+ parameter: import_format10.StandardParameter.MouthOpen,
5603
6021
  channel: "translateY",
5604
6022
  from: 0,
5605
6023
  to: -roundTo(grow * bh(b) / 2, 0.01)
@@ -5611,17 +6029,26 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5611
6029
  case "mouth_open":
5612
6030
  warps.push(mouthForm(mesh, b), mouthOpenGrow(mesh, b));
5613
6031
  bindings.push({
5614
- parameter: import_format8.StandardParameter.MouthOpen,
6032
+ parameter: import_format10.StandardParameter.MouthOpen,
5615
6033
  channel: "opacity",
5616
6034
  from: 0,
5617
6035
  to: 1
5618
6036
  });
5619
6037
  extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
5620
6038
  break;
6039
+ case "mouth_inner":
6040
+ case "lip_lower":
6041
+ case "lip_upper":
6042
+ warps.push(
6043
+ mouthFoldWarp(spec.role, mesh, knotXs, b, lips.opening),
6044
+ mouthForm(mesh, b, lips.frame),
6045
+ mouthWiden(mesh, b, lips.frame)
6046
+ );
6047
+ break;
5621
6048
  case "blush_L":
5622
6049
  case "blush_R":
5623
6050
  bindings.push({
5624
- parameter: import_format8.StandardParameter.Cheek,
6051
+ parameter: import_format10.StandardParameter.Cheek,
5625
6052
  channel: "opacity",
5626
6053
  from: AMPLITUDE.blushRest,
5627
6054
  to: 1
@@ -5632,8 +6059,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5632
6059
  if (byRole.has("hair_front")) {
5633
6060
  const amp = style.sway * AMPLITUDE.sway * hh;
5634
6061
  warps.push(
5635
- hairSway(import_format8.StandardParameter.HairSwayX, mesh, b, amp),
5636
- hairSway(import_format8.StandardParameter.HairSwayZ, mesh, b, amp)
6062
+ hairSway(import_format10.StandardParameter.HairSwayX, mesh, b, amp),
6063
+ hairSway(import_format10.StandardParameter.HairSwayZ, mesh, b, amp)
5637
6064
  );
5638
6065
  }
5639
6066
  warps.push(tiltHang(mesh, b, pivot, hairHangWeight(frame.chinY, b.y0)));
@@ -5674,29 +6101,6 @@ function hairFrontGrid(b, frame, boxes) {
5674
6101
  );
5675
6102
  return { xs, ys };
5676
6103
  }
5677
- function columnMesh(b, grid) {
5678
- const { xs } = grid;
5679
- const rows = grid.ys.length - 1;
5680
- const vertices = [];
5681
- const uvs = [];
5682
- for (let r = 0; r <= rows; r++) {
5683
- for (const x of xs) {
5684
- const u = (x - b.x0) / bw(b);
5685
- vertices.push(roundTo(u - 0.5, 1e-5), roundTo(0.5 - r / rows, 1e-5));
5686
- uvs.push(roundTo(u, 1e-5), roundTo(r / rows, 1e-5));
5687
- }
5688
- }
5689
- const n = xs.length;
5690
- const indices = [];
5691
- for (let r = 0; r < rows; r++) {
5692
- for (let c = 0; c + 1 < n; c++) {
5693
- const tl = r * n + c;
5694
- const bl = tl + n;
5695
- indices.push(bl, bl + 1, tl, tl, bl + 1, tl + 1);
5696
- }
5697
- }
5698
- return { vertices, uvs, indices };
5699
- }
5700
6104
  function meshScale(role, frame) {
5701
6105
  switch (role) {
5702
6106
  case "hair_front":
@@ -5706,10 +6110,14 @@ function meshScale(role, frame) {
5706
6110
  return Infinity;
5707
6111
  }
5708
6112
  }
5709
- function deformers(frame, grids, bodyWarp, arms) {
6113
+ function deformers(frame, grids, bodyWarp, arms, byRole) {
5710
6114
  const out = [];
5711
6115
  if (bodyWarp !== void 0) out.push(bodyWarp);
5712
6116
  for (const [role, g] of arms) out.push(...armDeformers(role, g));
6117
+ for (const [role, arm] of arms) {
6118
+ const pose = POSE_OF_ARM[role];
6119
+ if (byRole.has(pose)) out.push(forearmPoseDeformer(pose, arm));
6120
+ }
5713
6121
  const hasBody = bodyWarp !== void 0;
5714
6122
  const roll = headOwnRoll(hasBody);
5715
6123
  out.push({
@@ -5718,9 +6126,9 @@ function deformers(frame, grids, bodyWarp, arms) {
5718
6126
  pivot: { x: roundTo(frame.axisX, 0.01), y: roundTo(frame.chinY, 0.01) },
5719
6127
  bindings: [
5720
6128
  // AngleZ is clockwise-positive (Live2D's); a rotation is CCW-positive.
5721
- { parameter: import_format8.StandardParameter.AngleZ, channel: "rotate", from: roll, to: -roll },
6129
+ { parameter: import_format10.StandardParameter.AngleZ, channel: "rotate", from: roll, to: -roll },
5722
6130
  {
5723
- parameter: import_format8.StandardParameter.Breath,
6131
+ parameter: import_format10.StandardParameter.Breath,
5724
6132
  channel: "translateY",
5725
6133
  from: 0,
5726
6134
  to: roundTo(headOwnBreath(hasBody) * frame.hh, 0.01)
@@ -5738,8 +6146,8 @@ function deformers(frame, grids, bodyWarp, arms) {
5738
6146
  points: g.lattice.points
5739
6147
  },
5740
6148
  warp2d: {
5741
- parameter: import_format8.StandardParameter.AngleX,
5742
- parameterY: import_format8.StandardParameter.AngleY,
6149
+ parameter: import_format10.StandardParameter.AngleX,
6150
+ parameterY: import_format10.StandardParameter.AngleY,
5743
6151
  valuesX: X_STOPS,
5744
6152
  valuesY: Y_STOPS,
5745
6153
  keyforms2d: g.keyforms.map((offsets) => ({ offsets }))
@@ -5752,16 +6160,16 @@ function hairPhysics() {
5752
6160
  return [
5753
6161
  {
5754
6162
  id: "hairSway",
5755
- input: { parameter: import_format8.StandardParameter.AngleX, weight: 1 },
5756
- output: { parameter: import_format8.StandardParameter.HairSwayX, scale: 5 },
6163
+ input: { parameter: import_format10.StandardParameter.AngleX, weight: 1 },
6164
+ output: { parameter: import_format10.StandardParameter.HairSwayX, scale: 5 },
5757
6165
  mass: 1,
5758
6166
  stiffness: 30,
5759
6167
  damping: 3
5760
6168
  },
5761
6169
  {
5762
6170
  id: "hairTilt",
5763
- input: { parameter: import_format8.StandardParameter.AngleZ, weight: 1 },
5764
- output: { parameter: import_format8.StandardParameter.HairSwayZ, scale: 8 },
6171
+ input: { parameter: import_format10.StandardParameter.AngleZ, weight: 1 },
6172
+ output: { parameter: import_format10.StandardParameter.HairSwayZ, scale: 8 },
5765
6173
  mass: 1,
5766
6174
  stiffness: 25,
5767
6175
  damping: 2.5
@@ -5771,47 +6179,55 @@ function hairPhysics() {
5771
6179
  function declareParameters(roles) {
5772
6180
  const out = [];
5773
6181
  const add = (id, name, min, max, def) => out.push({ id, name, min, max, default: def });
5774
- add(import_format8.StandardParameter.AngleX, "Head Angle", -30, 30, 0);
5775
- add(import_format8.StandardParameter.AngleY, "Head Angle Y", -30, 30, 0);
5776
- add(import_format8.StandardParameter.AngleZ, "Head Angle Z", -30, 30, 0);
6182
+ add(import_format10.StandardParameter.AngleX, "Head Angle", -30, 30, 0);
6183
+ add(import_format10.StandardParameter.AngleY, "Head Angle Y", -30, 30, 0);
6184
+ add(import_format10.StandardParameter.AngleZ, "Head Angle Z", -30, 30, 0);
5777
6185
  if (roles.has("body")) {
5778
- add(import_format8.StandardParameter.BodyAngleX, "Body Angle X", -10, 10, 0);
5779
- add(import_format8.StandardParameter.BodyAngleY, "Body Angle Y", -10, 10, 0);
5780
- add(import_format8.StandardParameter.BodyAngleZ, "Body Angle Z", -10, 10, 0);
6186
+ add(import_format10.StandardParameter.BodyAngleX, "Body Angle X", -10, 10, 0);
6187
+ add(import_format10.StandardParameter.BodyAngleY, "Body Angle Y", -10, 10, 0);
6188
+ add(import_format10.StandardParameter.BodyAngleZ, "Body Angle Z", -10, 10, 0);
5781
6189
  }
6190
+ const addPose = (side, pose, angle) => {
6191
+ add(pose, `Arm Pose ${side}`, 0, 1, 0);
6192
+ add(angle, `Arm Pose Angle ${side}`, ...POSE_ANGLE_RANGE, 0);
6193
+ };
5782
6194
  const [armMin, armMax] = ARM_RANGE;
5783
6195
  const [elbowMin, elbowMax] = ELBOW_RANGE;
5784
6196
  if (roles.has("arm_L")) {
5785
- add(import_format8.StandardParameter.ArmLeft, "Arm L", armMin, armMax, 0);
5786
- add(import_format8.StandardParameter.ElbowLeft, "Elbow L", elbowMin, elbowMax, 0);
6197
+ add(import_format10.StandardParameter.ArmLeft, "Arm L", armMin, armMax, 0);
6198
+ add(import_format10.StandardParameter.ElbowLeft, "Elbow L", elbowMin, elbowMax, 0);
6199
+ if (roles.has("forearm_pose_L"))
6200
+ addPose("L", import_format10.StandardParameter.ArmPoseLeft, import_format10.StandardParameter.ArmPoseAngleLeft);
5787
6201
  }
5788
6202
  if (roles.has("arm_R")) {
5789
- add(import_format8.StandardParameter.ArmRight, "Arm R", armMin, armMax, 0);
5790
- add(import_format8.StandardParameter.ElbowRight, "Elbow R", elbowMin, elbowMax, 0);
6203
+ add(import_format10.StandardParameter.ArmRight, "Arm R", armMin, armMax, 0);
6204
+ add(import_format10.StandardParameter.ElbowRight, "Elbow R", elbowMin, elbowMax, 0);
6205
+ if (roles.has("forearm_pose_R"))
6206
+ addPose("R", import_format10.StandardParameter.ArmPoseRight, import_format10.StandardParameter.ArmPoseAngleRight);
5791
6207
  }
5792
- add(import_format8.StandardParameter.EyeOpenLeft, "Eye L", 0, 1, 1);
5793
- add(import_format8.StandardParameter.EyeOpenRight, "Eye R", 0, 1, 1);
6208
+ add(import_format10.StandardParameter.EyeOpenLeft, "Eye L", 0, 1, 1);
6209
+ add(import_format10.StandardParameter.EyeOpenRight, "Eye R", 0, 1, 1);
5794
6210
  if ([...roles].some((r) => /^(iris|pupil|highlight)_/.test(r))) {
5795
- add(import_format8.StandardParameter.EyeballX, "Gaze X", -1, 1, 0);
5796
- add(import_format8.StandardParameter.EyeballY, "Gaze Y", -1, 1, 0);
6211
+ add(import_format10.StandardParameter.EyeballX, "Gaze X", -1, 1, 0);
6212
+ add(import_format10.StandardParameter.EyeballY, "Gaze Y", -1, 1, 0);
5797
6213
  }
5798
- add(import_format8.StandardParameter.MouthOpen, "Mouth Open", 0, 1, 0);
5799
- add(import_format8.StandardParameter.MouthForm, "Mouth Form", -1, 1, 0);
6214
+ add(import_format10.StandardParameter.MouthOpen, "Mouth Open", 0, 1, 0);
6215
+ add(import_format10.StandardParameter.MouthForm, "Mouth Form", -1, 1, 0);
5800
6216
  if (roles.has("brow_L")) {
5801
- add(import_format8.StandardParameter.BrowLeftY, "Brow L Y", -1, 1, 0);
5802
- add(import_format8.StandardParameter.BrowLeftAngle, "Brow L Angle", -1, 1, 0);
6217
+ add(import_format10.StandardParameter.BrowLeftY, "Brow L Y", -1, 1, 0);
6218
+ add(import_format10.StandardParameter.BrowLeftAngle, "Brow L Angle", -1, 1, 0);
5803
6219
  }
5804
6220
  if (roles.has("brow_R")) {
5805
- add(import_format8.StandardParameter.BrowRightY, "Brow R Y", -1, 1, 0);
5806
- add(import_format8.StandardParameter.BrowRightAngle, "Brow R Angle", -1, 1, 0);
6221
+ add(import_format10.StandardParameter.BrowRightY, "Brow R Y", -1, 1, 0);
6222
+ add(import_format10.StandardParameter.BrowRightAngle, "Brow R Angle", -1, 1, 0);
5807
6223
  }
5808
6224
  if (roles.has("blush_L") || roles.has("blush_R")) {
5809
- add(import_format8.StandardParameter.Cheek, "Cheek", 0, 1, 0);
6225
+ add(import_format10.StandardParameter.Cheek, "Cheek", 0, 1, 0);
5810
6226
  }
5811
- add(import_format8.StandardParameter.Breath, "Breath", 0, 1, 0);
6227
+ add(import_format10.StandardParameter.Breath, "Breath", 0, 1, 0);
5812
6228
  if (roles.has("hair_front")) {
5813
- add(import_format8.StandardParameter.HairSwayX, "Hair Sway X", -SWAY_RANGE, SWAY_RANGE, 0);
5814
- add(import_format8.StandardParameter.HairSwayZ, "Hair Sway Z", -SWAY_RANGE, SWAY_RANGE, 0);
6229
+ add(import_format10.StandardParameter.HairSwayX, "Hair Sway X", -SWAY_RANGE, SWAY_RANGE, 0);
6230
+ add(import_format10.StandardParameter.HairSwayZ, "Hair Sway Z", -SWAY_RANGE, SWAY_RANGE, 0);
5815
6231
  }
5816
6232
  return out;
5817
6233
  }
@@ -5859,6 +6275,7 @@ function declareParameters(roles) {
5859
6275
  createLayerSetMeasurer,
5860
6276
  denseCoreOf,
5861
6277
  detectAlphaBbox,
6278
+ forearmPoseGeometry,
5862
6279
  foregroundSpan,
5863
6280
  generateIkiFromLayerSet,
5864
6281
  headHalfOf,