@ikijs/editor 0.16.1 → 0.18.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
@@ -1559,6 +1559,14 @@ function facePlateHalfOf(layers) {
1559
1559
  return face.cropW / 2;
1560
1560
  }
1561
1561
  var STRAND_MIN_RUN_FRACTION = 0.5;
1562
+ var ROW_RUN_ROLES = /* @__PURE__ */ new Set([
1563
+ "face",
1564
+ "hair_front",
1565
+ "hair_back",
1566
+ "body",
1567
+ "arm_L",
1568
+ "arm_R"
1569
+ ]);
1562
1570
  function createLayerSetMeasurer(canvas) {
1563
1571
  for (const field of ["width", "height"]) {
1564
1572
  const value = canvas[field];
@@ -1588,7 +1596,7 @@ function createLayerSetMeasurer(canvas) {
1588
1596
  if (bbox === null) return null;
1589
1597
  const rowLeft = new Int32Array(canvasH).fill(canvasW);
1590
1598
  const rowRight = new Int32Array(canvasH).fill(-1);
1591
- const rowRuns = role === "face" || role === "hair_front" || role === "hair_back" ? [] : void 0;
1599
+ const rowRuns = ROW_RUN_ROLES.has(role) ? [] : void 0;
1592
1600
  for (let y = 0; y < canvasH; y++) {
1593
1601
  let runs;
1594
1602
  if (rowRuns !== void 0 && y >= bbox.y && y < bbox.y + bbox.h) {
@@ -1930,7 +1938,13 @@ var PROFILE_VALUES = {
1930
1938
  brow: 0.15,
1931
1939
  breath: 0.03,
1932
1940
  sway: 0.08,
1933
- blushRest: 0.4
1941
+ blushRest: 0.4,
1942
+ bodySlide: 0.1,
1943
+ bodyNarrow: 0.05,
1944
+ bodyBow: 0.06,
1945
+ bodyRoll: 4,
1946
+ bodyFollowX: 0.3,
1947
+ bodyFollowZ: 0.3
1934
1948
  };
1935
1949
  function deriveProfile(v) {
1936
1950
  const farLead = v.lead * (1 - v.fore / 2);
@@ -2024,9 +2038,32 @@ function deriveProfile(v) {
2024
2038
  * slide, to the next 0.05. */
2025
2039
  hiddenNeck: Math.ceil(30 * (v.slide + v.lead) - 1e-9) / 20
2026
2040
  };
2027
- return { TURN: turn, NOD: nod, ROLL_DEG: v.rollDeg, AMPLITUDE: amplitude };
2041
+ const body = {
2042
+ /** BodyAngleX: the upper body's slide, hh. */
2043
+ slide: v.bodySlide,
2044
+ /** BodyAngleX: how much narrower the upper body gets, either way. */
2045
+ narrow: v.bodyNarrow,
2046
+ /** BodyAngleY: the upper body's rise (+) or bow (−), hh. */
2047
+ bow: v.bodyBow,
2048
+ /** BodyAngleZ: the upper body's roll about the hips, degrees. */
2049
+ rollDeg: v.bodyRoll,
2050
+ /** AngleX: the body's follow, as a share of BodyAngleX's motion. */
2051
+ followX: v.bodyFollowX,
2052
+ /** AngleZ: the body's follow, as a share of its roll. */
2053
+ followZ: v.bodyFollowZ,
2054
+ /** The roll the follow gives the body at AngleZ ±30, degrees: β, the
2055
+ * share of `ROLL_DEG` the head's own roll leaves to the body. */
2056
+ followRoll: v.bodyFollowZ * v.bodyRoll
2057
+ };
2058
+ return {
2059
+ TURN: turn,
2060
+ NOD: nod,
2061
+ ROLL_DEG: v.rollDeg,
2062
+ AMPLITUDE: amplitude,
2063
+ BODY: body
2064
+ };
2028
2065
  }
2029
- var { TURN, NOD, ROLL_DEG, AMPLITUDE } = deriveProfile(PROFILE_VALUES);
2066
+ var { TURN, NOD, ROLL_DEG, AMPLITUDE, BODY } = deriveProfile(PROFILE_VALUES);
2030
2067
  var STYLE_KNOBS = [
2031
2068
  "turn",
2032
2069
  "featureLead",
@@ -2056,242 +2093,27 @@ var TurnTargetError = class extends Error {
2056
2093
  this.name = "TurnTargetError";
2057
2094
  }
2058
2095
  };
2096
+ var LayerGeometryError = class extends Error {
2097
+ constructor(message) {
2098
+ super(message);
2099
+ this.name = "LayerGeometryError";
2100
+ }
2101
+ };
2059
2102
  var DEFAULT_TURN_TARGETS = Object.freeze({
2060
2103
  eyeShift: round2((TURN.eyeFar + TURN.eyeNear) / 2 - TURN.hairBack),
2061
2104
  farEyeRatio: round2(TURN.eyeFarScale / TURN.eyeNearScale),
2062
2105
  silhouetteRatio: 1
2063
2106
  });
2064
2107
 
2065
- // src/auto-rig/roles.ts
2066
- var ROLE_TABLE = [
2067
- { role: "hair_back", family: "hair_back" },
2068
- { role: "body", family: "body" },
2069
- { role: "face", family: "face", required: true },
2070
- { role: "blush_L", family: "face" },
2071
- { role: "blush_R", family: "face" },
2072
- { role: "nose", family: "nose" },
2073
- { role: "mouth", family: "mouth", required: true },
2074
- { role: "mouth_open", family: "mouth" },
2075
- { role: "eye_L", family: "eye_L", required: true },
2076
- { role: "eye_R", family: "eye_R", required: true },
2077
- { role: "iris_L", family: "eye_L" },
2078
- { role: "iris_R", family: "eye_R" },
2079
- { role: "pupil_L", family: "eye_L" },
2080
- { role: "pupil_R", family: "eye_R" },
2081
- { role: "highlight_L", family: "eye_L" },
2082
- { role: "highlight_R", family: "eye_R" },
2083
- { role: "lash_lower_L", family: "eye_L" },
2084
- { role: "lash_lower_R", family: "eye_R" },
2085
- { role: "lash_L", family: "eye_L" },
2086
- { role: "lash_R", family: "eye_R" },
2087
- { role: "brow_L", family: "brow_L" },
2088
- { role: "brow_R", family: "brow_R" },
2089
- { role: "hair_front", family: "hair_front" }
2090
- ];
2091
- var REQUIRED_ROLES = ROLE_TABLE.filter(
2092
- (r) => r.required
2093
- ).map((r) => r.role);
2094
- var SPEC_BY_ROLE = new Map(ROLE_TABLE.map((r, i) => [r.role, { ...r, i }]));
2095
- function roleSpec(role) {
2096
- const spec = SPEC_BY_ROLE.get(role);
2097
- if (spec === void 0) throw new Error(`auto-rig: unknown role "${role}"`);
2098
- return spec;
2099
- }
2100
- var ALIASES = {
2101
- eyebrow_L: "brow_L",
2102
- eyebrow_R: "brow_R",
2103
- eye_white_L: "eye_L",
2104
- eye_white_R: "eye_R"
2105
- };
2106
- function roleOf(fileName) {
2107
- const role = fileName.replace(/\.[^.]+$/, "").toLowerCase().replace(/[-\s]+/g, "_").replace(/_([lr])$/, (_, s) => `_${s.toUpperCase()}`);
2108
- return ALIASES[role] ?? role;
2109
- }
2110
- function parseLayerRoles(fileNames) {
2111
- const seen = /* @__PURE__ */ new Map();
2112
- const out = [];
2113
- for (const fileName of fileNames) {
2114
- const role = roleOf(fileName);
2115
- if (!SPEC_BY_ROLE.has(role)) {
2116
- throw new Error(
2117
- `auto-rig: "${fileName}" names no known role (expected one of ${ROLE_TABLE.map((r) => r.role).join(", ")})`
2118
- );
2119
- }
2120
- const prior = seen.get(role);
2121
- if (prior !== void 0) {
2122
- throw new Error(
2123
- `auto-rig: role "${role}" is named twice ("${prior}" and "${fileName}")`
2124
- );
2125
- }
2126
- seen.set(role, fileName);
2127
- out.push({ role, fileName });
2128
- }
2129
- const missing = REQUIRED_ROLES.filter((r) => !seen.has(r));
2130
- if (missing.length > 0) {
2131
- throw new Error(
2132
- `auto-rig: missing required role${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`
2133
- );
2134
- }
2135
- return out;
2136
- }
2137
-
2138
- // src/auto-rig/generate.ts
2108
+ // src/auto-rig/arms.ts
2139
2109
  import {
2140
- IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
2141
- StandardParameter as P4,
2142
- parseIkiModel as parseIkiModel3
2110
+ StandardParameter as P3
2143
2111
  } from "@ikijs/format";
2144
2112
 
2145
- // src/auto-rig/animations.ts
2113
+ // src/auto-rig/body.ts
2146
2114
  import {
2147
- StandardParameter as P
2115
+ StandardParameter as P2
2148
2116
  } from "@ikijs/format";
2149
- function browsY(y) {
2150
- return [
2151
- { parameter: P.BrowLeftY, value: y, blend: "add" },
2152
- { parameter: P.BrowRightY, value: y, blend: "add" }
2153
- ];
2154
- }
2155
- function browsTilt(innerDown) {
2156
- return [
2157
- { parameter: P.BrowLeftAngle, value: innerDown, blend: "add" },
2158
- { parameter: P.BrowRightAngle, value: -innerDown, blend: "add" }
2159
- ];
2160
- }
2161
- function cheek(value) {
2162
- return { parameter: P.Cheek, value, blend: "add" };
2163
- }
2164
- var BROWS = [P.BrowLeftY, P.BrowRightY, P.BrowLeftAngle, P.BrowRightAngle];
2165
- var EXPRESSIONS = [
2166
- {
2167
- id: "smile",
2168
- description: "Smiling and pleased: warm, friendly, content. For greetings, thanks, agreement and gentle happiness.",
2169
- parameters: [
2170
- { parameter: P.MouthForm, value: 1, blend: "add" },
2171
- ...browsY(0.25),
2172
- cheek(0.4)
2173
- ]
2174
- },
2175
- {
2176
- id: "laugh",
2177
- description: "Laughing, eyes shut and mouth open: delighted, amused. For jokes, playfulness and big joy.",
2178
- parameters: [
2179
- { parameter: P.EyeOpenLeft, value: 0, blend: "multiply" },
2180
- { parameter: P.EyeOpenRight, value: 0, blend: "multiply" },
2181
- { parameter: P.MouthForm, value: 1, blend: "add" },
2182
- { parameter: P.MouthOpen, value: 0.8, blend: "overwrite" },
2183
- ...browsY(0.4),
2184
- cheek(0.65)
2185
- ]
2186
- },
2187
- {
2188
- id: "angry",
2189
- description: "Angry or annoyed, frowning. For irritation, frustration, indignation or scolding.",
2190
- needs: BROWS,
2191
- parameters: [
2192
- ...browsY(-0.5),
2193
- ...browsTilt(0.8),
2194
- { parameter: P.MouthForm, value: -0.9, blend: "add" }
2195
- ]
2196
- },
2197
- {
2198
- id: "sad",
2199
- description: "Sad or disappointed, downcast. For sorrow, regret, apology or sympathy.",
2200
- needs: BROWS,
2201
- parameters: [
2202
- ...browsY(0.35),
2203
- ...browsTilt(-0.8),
2204
- { parameter: P.MouthForm, value: -0.8, blend: "add" }
2205
- ]
2206
- },
2207
- {
2208
- id: "surprised",
2209
- description: "Surprised, mouth dropped open: startled, amazed. For shock, sudden news or disbelief.",
2210
- parameters: [
2211
- ...browsY(0.8),
2212
- { parameter: P.MouthOpen, value: 0.9, blend: "overwrite" }
2213
- ]
2214
- },
2215
- {
2216
- id: "shy",
2217
- description: "Shy or embarrassed: bashful, flustered. For being praised, teased or caught off guard.",
2218
- needs: [P.Cheek],
2219
- parameters: [
2220
- cheek(1),
2221
- // Gaze down.
2222
- { parameter: P.EyeballY, value: -0.65, blend: "add" },
2223
- { parameter: P.MouthForm, value: 0.4, blend: "add" },
2224
- ...browsTilt(-0.25)
2225
- ]
2226
- }
2227
- ];
2228
- function defaultExpressions(declared) {
2229
- return EXPRESSIONS.filter(
2230
- (e) => e.needs === void 0 || e.needs.some((id) => declared.has(id))
2231
- ).map(({ needs: _needs, ...e }) => ({
2232
- ...e,
2233
- parameters: e.parameters.filter((t) => declared.has(t.parameter))
2234
- }));
2235
- }
2236
- var DEFAULT_MOTIONS = {
2237
- Nod: [
2238
- {
2239
- description: "Nods yes: agreement, acknowledgement, understanding.",
2240
- duration: 1,
2241
- curves: [
2242
- {
2243
- parameter: P.AngleY,
2244
- keys: [
2245
- [0, 0],
2246
- [0.3, -18],
2247
- [0.6, 4],
2248
- [0.8, -1],
2249
- [1, 0]
2250
- ]
2251
- }
2252
- ]
2253
- }
2254
- ],
2255
- Shake: [
2256
- {
2257
- description: "Shakes the head no: disagreement, refusal, disbelief.",
2258
- duration: 1.2,
2259
- // The first swing peaks at 0.2 s, which the default fade would still
2260
- // be damping.
2261
- fadeIn: 0.15,
2262
- curves: [
2263
- {
2264
- parameter: P.AngleX,
2265
- keys: [
2266
- [0, 0],
2267
- [0.2, -18],
2268
- [0.45, 18],
2269
- [0.7, -16],
2270
- [0.95, 10],
2271
- [1.2, 0]
2272
- ]
2273
- }
2274
- ]
2275
- }
2276
- ],
2277
- Tilt: [
2278
- {
2279
- description: "Tilts the head to one side and back: curiosity, puzzlement, a question.",
2280
- duration: 1.4,
2281
- curves: [
2282
- {
2283
- parameter: P.AngleZ,
2284
- keys: [
2285
- [0, 0],
2286
- [0.35, 20],
2287
- [1, 20],
2288
- [1.4, 0]
2289
- ]
2290
- }
2291
- ]
2292
- }
2293
- ]
2294
- };
2295
2117
 
2296
2118
  // src/auto-rig/layout.ts
2297
2119
  function boxOfLayer(layer) {
@@ -2366,49 +2188,949 @@ function meshPoints(mesh, box) {
2366
2188
  return out;
2367
2189
  }
2368
2190
 
2369
- // src/auto-rig/checks.ts
2370
- var finite = (v) => typeof v === "number" && Number.isFinite(v);
2371
- var isObject = (v) => typeof v === "object" && v !== null;
2372
- var MIN_FACE_PX = 24;
2373
- function checkLayers(layers, canvas) {
2374
- if (!Array.isArray(layers) || layers.length === 0) {
2375
- throw new Error(
2376
- "auto-rig: generateIkiFromLayerSet: layers must be a non-empty array"
2377
- );
2378
- }
2379
- if (!(finite(canvas?.width) && canvas.width > 0)) {
2380
- throw new Error("auto-rig: canvas.width must be a positive number");
2381
- }
2382
- if (!(finite(canvas?.height) && canvas.height > 0)) {
2383
- throw new Error("auto-rig: canvas.height must be a positive number");
2384
- }
2385
- const seen = /* @__PURE__ */ new Set();
2386
- for (const l of layers) {
2387
- roleSpec(l.role);
2388
- if (seen.has(l.role)) {
2389
- throw new Error(`auto-rig: role "${l.role}" appears twice`);
2390
- }
2391
- seen.add(l.role);
2392
- const at = `auto-rig: layer "${l.fileName}"`;
2393
- const { x, y, w, h } = l.bbox ?? {};
2394
- if (![x, y, w, h].every(finite) || w <= 0 || h <= 0 || x < 0 || y < 0 || x + w > canvas.width || y + h > canvas.height) {
2395
- throw new Error(
2396
- `${at} has a bbox outside its ${canvas.width}x${canvas.height} canvas`
2397
- );
2398
- }
2399
- if (l.canvasW !== canvas.width || l.canvasH !== canvas.height) {
2400
- throw new Error(
2401
- `${at} was measured on a ${l.canvasW}x${l.canvasH} canvas, not ${canvas.width}x${canvas.height}`
2402
- );
2403
- }
2404
- if (l.cropW !== w || l.cropH !== h) {
2405
- throw new Error(
2406
- `${at}: crop ${l.cropW}x${l.cropH} is not its bbox ${w}x${h}`
2407
- );
2408
- }
2409
- if (l.role === "face" && (w < MIN_FACE_PX || h < MIN_FACE_PX)) {
2410
- throw new Error(
2411
- `${at}: the face is ${w}x${h} px; the face plate needs at least ${MIN_FACE_PX} px each way to be meshed`
2191
+ // src/auto-rig/drivers.ts
2192
+ import {
2193
+ StandardParameter as P
2194
+ } from "@ikijs/format";
2195
+ var SWAY_RANGE = 20;
2196
+ var SWAY_CURVE = 1.4;
2197
+ var HAIR_HANG = 0.35;
2198
+ var Z_STOPS = [-30, 0, 30];
2199
+ function localWarp(parameter, mesh, box, values, f) {
2200
+ const pts = meshPoints(mesh, box);
2201
+ const w = bw(box);
2202
+ const h = bh(box);
2203
+ return {
2204
+ parameter,
2205
+ keyforms: values.map((value) => ({
2206
+ value,
2207
+ offsets: pts.flatMap((p, i) => {
2208
+ const [dx, dy] = f(p, value, i);
2209
+ return [roundTo(dx / w, 1e-4), roundTo(dy / h, 1e-4)];
2210
+ })
2211
+ }))
2212
+ };
2213
+ }
2214
+ var rollOf = (v, deg = ROLL_DEG) => -v / 30 * deg * DEG;
2215
+ function motionReach(warps, mesh, box, extra = [0, 0]) {
2216
+ const out = {
2217
+ left: extra[0],
2218
+ right: extra[0],
2219
+ bottom: extra[1],
2220
+ top: extra[1]
2221
+ };
2222
+ if (mesh === void 0) return out;
2223
+ const pts = meshPoints(mesh, box);
2224
+ for (const warp of warps ?? []) {
2225
+ let l = 0;
2226
+ let r = 0;
2227
+ let b = 0;
2228
+ let t = 0;
2229
+ for (const k of warp.keyforms) {
2230
+ pts.forEach(([x, y], i) => {
2231
+ const px = x + k.offsets[2 * i] * bw(box);
2232
+ const py = y + k.offsets[2 * i + 1] * bh(box);
2233
+ l = Math.max(l, box.x0 - px);
2234
+ r = Math.max(r, px - box.x1);
2235
+ b = Math.max(b, box.y0 - py);
2236
+ t = Math.max(t, py - box.y1);
2237
+ });
2238
+ }
2239
+ out.left += l;
2240
+ out.right += r;
2241
+ out.bottom += b;
2242
+ out.top += t;
2243
+ }
2244
+ return out;
2245
+ }
2246
+ function blinkFold(param, mesh, box, travel, eye = box) {
2247
+ const yc = eye.y1 - travel * bh(eye);
2248
+ return localWarp(
2249
+ param,
2250
+ mesh,
2251
+ box,
2252
+ [0, 1],
2253
+ ([, y], v) => v === 1 ? [0, 0] : [0, yc - y]
2254
+ );
2255
+ }
2256
+ function lashFold(param, mesh, lash, eye, travel) {
2257
+ const yc = eye.y1 - travel * bh(eye) - 0.04 * bh(eye);
2258
+ const c = cx(lash);
2259
+ const half = bw(lash) / 2;
2260
+ const sag = 0.3 * bh(lash);
2261
+ return localWarp(param, mesh, lash, [0, 1], ([x, y], v) => {
2262
+ if (v === 1) return [0, 0];
2263
+ const u = (x - c) / half;
2264
+ return [0, yc + (y - lash.y0) * 0.5 - sag * (1 - u * u) - y];
2265
+ });
2266
+ }
2267
+ function gazeBindings(irisW, share = 1) {
2268
+ const gx = AMPLITUDE.gazeX * irisW * share;
2269
+ const gy = AMPLITUDE.gazeY * irisW * share;
2270
+ return [
2271
+ {
2272
+ parameter: P.EyeballX,
2273
+ channel: "translateX",
2274
+ from: -round1(gx),
2275
+ to: round1(gx)
2276
+ },
2277
+ {
2278
+ parameter: P.EyeballY,
2279
+ channel: "translateY",
2280
+ from: -round1(gy),
2281
+ to: round1(gy)
2282
+ }
2283
+ ];
2284
+ }
2285
+ function gazeReach(irisW, share = 1) {
2286
+ return [AMPLITUDE.gazeX * irisW * share, AMPLITUDE.gazeY * irisW * share];
2287
+ }
2288
+ function browReach(hh) {
2289
+ return AMPLITUDE.brow * hh;
2290
+ }
2291
+ function browBindings(side, reach) {
2292
+ const y = side === "L" ? P.BrowLeftY : P.BrowRightY;
2293
+ const a = side === "L" ? P.BrowLeftAngle : P.BrowRightAngle;
2294
+ return [
2295
+ {
2296
+ parameter: y,
2297
+ channel: "translateY",
2298
+ from: -round1(reach),
2299
+ to: round1(reach)
2300
+ },
2301
+ { parameter: a, channel: "rotate", from: -BROW_TILT, to: BROW_TILT }
2302
+ ];
2303
+ }
2304
+ var BROW_TILT = 15;
2305
+ function mouthForm(mesh, box) {
2306
+ const a = 0.3 * bh(box);
2307
+ const c = cx(box);
2308
+ const half = bw(box) / 2;
2309
+ return localWarp(P.MouthForm, mesh, box, [-1, 0, 1], ([x], v) => {
2310
+ if (v === 0) return [0, 0];
2311
+ const u = (x - c) / half;
2312
+ const widen = v > 0 ? 0.05 : 0.03;
2313
+ return [v * widen * (x - c), v * a * (u * u - 0.3)];
2314
+ });
2315
+ }
2316
+ function mouthOpenGrow(mesh, box) {
2317
+ const c = cx(box);
2318
+ return localWarp(
2319
+ P.MouthOpen,
2320
+ mesh,
2321
+ box,
2322
+ [0, 1],
2323
+ ([x, y], v) => v === 1 ? [(AMPLITUDE.mouthOpenWidth - 1) * (x - c), 0] : [0, box.y1 + (y - box.y1) * 0.2 - y]
2324
+ );
2325
+ }
2326
+ function mouthWiden(mesh, box) {
2327
+ const c = cx(box);
2328
+ return localWarp(P.MouthOpen, mesh, box, [0, 1], ([x], v) => [
2329
+ v * (AMPLITUDE.mouthOpenWidth - 1) * (x - c),
2330
+ 0
2331
+ ]);
2332
+ }
2333
+ function hairSway(param, mesh, box, amp) {
2334
+ return localWarp(
2335
+ param,
2336
+ mesh,
2337
+ box,
2338
+ [-SWAY_RANGE, 0, SWAY_RANGE],
2339
+ ([, y], v) => {
2340
+ const t = Math.max(0, (box.y1 - y) / bh(box));
2341
+ return [v / SWAY_RANGE * amp * t ** SWAY_CURVE, 0];
2342
+ }
2343
+ );
2344
+ }
2345
+ function tiltHang(mesh, box, pivot, weight, deg = ROLL_DEG) {
2346
+ return localWarp(P.AngleZ, mesh, box, Z_STOPS, ([x, y], v, i) => {
2347
+ const g = weight(y, i);
2348
+ if (g === 0 || v === 0) return [0, 0];
2349
+ const a = -g * rollOf(v, deg);
2350
+ const px = x - pivot[0];
2351
+ const py = y - pivot[1];
2352
+ return [
2353
+ pivot[0] + px * Math.cos(a) - py * Math.sin(a) - x,
2354
+ pivot[1] + px * Math.sin(a) + py * Math.cos(a) - y
2355
+ ];
2356
+ });
2357
+ }
2358
+ function hairHangWeight(pivotY, bottom) {
2359
+ if (bottom >= pivotY) return () => 0;
2360
+ return (y) => HAIR_HANG * smoothstep(pivotY, bottom, y);
2361
+ }
2362
+ var round1 = (v) => roundTo(v, 0.1);
2363
+
2364
+ // src/auto-rig/grid.ts
2365
+ var X_STOPS = [-30, 0, 30];
2366
+ var Y_STOPS = [-30, 0, 30];
2367
+ function lattice(box, cell, min, max) {
2368
+ const cols = cellsFor(box.x1 - box.x0, cell, min, max);
2369
+ const rows = cellsFor(box.y1 - box.y0, cell, min, max);
2370
+ const points = [];
2371
+ for (let r = 0; r <= rows; r++) {
2372
+ const y = box.y1 - (box.y1 - box.y0) * r / rows;
2373
+ for (let c = 0; c <= cols; c++) {
2374
+ points.push(
2375
+ roundTo(box.x0 + (box.x1 - box.x0) * c / cols, 0.01),
2376
+ roundTo(y, 0.01)
2377
+ );
2378
+ }
2379
+ }
2380
+ return { box, cols, rows, points };
2381
+ }
2382
+ function bake(l, field) {
2383
+ const out = [];
2384
+ for (const ay of Y_STOPS) {
2385
+ for (const ax of X_STOPS) {
2386
+ const offsets = [];
2387
+ for (let k = 0; k < l.points.length; k += 2) {
2388
+ if (ax === 0 && ay === 0) {
2389
+ offsets.push(0, 0);
2390
+ continue;
2391
+ }
2392
+ const [dx, dy] = field(l.points[k], l.points[k + 1], ax, ay);
2393
+ offsets.push(roundTo(dx, 0.1), roundTo(dy, 0.1));
2394
+ }
2395
+ out.push(offsets);
2396
+ }
2397
+ }
2398
+ return out;
2399
+ }
2400
+ function keyformAt(keyforms, ax) {
2401
+ const j = Y_STOPS.indexOf(0);
2402
+ const row = X_STOPS.map((_, i) => keyforms[j * X_STOPS.length + i]);
2403
+ return blendStops(X_STOPS, row, ax);
2404
+ }
2405
+ function blendStops(values, offsets, v) {
2406
+ const x = clamp(v, values[0], values[values.length - 1]);
2407
+ let i = 0;
2408
+ while (i < values.length - 2 && x > values[i + 1]) i++;
2409
+ const t = (x - values[i]) / (values[i + 1] - values[i]);
2410
+ return offsets[i].map((o, k) => o + (offsets[i + 1][k] - o) * t);
2411
+ }
2412
+ function land(l, offsets, x, y) {
2413
+ const { cols, rows, points } = l;
2414
+ const stride = cols + 1;
2415
+ let col = cols - 1;
2416
+ for (let c = 0; c < cols; c++) {
2417
+ if (x < points[(c + 1) * 2]) {
2418
+ col = c;
2419
+ break;
2420
+ }
2421
+ }
2422
+ let row = rows - 1;
2423
+ for (let r = 0; r < rows; r++) {
2424
+ if (y > points[(r + 1) * stride * 2 + 1]) {
2425
+ row = r;
2426
+ break;
2427
+ }
2428
+ }
2429
+ const x0 = points[col * 2];
2430
+ const x1 = points[(col + 1) * 2];
2431
+ const yT = points[row * stride * 2 + 1];
2432
+ const yB = points[(row + 1) * stride * 2 + 1];
2433
+ const s = clamp((x - x0) / (x1 - x0), 0, 1);
2434
+ const t = clamp((yT - y) / (yT - yB), 0, 1);
2435
+ const at = (r, c) => {
2436
+ const k = (r * stride + c) * 2;
2437
+ return [points[k] + offsets[k], points[k + 1] + offsets[k + 1]];
2438
+ };
2439
+ const p00 = at(row, col);
2440
+ const p10 = at(row, col + 1);
2441
+ const p01 = at(row + 1, col);
2442
+ const p11 = at(row + 1, col + 1);
2443
+ const topX = p00[0] + (p10[0] - p00[0]) * s;
2444
+ const topY = p00[1] + (p10[1] - p00[1]) * s;
2445
+ const botX = p01[0] + (p11[0] - p01[0]) * s;
2446
+ const botY = p01[1] + (p11[1] - p01[1]) * s;
2447
+ return [topX + (botX - topX) * t, topY + (botY - topY) * t];
2448
+ }
2449
+
2450
+ // src/auto-rig/body.ts
2451
+ var BODY_WARP_ID = "bodyWarp";
2452
+ var BODY_GRID_PAD = 6;
2453
+ var CHEST_DROP = 0.8;
2454
+ var HIP_BAND = [0.35, 0.75];
2455
+ var LEG_MIN = 0.1;
2456
+ var HIP_FRACTION = 0.5;
2457
+ var BODY_MESH_PX = 48;
2458
+ var BODY_STOPS = [-10, 0, 10];
2459
+ var FOLD_MARGIN = 0.01;
2460
+ function hipLine(layer) {
2461
+ const { bbox, canvasH, rowRuns } = layer;
2462
+ const top = canvasH / 2 - bbox.y;
2463
+ if (rowRuns !== void 0) {
2464
+ const legMin = LEG_MIN * bbox.w;
2465
+ const last = Math.floor(HIP_BAND[1] * bbox.h);
2466
+ for (let k = Math.ceil(HIP_BAND[0] * bbox.h); k <= last; k++) {
2467
+ const runs = rowRuns[k];
2468
+ let legs = 0;
2469
+ for (let i = 0; i < runs.length; i += 2) {
2470
+ if (runs[i + 1] - runs[i] >= legMin) legs++;
2471
+ }
2472
+ if (legs >= 2) return roundTo(top - k, 0.01);
2473
+ }
2474
+ }
2475
+ if (bbox.y + bbox.h === canvasH) return roundTo(top - bbox.h, 0.01);
2476
+ return roundTo(top - HIP_FRACTION * bbox.h, 0.01);
2477
+ }
2478
+ function bodyLattice(body, chin, hh, pivots, anchor) {
2479
+ const b = boxOfLayer(body);
2480
+ const g = (v) => Math.round(roundTo(v, 0.01) * 100);
2481
+ const pad = BODY_GRID_PAD * 100;
2482
+ const all = [chin, ...pivots];
2483
+ const a = g(anchor);
2484
+ const p = Math.min(...all.map((q) => g(q.y)));
2485
+ if (p - a < 2) {
2486
+ throw new LayerGeometryError(
2487
+ `auto-rig: layer "${body.fileName}": its hips (y ${anchor}) leave no row line strictly between them and the lowest of the head's and shoulders' pivots (y ${p / 100}), so the body warp cannot plant the legs below the pivots`
2488
+ );
2489
+ }
2490
+ let full = Math.min(g(chin.y - CHEST_DROP * hh), p - pad);
2491
+ if (full - a < pad) {
2492
+ full = Math.min(Math.max(Math.round((a + p) / 2), a + 1), p - 1);
2493
+ }
2494
+ const top = Math.max(g(b.y1), ...all.map((q) => g(q.y))) + pad;
2495
+ const bottom = Math.min(a, g(b.y0)) - pad;
2496
+ const split = (from, to, cells) => {
2497
+ const n = Math.max(1, Math.min(cells, from - to));
2498
+ const out = [from];
2499
+ for (let i = 1; i < n; i++) {
2500
+ out.push(Math.round(from - (from - to) * i / n));
2501
+ }
2502
+ out.push(to);
2503
+ return out;
2504
+ };
2505
+ const bandA = split(top, full, cellsFor((top - full) / 100, 96, 1, 8));
2506
+ const ramp = split(full, a, cellsFor((full - a) / 100, 96, 2, 8));
2507
+ const rows = [...bandA, ...ramp.slice(1, -1)];
2508
+ const ys = [...rows.map((y) => y / 100), anchor, bottom / 100];
2509
+ const weights = [...rows.map((y) => smoothstep(a, full, y)), 0, 0];
2510
+ const x0 = Math.min(b.x0, ...all.map((q) => q.x)) - BODY_GRID_PAD;
2511
+ const x1 = Math.max(b.x1, ...all.map((q) => q.x)) + BODY_GRID_PAD;
2512
+ const cols = cellsFor(x1 - x0, 96, 2, 8);
2513
+ const xs = [];
2514
+ for (let c = 0; c <= cols; c++) {
2515
+ xs.push(roundTo(x0 + (x1 - x0) * c / cols, 0.01));
2516
+ }
2517
+ return { xs, ys, weights, yFull: full / 100 };
2518
+ }
2519
+ function bodyMesh(b, rows) {
2520
+ const lines = [b.y1, ...rows.filter((y) => y > b.y0 && y < b.y1), b.y0];
2521
+ const ys = [lines[0]];
2522
+ for (let i = 1; i < lines.length; i++) {
2523
+ const [from, to] = [lines[i - 1], lines[i]];
2524
+ const n = Math.ceil((from - to) / BODY_MESH_PX);
2525
+ for (let k = 1; k < n; k++) ys.push(from - (from - to) * k / n);
2526
+ ys.push(to);
2527
+ }
2528
+ const cols = cellsFor(bw(b), BODY_MESH_PX, 2, 16);
2529
+ const vertices = [];
2530
+ const uvs = [];
2531
+ for (const y of ys) {
2532
+ const vy = roundTo((y - cy(b)) / bh(b), 1e-5);
2533
+ for (let c = 0; c <= cols; c++) {
2534
+ const vx = roundTo(c / cols - 0.5, 1e-5);
2535
+ vertices.push(vx, vy);
2536
+ uvs.push(roundTo(vx + 0.5, 1e-5), roundTo(0.5 - vy, 1e-5));
2537
+ }
2538
+ }
2539
+ const indices = [];
2540
+ for (let r = 0; r + 1 < ys.length; r++) {
2541
+ for (let c = 0; c < cols; c++) {
2542
+ const tl = r * (cols + 1) + c;
2543
+ const tr = tl + 1;
2544
+ const bl = tl + cols + 1;
2545
+ const br = bl + 1;
2546
+ indices.push(bl, br, tl, tl, br, tr);
2547
+ }
2548
+ }
2549
+ return { vertices, uvs, indices };
2550
+ }
2551
+ function buildBodyWarp({
2552
+ body,
2553
+ chin,
2554
+ hh,
2555
+ pivots
2556
+ }) {
2557
+ const b = boxOfLayer(body);
2558
+ const anchor = hipLine(body);
2559
+ const { xs, ys, weights } = bodyLattice(body, chin, hh, pivots, anchor);
2560
+ const hx = cx(b);
2561
+ const turn = (x, t) => t * BODY.slide * hh - Math.abs(t) * BODY.narrow * (x - hx);
2562
+ const roll = (x, y, deg) => {
2563
+ const c = Math.cos(deg * DEG) - 1;
2564
+ const s = Math.sin(deg * DEG);
2565
+ const [dx, dy] = [x - hx, y - anchor];
2566
+ return [c * dx - s * dy, s * dx + c * dy];
2567
+ };
2568
+ const warp = (parameter, values, field) => {
2569
+ const extreme = Math.max(...values.map(Math.abs));
2570
+ return {
2571
+ parameter,
2572
+ keyforms: values.map((value) => ({
2573
+ value,
2574
+ offsets: ys.flatMap(
2575
+ (y, r) => xs.flatMap((x) => {
2576
+ const [dx, dy] = field(x, y, value / extreme);
2577
+ return [
2578
+ roundTo(weights[r] * dx, 0.01),
2579
+ roundTo(weights[r] * dy, 0.01)
2580
+ ];
2581
+ })
2582
+ )
2583
+ }))
2584
+ };
2585
+ };
2586
+ const deformer = {
2587
+ kind: "warp",
2588
+ id: BODY_WARP_ID,
2589
+ grid: {
2590
+ cols: xs.length - 1,
2591
+ rows: ys.length - 1,
2592
+ points: ys.flatMap((y) => xs.flatMap((x) => [x, y]))
2593
+ },
2594
+ warps: [
2595
+ warp(P2.BodyAngleX, BODY_STOPS, (x, _y, t) => [turn(x, t), 0]),
2596
+ warp(P2.BodyAngleY, BODY_STOPS, (_x, _y, t) => [0, t * BODY.bow * hh]),
2597
+ warp(
2598
+ P2.BodyAngleZ,
2599
+ BODY_STOPS,
2600
+ (x, y, t) => roll(x, y, -t * BODY.rollDeg)
2601
+ ),
2602
+ warp(P2.AngleX, X_STOPS, (x, _y, t) => [BODY.followX * turn(x, t), 0]),
2603
+ warp(P2.AngleZ, Z_STOPS, (x, y, t) => roll(x, y, -t * BODY.followRoll)),
2604
+ warp(P2.Breath, [0, 1], (_x, _y, t) => [0, t * AMPLITUDE.breathBody * hh])
2605
+ ]
2606
+ };
2607
+ refuseFold(body, deformer, anchor, [chin, ...pivots]);
2608
+ return { deformer, mesh: bodyMesh(b, ys) };
2609
+ }
2610
+ function refuseFold(body, d, anchor, pivots) {
2611
+ const { cols, rows, points } = d.grid;
2612
+ const warps = d.warps ?? [];
2613
+ const stride = cols + 1;
2614
+ for (let r = 0; r < rows; r++) {
2615
+ for (let c = 0; c < cols; c++) {
2616
+ const tl = r * stride + c;
2617
+ const [tr, bl, br] = [tl + 1, tl + stride, tl + stride + 1];
2618
+ const corners = [
2619
+ [bl, br, tl],
2620
+ [br, tr, bl],
2621
+ [tr, tl, br],
2622
+ [tl, bl, tr]
2623
+ ];
2624
+ for (const [p, q, s] of corners) {
2625
+ const fold = leastArea(points, warps, p, q, s);
2626
+ if (fold.least >= FOLD_MARGIN * fold.rest) continue;
2627
+ const lowest = roundTo(Math.min(...pivots.map((v) => v.y)), 0.01);
2628
+ const pose = warps.map((w, i) => `${w.parameter} ${w.keyforms[fold.stops[i]].value}`).join(", ");
2629
+ throw new LayerGeometryError(
2630
+ `auto-rig: layer "${body.fileName}": the body is too short between its hips (y ${anchor}) and the lowest of the head's and shoulders' pivots (y ${lowest}) for its motion: its body warp's cell from x ${points[tl * 2]} to ${points[tr * 2]}, y ${points[tl * 2 + 1]} to ${points[bl * 2 + 1]}, would fold at or near ${pose}`
2631
+ );
2632
+ }
2633
+ }
2634
+ }
2635
+ }
2636
+ function leastArea(points, warps, p, q, s) {
2637
+ const moves = warps.map(
2638
+ (w) => w.keyforms.map(({ offsets: o }) => [
2639
+ o[q * 2] - o[p * 2],
2640
+ o[q * 2 + 1] - o[p * 2 + 1],
2641
+ o[s * 2] - o[p * 2],
2642
+ o[s * 2 + 1] - o[p * 2 + 1]
2643
+ ])
2644
+ );
2645
+ const cross = (e) => e[0] * e[3] - e[1] * e[2];
2646
+ const edges = [
2647
+ points[q * 2] - points[p * 2],
2648
+ points[q * 2 + 1] - points[p * 2 + 1],
2649
+ points[s * 2] - points[p * 2],
2650
+ points[s * 2 + 1] - points[p * 2 + 1]
2651
+ ];
2652
+ let least = Infinity;
2653
+ let stops = [];
2654
+ const at = [];
2655
+ const walk = (w, ax, ay, bx, by) => {
2656
+ if (w === moves.length) {
2657
+ const area = ax * by - ay * bx;
2658
+ if (area < least) [least, stops] = [area, [...at]];
2659
+ return;
2660
+ }
2661
+ moves[w].forEach(([mx, my, nx, ny], k) => {
2662
+ at[w] = k;
2663
+ walk(w + 1, ax + mx, ay + my, bx + nx, by + ny);
2664
+ });
2665
+ };
2666
+ walk(0, edges[0], edges[1], edges[2], edges[3]);
2667
+ for (const m of moves) {
2668
+ let most = 0;
2669
+ for (let k = 1; k < m.length; k++) {
2670
+ most = Math.max(most, cross(m[k].map((v, i) => v - m[k - 1][i])));
2671
+ }
2672
+ least -= most / 4;
2673
+ }
2674
+ return { rest: cross(edges), least, stops };
2675
+ }
2676
+ function headOwnRoll(hasBody) {
2677
+ return hasBody ? ROLL_DEG - BODY.followRoll : ROLL_DEG;
2678
+ }
2679
+ function headOwnBreath(hasBody) {
2680
+ return hasBody ? AMPLITUDE.breathHead - AMPLITUDE.breathBody : AMPLITUDE.breathHead;
2681
+ }
2682
+
2683
+ // src/auto-rig/arms.ts
2684
+ var ELBOW_AT = 0.42;
2685
+ var CAP_ROWS = [0.1, 0.3];
2686
+ var CAP_SEGMENTS = 12;
2687
+ var ARM_RANGE = [-30, 150];
2688
+ var ARM_PARAMS = {
2689
+ arm_L: { arm: P3.ArmLeft, elbow: P3.ElbowLeft },
2690
+ arm_R: { arm: P3.ArmRight, elbow: P3.ElbowRight }
2691
+ };
2692
+ var ARM_IDS = {
2693
+ arm_L: {
2694
+ forearm: "forearm_L",
2695
+ armDeformer: "armDeformer_L",
2696
+ forearmDeformer: "forearmDeformer_L"
2697
+ },
2698
+ arm_R: {
2699
+ forearm: "forearm_R",
2700
+ armDeformer: "armDeformer_R",
2701
+ forearmDeformer: "forearmDeformer_R"
2702
+ }
2703
+ };
2704
+ function armGeometry(layer, bodyAxisX) {
2705
+ const { bbox, canvasW, canvasH, rowRuns } = layer;
2706
+ const b = boxOfLayer(layer);
2707
+ const span = (k) => {
2708
+ const runs = rowRuns?.[k] ?? [];
2709
+ if (runs.length === 0) return [bbox.x, bbox.x + bbox.w];
2710
+ let [s2, e2] = [runs[0], runs[1]];
2711
+ for (let i = 2; i < runs.length; i += 2) {
2712
+ if (runs[i + 1] - runs[i] > e2 - s2) [s2, e2] = [runs[i], runs[i + 1]];
2713
+ }
2714
+ return [s2, e2];
2715
+ };
2716
+ const centreX = (k) => {
2717
+ const [s2, e2] = span(k);
2718
+ return (s2 + e2) / 2 - canvasW / 2;
2719
+ };
2720
+ const rowY = (k) => canvasH / 2 - (bbox.y + k + 0.5);
2721
+ const painted = rowRuns?.flatMap((runs, k) => runs.length > 0 ? [k] : []) ?? [];
2722
+ const first = painted[0] ?? 0;
2723
+ const last = painted.at(-1) ?? bbox.h - 1;
2724
+ const lo = Math.ceil(CAP_ROWS[0] * bbox.h);
2725
+ const hi = Math.max(lo, Math.floor(CAP_ROWS[1] * bbox.h));
2726
+ const widths = [];
2727
+ for (let k = lo; k <= hi; k++) {
2728
+ const [s2, e2] = span(k);
2729
+ widths.push(e2 - s2);
2730
+ }
2731
+ widths.sort((p, q) => p - q);
2732
+ const ru = widths[widths.length >> 1] / 2;
2733
+ const ks = first + ru;
2734
+ const shoulder = {
2735
+ x: roundTo(centreX(Math.round(ks)), 0.01),
2736
+ y: roundTo(rowY(ks), 0.01)
2737
+ };
2738
+ const ke = Math.round(ks + ELBOW_AT * (last - ks));
2739
+ if (ke <= ks) {
2740
+ throw new LayerGeometryError(
2741
+ `auto-rig: layer "${layer.fileName}": the arm is too short for its width to place a shoulder above an elbow (draw the arm hanging, shoulder at the top)`
2742
+ );
2743
+ }
2744
+ const elbow = { x: roundTo(centreX(ke), 0.01), y: roundTo(rowY(ke), 0.01) };
2745
+ const [s, e] = span(ke);
2746
+ const capRadius = Math.min(
2747
+ (e - s) / 2 + 1,
2748
+ elbow.x - b.x0,
2749
+ b.x1 - elbow.x,
2750
+ b.y1 - elbow.y
2751
+ );
2752
+ return {
2753
+ shoulder,
2754
+ shoulderRadius: ru,
2755
+ elbow,
2756
+ capRadius,
2757
+ side: shoulder.x > bodyAxisX ? 1 : -1
2758
+ };
2759
+ }
2760
+ function meshOf(vertices, indices) {
2761
+ const uvs = vertices.map(
2762
+ (v, i) => roundTo(i % 2 === 0 ? v + 0.5 : 0.5 - v, 1e-5)
2763
+ );
2764
+ return { vertices, uvs, indices };
2765
+ }
2766
+ function armParts(role, layer, g, order) {
2767
+ const b = boxOfLayer(layer);
2768
+ const lx = (x) => roundTo((x - cx(b)) / bw(b), 1e-5);
2769
+ const ly = (y) => roundTo((y - cy(b)) / bh(b), 1e-5);
2770
+ const seam = ly(g.elbow.y);
2771
+ const band = (top, bottom) => [
2772
+ -0.5,
2773
+ top,
2774
+ 0.5,
2775
+ top,
2776
+ -0.5,
2777
+ bottom,
2778
+ 0.5,
2779
+ bottom
2780
+ ];
2781
+ const quad = [2, 3, 0, 0, 3, 1];
2782
+ const cap = [lx(g.elbow.x), seam];
2783
+ const fan = [];
2784
+ for (let i = 0; i <= CAP_SEGMENTS; i++) {
2785
+ const a = Math.PI * i / CAP_SEGMENTS;
2786
+ cap.push(
2787
+ lx(g.elbow.x + g.capRadius * Math.cos(a)),
2788
+ ly(g.elbow.y + g.capRadius * Math.sin(a))
2789
+ );
2790
+ if (i > 0) fan.push(4, 4 + i, 5 + i);
2791
+ }
2792
+ const part = (id, deformer, at, mesh) => ({
2793
+ id,
2794
+ color: [1, 1, 1, 1],
2795
+ width: bw(b),
2796
+ height: bh(b),
2797
+ transform: { x: cx(b), y: cy(b) },
2798
+ order: at,
2799
+ deformer,
2800
+ mesh
2801
+ });
2802
+ const ids = ARM_IDS[role];
2803
+ return [
2804
+ part(role, ids.armDeformer, order, meshOf(band(0.5, seam), quad)),
2805
+ part(
2806
+ ids.forearm,
2807
+ ids.forearmDeformer,
2808
+ order + 1,
2809
+ meshOf([...band(seam, -0.5), ...cap], [...quad, ...fan])
2810
+ )
2811
+ ];
2812
+ }
2813
+ function armDeformers(role, g) {
2814
+ const ids = ARM_IDS[role];
2815
+ const params = ARM_PARAMS[role];
2816
+ const rotate = (parameter) => ({
2817
+ parameter,
2818
+ channel: "rotate",
2819
+ from: g.side * ARM_RANGE[0],
2820
+ to: g.side * ARM_RANGE[1]
2821
+ });
2822
+ return [
2823
+ {
2824
+ id: ids.armDeformer,
2825
+ parent: BODY_WARP_ID,
2826
+ pivot: { ...g.shoulder },
2827
+ bindings: [rotate(params.arm)]
2828
+ },
2829
+ {
2830
+ id: ids.forearmDeformer,
2831
+ parent: ids.armDeformer,
2832
+ pivot: { ...g.elbow },
2833
+ bindings: [rotate(params.elbow)]
2834
+ }
2835
+ ];
2836
+ }
2837
+
2838
+ // src/auto-rig/roles.ts
2839
+ var ROLE_TABLE = [
2840
+ { role: "hair_back", family: "hair_back" },
2841
+ { role: "body", family: "body" },
2842
+ { role: "arm_L", family: "arm", parts: ["arm_L", "forearm_L"] },
2843
+ { role: "arm_R", family: "arm", parts: ["arm_R", "forearm_R"] },
2844
+ { role: "face", family: "face", required: true },
2845
+ { role: "blush_L", family: "face" },
2846
+ { role: "blush_R", family: "face" },
2847
+ { role: "nose", family: "nose" },
2848
+ { role: "mouth", family: "mouth", required: true },
2849
+ { role: "mouth_open", family: "mouth" },
2850
+ { role: "eye_L", family: "eye_L", required: true },
2851
+ { role: "eye_R", family: "eye_R", required: true },
2852
+ { role: "iris_L", family: "eye_L" },
2853
+ { role: "iris_R", family: "eye_R" },
2854
+ { role: "pupil_L", family: "eye_L" },
2855
+ { role: "pupil_R", family: "eye_R" },
2856
+ { role: "highlight_L", family: "eye_L" },
2857
+ { role: "highlight_R", family: "eye_R" },
2858
+ { role: "lash_lower_L", family: "eye_L" },
2859
+ { role: "lash_lower_R", family: "eye_R" },
2860
+ { role: "lash_L", family: "eye_L" },
2861
+ { role: "lash_R", family: "eye_R" },
2862
+ { role: "brow_L", family: "brow_L" },
2863
+ { role: "brow_R", family: "brow_R" },
2864
+ { role: "hair_front", family: "hair_front" }
2865
+ ];
2866
+ var REQUIRED_ROLES = ROLE_TABLE.filter(
2867
+ (r) => r.required
2868
+ ).map((r) => r.role);
2869
+ var SPEC_BY_ROLE = new Map(ROLE_TABLE.map((r, i) => [r.role, { ...r, i }]));
2870
+ function roleSpec(role) {
2871
+ const spec = SPEC_BY_ROLE.get(role);
2872
+ if (spec === void 0) throw new Error(`auto-rig: unknown role "${role}"`);
2873
+ return spec;
2874
+ }
2875
+ function partIdsOfRole(role) {
2876
+ return roleSpec(role).parts ?? [role];
2877
+ }
2878
+ var ROLE_BY_PART = new Map(
2879
+ ROLE_TABLE.flatMap((r) => (r.parts ?? []).map((p) => [p, r.role]))
2880
+ );
2881
+ function roleOfPart(partId) {
2882
+ return ROLE_BY_PART.get(partId) ?? partId;
2883
+ }
2884
+ var ALIASES = {
2885
+ eyebrow_L: "brow_L",
2886
+ eyebrow_R: "brow_R",
2887
+ eye_white_L: "eye_L",
2888
+ eye_white_R: "eye_R"
2889
+ };
2890
+ function roleOf(fileName) {
2891
+ const role = fileName.replace(/\.[^.]+$/, "").toLowerCase().replace(/[-\s]+/g, "_").replace(/_([lr])$/, (_, s) => `_${s.toUpperCase()}`);
2892
+ return ALIASES[role] ?? role;
2893
+ }
2894
+ function checkArmsHaveBody(roles) {
2895
+ const has = new Set(roles);
2896
+ if (has.has("body")) return;
2897
+ const arm = ROLE_TABLE.find((r) => r.family === "arm" && has.has(r.role));
2898
+ if (arm !== void 0) {
2899
+ throw new Error(
2900
+ `auto-rig: ${arm.role} needs a body layer (an arm hangs from the body's shoulder)`
2901
+ );
2902
+ }
2903
+ }
2904
+ function parseLayerRoles(fileNames) {
2905
+ const seen = /* @__PURE__ */ new Map();
2906
+ const out = [];
2907
+ for (const fileName of fileNames) {
2908
+ const role = roleOf(fileName);
2909
+ if (!SPEC_BY_ROLE.has(role)) {
2910
+ throw new Error(
2911
+ `auto-rig: "${fileName}" names no known role (expected one of ${ROLE_TABLE.map((r) => r.role).join(", ")})`
2912
+ );
2913
+ }
2914
+ const prior = seen.get(role);
2915
+ if (prior !== void 0) {
2916
+ throw new Error(
2917
+ `auto-rig: role "${role}" is named twice ("${prior}" and "${fileName}")`
2918
+ );
2919
+ }
2920
+ seen.set(role, fileName);
2921
+ out.push({ role, fileName });
2922
+ }
2923
+ const missing = REQUIRED_ROLES.filter((r) => !seen.has(r));
2924
+ if (missing.length > 0) {
2925
+ throw new Error(
2926
+ `auto-rig: missing required role${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`
2927
+ );
2928
+ }
2929
+ checkArmsHaveBody(seen.keys());
2930
+ return out;
2931
+ }
2932
+
2933
+ // src/auto-rig/generate.ts
2934
+ import {
2935
+ IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
2936
+ StandardParameter as P6,
2937
+ parseIkiModel as parseIkiModel3
2938
+ } from "@ikijs/format";
2939
+
2940
+ // src/auto-rig/animations.ts
2941
+ import {
2942
+ StandardParameter as P4
2943
+ } from "@ikijs/format";
2944
+ function browsY(y) {
2945
+ return [
2946
+ { parameter: P4.BrowLeftY, value: y, blend: "add" },
2947
+ { parameter: P4.BrowRightY, value: y, blend: "add" }
2948
+ ];
2949
+ }
2950
+ function browsTilt(innerDown) {
2951
+ return [
2952
+ { parameter: P4.BrowLeftAngle, value: innerDown, blend: "add" },
2953
+ { parameter: P4.BrowRightAngle, value: -innerDown, blend: "add" }
2954
+ ];
2955
+ }
2956
+ function cheek(value) {
2957
+ return { parameter: P4.Cheek, value, blend: "add" };
2958
+ }
2959
+ var BROWS = [P4.BrowLeftY, P4.BrowRightY, P4.BrowLeftAngle, P4.BrowRightAngle];
2960
+ var EXPRESSIONS = [
2961
+ {
2962
+ id: "smile",
2963
+ description: "Smiling and pleased: warm, friendly, content. For greetings, thanks, agreement and gentle happiness.",
2964
+ parameters: [
2965
+ { parameter: P4.MouthForm, value: 1, blend: "add" },
2966
+ ...browsY(0.25),
2967
+ cheek(0.4)
2968
+ ]
2969
+ },
2970
+ {
2971
+ id: "laugh",
2972
+ description: "Laughing, eyes shut and mouth open: delighted, amused. For jokes, playfulness and big joy.",
2973
+ parameters: [
2974
+ { parameter: P4.EyeOpenLeft, value: 0, blend: "multiply" },
2975
+ { parameter: P4.EyeOpenRight, value: 0, blend: "multiply" },
2976
+ { parameter: P4.MouthForm, value: 1, blend: "add" },
2977
+ { parameter: P4.MouthOpen, value: 0.8, blend: "overwrite" },
2978
+ ...browsY(0.4),
2979
+ cheek(0.65)
2980
+ ]
2981
+ },
2982
+ {
2983
+ id: "angry",
2984
+ description: "Angry or annoyed, frowning. For irritation, frustration, indignation or scolding.",
2985
+ needs: BROWS,
2986
+ parameters: [
2987
+ ...browsY(-0.5),
2988
+ ...browsTilt(0.8),
2989
+ { parameter: P4.MouthForm, value: -0.9, blend: "add" }
2990
+ ]
2991
+ },
2992
+ {
2993
+ id: "sad",
2994
+ description: "Sad or disappointed, downcast. For sorrow, regret, apology or sympathy.",
2995
+ needs: BROWS,
2996
+ parameters: [
2997
+ ...browsY(0.35),
2998
+ ...browsTilt(-0.8),
2999
+ { parameter: P4.MouthForm, value: -0.8, blend: "add" }
3000
+ ]
3001
+ },
3002
+ {
3003
+ id: "surprised",
3004
+ description: "Surprised, mouth dropped open: startled, amazed. For shock, sudden news or disbelief.",
3005
+ parameters: [
3006
+ ...browsY(0.8),
3007
+ { parameter: P4.MouthOpen, value: 0.9, blend: "overwrite" }
3008
+ ]
3009
+ },
3010
+ {
3011
+ id: "shy",
3012
+ description: "Shy or embarrassed: bashful, flustered. For being praised, teased or caught off guard.",
3013
+ needs: [P4.Cheek],
3014
+ parameters: [
3015
+ cheek(1),
3016
+ // Gaze down.
3017
+ { parameter: P4.EyeballY, value: -0.65, blend: "add" },
3018
+ { parameter: P4.MouthForm, value: 0.4, blend: "add" },
3019
+ ...browsTilt(-0.25)
3020
+ ]
3021
+ }
3022
+ ];
3023
+ function defaultExpressions(declared) {
3024
+ return EXPRESSIONS.filter(
3025
+ (e) => e.needs === void 0 || e.needs.some((id) => declared.has(id))
3026
+ ).map(({ needs: _needs, ...e }) => ({
3027
+ ...e,
3028
+ parameters: e.parameters.filter((t) => declared.has(t.parameter))
3029
+ }));
3030
+ }
3031
+ var DEFAULT_MOTIONS = {
3032
+ Nod: [
3033
+ {
3034
+ description: "Nods yes: agreement, acknowledgement, understanding.",
3035
+ duration: 1,
3036
+ curves: [
3037
+ {
3038
+ parameter: P4.AngleY,
3039
+ keys: [
3040
+ [0, 0],
3041
+ [0.3, -18],
3042
+ [0.6, 4],
3043
+ [0.8, -1],
3044
+ [1, 0]
3045
+ ]
3046
+ }
3047
+ ]
3048
+ }
3049
+ ],
3050
+ Shake: [
3051
+ {
3052
+ description: "Shakes the head no: disagreement, refusal, disbelief.",
3053
+ duration: 1.2,
3054
+ // The first swing peaks at 0.2 s, which the default fade would still
3055
+ // be damping.
3056
+ fadeIn: 0.15,
3057
+ curves: [
3058
+ {
3059
+ parameter: P4.AngleX,
3060
+ keys: [
3061
+ [0, 0],
3062
+ [0.2, -18],
3063
+ [0.45, 18],
3064
+ [0.7, -16],
3065
+ [0.95, 10],
3066
+ [1.2, 0]
3067
+ ]
3068
+ }
3069
+ ]
3070
+ }
3071
+ ],
3072
+ Tilt: [
3073
+ {
3074
+ description: "Tilts the head to one side and back: curiosity, puzzlement, a question.",
3075
+ duration: 1.4,
3076
+ curves: [
3077
+ {
3078
+ parameter: P4.AngleZ,
3079
+ keys: [
3080
+ [0, 0],
3081
+ [0.35, 20],
3082
+ [1, 20],
3083
+ [1.4, 0]
3084
+ ]
3085
+ }
3086
+ ]
3087
+ }
3088
+ ]
3089
+ };
3090
+
3091
+ // src/auto-rig/checks.ts
3092
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
3093
+ var isObject = (v) => typeof v === "object" && v !== null;
3094
+ var MIN_FACE_PX = 24;
3095
+ function checkLayers(layers, canvas) {
3096
+ if (!Array.isArray(layers) || layers.length === 0) {
3097
+ throw new Error(
3098
+ "auto-rig: generateIkiFromLayerSet: layers must be a non-empty array"
3099
+ );
3100
+ }
3101
+ if (!(finite(canvas?.width) && canvas.width > 0)) {
3102
+ throw new Error("auto-rig: canvas.width must be a positive number");
3103
+ }
3104
+ if (!(finite(canvas?.height) && canvas.height > 0)) {
3105
+ throw new Error("auto-rig: canvas.height must be a positive number");
3106
+ }
3107
+ const seen = /* @__PURE__ */ new Set();
3108
+ for (const l of layers) {
3109
+ roleSpec(l.role);
3110
+ if (seen.has(l.role)) {
3111
+ throw new Error(`auto-rig: role "${l.role}" appears twice`);
3112
+ }
3113
+ seen.add(l.role);
3114
+ const at = `auto-rig: layer "${l.fileName}"`;
3115
+ const { x, y, w, h } = l.bbox ?? {};
3116
+ if (![x, y, w, h].every(finite) || w <= 0 || h <= 0 || x < 0 || y < 0 || x + w > canvas.width || y + h > canvas.height) {
3117
+ throw new Error(
3118
+ `${at} has a bbox outside its ${canvas.width}x${canvas.height} canvas`
3119
+ );
3120
+ }
3121
+ if (l.canvasW !== canvas.width || l.canvasH !== canvas.height) {
3122
+ throw new Error(
3123
+ `${at} was measured on a ${l.canvasW}x${l.canvasH} canvas, not ${canvas.width}x${canvas.height}`
3124
+ );
3125
+ }
3126
+ if (l.cropW !== w || l.cropH !== h) {
3127
+ throw new Error(
3128
+ `${at}: crop ${l.cropW}x${l.cropH} is not its bbox ${w}x${h}`
3129
+ );
3130
+ }
3131
+ if (l.role === "face" && (w < MIN_FACE_PX || h < MIN_FACE_PX)) {
3132
+ throw new Error(
3133
+ `${at}: the face is ${w}x${h} px; the face plate needs at least ${MIN_FACE_PX} px each way to be meshed`
2412
3134
  );
2413
3135
  }
2414
3136
  if (l.rowHalfWidths !== void 0) {
@@ -2469,6 +3191,7 @@ function checkLayers(layers, canvas) {
2469
3191
  if (missing.length > 0) {
2470
3192
  throw new Error(`auto-rig: missing required role(s) ${missing.join(", ")}`);
2471
3193
  }
3194
+ checkArmsHaveBody(seen);
2472
3195
  }
2473
3196
  function checkHeadEdges(edges, byRole) {
2474
3197
  if (edges === void 0) return;
@@ -2551,129 +3274,43 @@ function checkStrandEdges(strand, irises, hair) {
2551
3274
  ["irisInner", inner]
2552
3275
  ]) {
2553
3276
  if (x < iris.x0 || x > iris.x1) {
2554
- throw new TurnTargetError(`${at}.${k} ${x} is off its iris's crop`);
2555
- }
2556
- }
2557
- if (sign * (outer - inner) <= 0) {
2558
- throw new TurnTargetError(
2559
- `${at}.irisOuter ${outer} is not strictly outward of irisInner ${inner}`
2560
- );
2561
- }
2562
- for (const [k, x] of [
2563
- ["runOuter", runOuter],
2564
- ["runFace", runFace]
2565
- ]) {
2566
- if (x !== null && (x < hair.x0 || x > hair.x1)) {
2567
- throw new TurnTargetError(`${at}.${k} ${x} is off hair_front's crop`);
2568
- }
2569
- }
2570
- if (sign * (runOuter - cx(iris)) <= 0) {
2571
- throw new TurnTargetError(
2572
- `${at}.runOuter ${runOuter} is not strictly outward of its iris's centre`
2573
- );
2574
- }
2575
- if (runFace !== null && sign * (runOuter - runFace) <= 0) {
2576
- throw new TurnTargetError(
2577
- `${at}.runOuter ${runOuter} is not strictly outward of runFace ${runFace}`
2578
- );
2579
- }
2580
- if (runFace !== null && sign * (runFace - cx(other)) <= 0) {
2581
- throw new TurnTargetError(
2582
- `${at}.runFace ${runFace} reaches the other iris's centre; a run that does is a fringe, runFace null`
2583
- );
2584
- }
2585
- }
2586
- }
2587
-
2588
- // src/auto-rig/context.ts
2589
- import { StandardParameter as P2 } from "@ikijs/format";
2590
-
2591
- // src/auto-rig/grid.ts
2592
- var X_STOPS = [-30, 0, 30];
2593
- var Y_STOPS = [-30, 0, 30];
2594
- function lattice(box, cell, min, max) {
2595
- const cols = cellsFor(box.x1 - box.x0, cell, min, max);
2596
- const rows = cellsFor(box.y1 - box.y0, cell, min, max);
2597
- const points = [];
2598
- for (let r = 0; r <= rows; r++) {
2599
- const y = box.y1 - (box.y1 - box.y0) * r / rows;
2600
- for (let c = 0; c <= cols; c++) {
2601
- points.push(
2602
- roundTo(box.x0 + (box.x1 - box.x0) * c / cols, 0.01),
2603
- roundTo(y, 0.01)
2604
- );
2605
- }
2606
- }
2607
- return { box, cols, rows, points };
2608
- }
2609
- function bake(l, field) {
2610
- const out = [];
2611
- for (const ay of Y_STOPS) {
2612
- for (const ax of X_STOPS) {
2613
- const offsets = [];
2614
- for (let k = 0; k < l.points.length; k += 2) {
2615
- if (ax === 0 && ay === 0) {
2616
- offsets.push(0, 0);
2617
- continue;
2618
- }
2619
- const [dx, dy] = field(l.points[k], l.points[k + 1], ax, ay);
2620
- offsets.push(roundTo(dx, 0.1), roundTo(dy, 0.1));
3277
+ throw new TurnTargetError(`${at}.${k} ${x} is off its iris's crop`);
2621
3278
  }
2622
- out.push(offsets);
2623
3279
  }
2624
- }
2625
- return out;
2626
- }
2627
- function keyformAt(keyforms, ax) {
2628
- const j = Y_STOPS.indexOf(0);
2629
- const row = X_STOPS.map((_, i) => keyforms[j * X_STOPS.length + i]);
2630
- return blendStops(X_STOPS, row, ax);
2631
- }
2632
- function blendStops(values, offsets, v) {
2633
- const x = clamp(v, values[0], values[values.length - 1]);
2634
- let i = 0;
2635
- while (i < values.length - 2 && x > values[i + 1]) i++;
2636
- const t = (x - values[i]) / (values[i + 1] - values[i]);
2637
- return offsets[i].map((o, k) => o + (offsets[i + 1][k] - o) * t);
2638
- }
2639
- function land(l, offsets, x, y) {
2640
- const { cols, rows, points } = l;
2641
- const stride = cols + 1;
2642
- let col = cols - 1;
2643
- for (let c = 0; c < cols; c++) {
2644
- if (x < points[(c + 1) * 2]) {
2645
- col = c;
2646
- break;
3280
+ if (sign * (outer - inner) <= 0) {
3281
+ throw new TurnTargetError(
3282
+ `${at}.irisOuter ${outer} is not strictly outward of irisInner ${inner}`
3283
+ );
2647
3284
  }
2648
- }
2649
- let row = rows - 1;
2650
- for (let r = 0; r < rows; r++) {
2651
- if (y > points[(r + 1) * stride * 2 + 1]) {
2652
- row = r;
2653
- break;
3285
+ for (const [k, x] of [
3286
+ ["runOuter", runOuter],
3287
+ ["runFace", runFace]
3288
+ ]) {
3289
+ if (x !== null && (x < hair.x0 || x > hair.x1)) {
3290
+ throw new TurnTargetError(`${at}.${k} ${x} is off hair_front's crop`);
3291
+ }
3292
+ }
3293
+ if (sign * (runOuter - cx(iris)) <= 0) {
3294
+ throw new TurnTargetError(
3295
+ `${at}.runOuter ${runOuter} is not strictly outward of its iris's centre`
3296
+ );
3297
+ }
3298
+ if (runFace !== null && sign * (runOuter - runFace) <= 0) {
3299
+ throw new TurnTargetError(
3300
+ `${at}.runOuter ${runOuter} is not strictly outward of runFace ${runFace}`
3301
+ );
3302
+ }
3303
+ if (runFace !== null && sign * (runFace - cx(other)) <= 0) {
3304
+ throw new TurnTargetError(
3305
+ `${at}.runFace ${runFace} reaches the other iris's centre; a run that does is a fringe, runFace null`
3306
+ );
2654
3307
  }
2655
3308
  }
2656
- const x0 = points[col * 2];
2657
- const x1 = points[(col + 1) * 2];
2658
- const yT = points[row * stride * 2 + 1];
2659
- const yB = points[(row + 1) * stride * 2 + 1];
2660
- const s = clamp((x - x0) / (x1 - x0), 0, 1);
2661
- const t = clamp((yT - y) / (yT - yB), 0, 1);
2662
- const at = (r, c) => {
2663
- const k = (r * stride + c) * 2;
2664
- return [points[k] + offsets[k], points[k + 1] + offsets[k + 1]];
2665
- };
2666
- const p00 = at(row, col);
2667
- const p10 = at(row, col + 1);
2668
- const p01 = at(row + 1, col);
2669
- const p11 = at(row + 1, col + 1);
2670
- const topX = p00[0] + (p10[0] - p00[0]) * s;
2671
- const topY = p00[1] + (p10[1] - p00[1]) * s;
2672
- const botX = p01[0] + (p11[0] - p01[0]) * s;
2673
- const botY = p01[1] + (p11[1] - p01[1]) * s;
2674
- return [topX + (botX - topX) * t, topY + (botY - topY) * t];
2675
3309
  }
2676
3310
 
3311
+ // src/auto-rig/context.ts
3312
+ import { StandardParameter as P5 } from "@ikijs/format";
3313
+
2677
3314
  // src/auto-rig/head.ts
2678
3315
  function outerEdge(runs, axisX, side) {
2679
3316
  if (runs.length === 0) return void 0;
@@ -3703,6 +4340,7 @@ function familyField(m, family) {
3703
4340
  case "hair_back":
3704
4341
  return hairField(m, m.hairBack, NOD.hairBack);
3705
4342
  case "body":
4343
+ case "arm":
3706
4344
  return () => [0, 0];
3707
4345
  }
3708
4346
  }
@@ -3802,7 +4440,7 @@ function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
3802
4440
  const family = roleSpec(role).family;
3803
4441
  const part = parts.find((p) => p.id === role);
3804
4442
  const g = grids.get(family);
3805
- const warp = part?.warps?.find((w) => w.parameter === P2.AngleX);
4443
+ const warp = part?.warps?.find((w) => w.parameter === P5.AngleX);
3806
4444
  if (part === void 0 || part.mesh === void 0 || !g && !warp) {
3807
4445
  return x + field(role)(x, y, ax, 0)[0];
3808
4446
  }
@@ -4511,179 +5149,6 @@ function buildTurn(frame, landmarks, q) {
4511
5149
  };
4512
5150
  }
4513
5151
 
4514
- // src/auto-rig/drivers.ts
4515
- import {
4516
- StandardParameter as P3
4517
- } from "@ikijs/format";
4518
- var SWAY_RANGE = 20;
4519
- var SWAY_CURVE = 1.4;
4520
- var HAIR_HANG = 0.35;
4521
- var Z_STOPS = [-30, 0, 30];
4522
- function localWarp(parameter, mesh, box, values, f) {
4523
- const pts = meshPoints(mesh, box);
4524
- const w = bw(box);
4525
- const h = bh(box);
4526
- return {
4527
- parameter,
4528
- keyforms: values.map((value) => ({
4529
- value,
4530
- offsets: pts.flatMap((p, i) => {
4531
- const [dx, dy] = f(p, value, i);
4532
- return [roundTo(dx / w, 1e-4), roundTo(dy / h, 1e-4)];
4533
- })
4534
- }))
4535
- };
4536
- }
4537
- var rollOf = (v) => -v / 30 * ROLL_DEG * DEG;
4538
- function motionReach(warps, mesh, box, extra = [0, 0]) {
4539
- const out = {
4540
- left: extra[0],
4541
- right: extra[0],
4542
- bottom: extra[1],
4543
- top: extra[1]
4544
- };
4545
- if (mesh === void 0) return out;
4546
- const pts = meshPoints(mesh, box);
4547
- for (const warp of warps ?? []) {
4548
- let l = 0;
4549
- let r = 0;
4550
- let b = 0;
4551
- let t = 0;
4552
- for (const k of warp.keyforms) {
4553
- pts.forEach(([x, y], i) => {
4554
- const px = x + k.offsets[2 * i] * bw(box);
4555
- const py = y + k.offsets[2 * i + 1] * bh(box);
4556
- l = Math.max(l, box.x0 - px);
4557
- r = Math.max(r, px - box.x1);
4558
- b = Math.max(b, box.y0 - py);
4559
- t = Math.max(t, py - box.y1);
4560
- });
4561
- }
4562
- out.left += l;
4563
- out.right += r;
4564
- out.bottom += b;
4565
- out.top += t;
4566
- }
4567
- return out;
4568
- }
4569
- function blinkFold(param, mesh, box, travel, eye = box) {
4570
- const yc = eye.y1 - travel * bh(eye);
4571
- return localWarp(
4572
- param,
4573
- mesh,
4574
- box,
4575
- [0, 1],
4576
- ([, y], v) => v === 1 ? [0, 0] : [0, yc - y]
4577
- );
4578
- }
4579
- function lashFold(param, mesh, lash, eye, travel) {
4580
- const yc = eye.y1 - travel * bh(eye) - 0.04 * bh(eye);
4581
- const c = cx(lash);
4582
- const half = bw(lash) / 2;
4583
- const sag = 0.3 * bh(lash);
4584
- return localWarp(param, mesh, lash, [0, 1], ([x, y], v) => {
4585
- if (v === 1) return [0, 0];
4586
- const u = (x - c) / half;
4587
- return [0, yc + (y - lash.y0) * 0.5 - sag * (1 - u * u) - y];
4588
- });
4589
- }
4590
- function gazeBindings(irisW, share = 1) {
4591
- const gx = AMPLITUDE.gazeX * irisW * share;
4592
- const gy = AMPLITUDE.gazeY * irisW * share;
4593
- return [
4594
- {
4595
- parameter: P3.EyeballX,
4596
- channel: "translateX",
4597
- from: -round1(gx),
4598
- to: round1(gx)
4599
- },
4600
- {
4601
- parameter: P3.EyeballY,
4602
- channel: "translateY",
4603
- from: -round1(gy),
4604
- to: round1(gy)
4605
- }
4606
- ];
4607
- }
4608
- function gazeReach(irisW, share = 1) {
4609
- return [AMPLITUDE.gazeX * irisW * share, AMPLITUDE.gazeY * irisW * share];
4610
- }
4611
- function browReach(hh) {
4612
- return AMPLITUDE.brow * hh;
4613
- }
4614
- function browBindings(side, reach) {
4615
- const y = side === "L" ? P3.BrowLeftY : P3.BrowRightY;
4616
- const a = side === "L" ? P3.BrowLeftAngle : P3.BrowRightAngle;
4617
- return [
4618
- {
4619
- parameter: y,
4620
- channel: "translateY",
4621
- from: -round1(reach),
4622
- to: round1(reach)
4623
- },
4624
- { parameter: a, channel: "rotate", from: -BROW_TILT, to: BROW_TILT }
4625
- ];
4626
- }
4627
- var BROW_TILT = 15;
4628
- function mouthForm(mesh, box) {
4629
- const a = 0.3 * bh(box);
4630
- const c = cx(box);
4631
- const half = bw(box) / 2;
4632
- return localWarp(P3.MouthForm, mesh, box, [-1, 0, 1], ([x], v) => {
4633
- if (v === 0) return [0, 0];
4634
- const u = (x - c) / half;
4635
- const widen = v > 0 ? 0.05 : 0.03;
4636
- return [v * widen * (x - c), v * a * (u * u - 0.3)];
4637
- });
4638
- }
4639
- function mouthOpenGrow(mesh, box) {
4640
- const c = cx(box);
4641
- return localWarp(
4642
- P3.MouthOpen,
4643
- mesh,
4644
- box,
4645
- [0, 1],
4646
- ([x, y], v) => v === 1 ? [(AMPLITUDE.mouthOpenWidth - 1) * (x - c), 0] : [0, box.y1 + (y - box.y1) * 0.2 - y]
4647
- );
4648
- }
4649
- function mouthWiden(mesh, box) {
4650
- const c = cx(box);
4651
- return localWarp(P3.MouthOpen, mesh, box, [0, 1], ([x], v) => [
4652
- v * (AMPLITUDE.mouthOpenWidth - 1) * (x - c),
4653
- 0
4654
- ]);
4655
- }
4656
- function hairSway(param, mesh, box, amp) {
4657
- return localWarp(
4658
- param,
4659
- mesh,
4660
- box,
4661
- [-SWAY_RANGE, 0, SWAY_RANGE],
4662
- ([, y], v) => {
4663
- const t = Math.max(0, (box.y1 - y) / bh(box));
4664
- return [v / SWAY_RANGE * amp * t ** SWAY_CURVE, 0];
4665
- }
4666
- );
4667
- }
4668
- function tiltHang(mesh, box, pivot, weight) {
4669
- return localWarp(P3.AngleZ, mesh, box, Z_STOPS, ([x, y], v, i) => {
4670
- const g = weight(y, i);
4671
- if (g === 0 || v === 0) return [0, 0];
4672
- const a = -g * rollOf(v);
4673
- const px = x - pivot[0];
4674
- const py = y - pivot[1];
4675
- return [
4676
- pivot[0] + px * Math.cos(a) - py * Math.sin(a) - x,
4677
- pivot[1] + px * Math.sin(a) + py * Math.cos(a) - y
4678
- ];
4679
- });
4680
- }
4681
- function hairHangWeight(pivotY, bottom) {
4682
- if (bottom >= pivotY) return () => 0;
4683
- return (y) => HAIR_HANG * smoothstep(pivotY, bottom, y);
4684
- }
4685
- var round1 = (v) => roundTo(v, 0.1);
4686
-
4687
5152
  // src/auto-rig/generate.ts
4688
5153
  var MESH_CELLS = {
4689
5154
  face: { px: 28, min: 4, max: 24 },
@@ -4710,6 +5175,7 @@ var WARP_ID = {
4710
5175
  mouth: "mouthWarp"
4711
5176
  };
4712
5177
  var isGridFamily = (f) => f in WARP_ID;
5178
+ var isArm = (spec) => spec.family === "arm";
4713
5179
  var GRID_PAD = 6;
4714
5180
  function generateIkiFromLayerSet(layers, canvas, options = {}) {
4715
5181
  checkLayers(layers, canvas);
@@ -4758,6 +5224,18 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
4758
5224
  chinSlide: chinSlide(fit.model)
4759
5225
  });
4760
5226
  const turn = { ...fit.model, frame };
5227
+ const arms = /* @__PURE__ */ new Map();
5228
+ for (const spec of ROLE_TABLE) {
5229
+ const layer = byRole.get(spec.role);
5230
+ if (layer === void 0 || !isArm(spec)) continue;
5231
+ arms.set(spec.role, armGeometry(layer, cx(box("body"))));
5232
+ }
5233
+ const bodyWarp = has("body") ? buildBodyWarp({
5234
+ body: byRole.get("body"),
5235
+ chin: { x: frame.axisX, y: frame.chinY },
5236
+ hh: frame.hh,
5237
+ pivots: [...arms.values()].map((g) => g.shoulder)
5238
+ }) : void 0;
4761
5239
  const parts = [];
4762
5240
  const reach = /* @__PURE__ */ new Map();
4763
5241
  let regionOf = [];
@@ -4765,6 +5243,12 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
4765
5243
  for (const spec of ROLE_TABLE) {
4766
5244
  const layer = byRole.get(spec.role);
4767
5245
  if (layer === void 0) continue;
5246
+ if (isArm(spec)) {
5247
+ parts.push(
5248
+ ...armParts(spec.role, layer, arms.get(spec.role), parts.length)
5249
+ );
5250
+ continue;
5251
+ }
4768
5252
  const built = buildPart(
4769
5253
  spec,
4770
5254
  boxOfLayer(layer),
@@ -4783,7 +5267,9 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
4783
5267
  }
4784
5268
  const grids = /* @__PURE__ */ new Map();
4785
5269
  for (const family of Object.keys(WARP_ID)) {
4786
- const members = parts.filter((p) => roleSpec(p.id).family === family);
5270
+ const members = parts.filter(
5271
+ (p) => roleSpec(roleOfPart(p.id)).family === family
5272
+ );
4787
5273
  if (members.length === 0) continue;
4788
5274
  const area = unionBoxes(
4789
5275
  members.map((p) => {
@@ -4805,13 +5291,19 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
4805
5291
  });
4806
5292
  }
4807
5293
  for (const part of parts) {
4808
- const family = roleSpec(part.id).family;
5294
+ const family = roleSpec(roleOfPart(part.id)).family;
4809
5295
  if (isGridFamily(family)) {
4810
5296
  part.deformer = WARP_ID[family];
4811
5297
  continue;
4812
5298
  }
4813
- part.deformer = family === "body" ? "bodyDeformer" : "headDeformer";
4814
- if (family === "body" || part.mesh === void 0) continue;
5299
+ if (family === "body") {
5300
+ part.deformer = BODY_WARP_ID;
5301
+ part.mesh = bodyWarp.mesh;
5302
+ continue;
5303
+ }
5304
+ if (family === "arm") continue;
5305
+ part.deformer = "headDeformer";
5306
+ if (part.mesh === void 0) continue;
4815
5307
  const own = familyField(turn, family);
4816
5308
  const fieldOf = part.id === "face" ? plateFields(turn, regionOf, own) : () => own;
4817
5309
  part.warps = [
@@ -4826,7 +5318,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
4826
5318
  canvas: { width: canvas.width, height: canvas.height },
4827
5319
  parameters,
4828
5320
  parts,
4829
- deformers: deformers(frame, grids, has("body") ? box("body") : void 0),
5321
+ deformers: deformers(frame, grids, bodyWarp?.deformer, arms),
4830
5322
  ...has("hair_front") ? { physics: hairPhysics() } : {},
4831
5323
  expressions: defaultExpressions(new Set(parameters.map((p) => p.id))),
4832
5324
  motions: DEFAULT_MOTIONS
@@ -4846,14 +5338,14 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
4846
5338
  function turnWarps(mesh, b, fieldOf) {
4847
5339
  return [
4848
5340
  localWarp(
4849
- P4.AngleX,
5341
+ P6.AngleX,
4850
5342
  mesh,
4851
5343
  b,
4852
5344
  X_STOPS,
4853
5345
  ([x, y], v, i) => v === 0 ? [0, 0] : [fieldOf(i)(x, y, v, 0)[0], 0]
4854
5346
  ),
4855
5347
  localWarp(
4856
- P4.AngleY,
5348
+ P6.AngleY,
4857
5349
  mesh,
4858
5350
  b,
4859
5351
  Y_STOPS,
@@ -4901,16 +5393,22 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
4901
5393
  }
4902
5394
  if (mesh !== void 0) part.mesh = mesh;
4903
5395
  const side = spec.role.endsWith("_L") ? "L" : "R";
4904
- const eyeOpen = side === "L" ? P4.EyeOpenLeft : P4.EyeOpenRight;
5396
+ const eyeOpen = side === "L" ? P6.EyeOpenLeft : P6.EyeOpenRight;
4905
5397
  const hh = frame.hh;
4906
5398
  switch (spec.role) {
4907
5399
  case "face":
4908
5400
  if (region?.some((r) => r === "neck")) {
4909
5401
  const isNeck = region.map((r) => r === "neck");
4910
5402
  warps.push(
4911
- tiltHang(mesh, b, pivot, (_y, i) => isNeck[i] ? 1 : 0),
5403
+ tiltHang(
5404
+ mesh,
5405
+ b,
5406
+ pivot,
5407
+ (_y, i) => isNeck[i] ? 1 : 0,
5408
+ headOwnRoll(byRole.has("body"))
5409
+ ),
4912
5410
  localWarp(
4913
- P4.Breath,
5411
+ P6.Breath,
4914
5412
  mesh,
4915
5413
  b,
4916
5414
  [0, 1],
@@ -4957,8 +5455,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
4957
5455
  if (byRole.has("mouth_open")) {
4958
5456
  warps.push(mouthWiden(mesh, b));
4959
5457
  bindings.push(
4960
- { parameter: P4.MouthOpen, channel: "opacity", from: 1, to: 0 },
4961
- { parameter: P4.MouthOpen, channel: "opacity", from: 1, to: 0 }
5458
+ { parameter: P6.MouthOpen, channel: "opacity", from: 1, to: 0 },
5459
+ { parameter: P6.MouthOpen, channel: "opacity", from: 1, to: 0 }
4962
5460
  );
4963
5461
  extra = [(AMPLITUDE.mouthOpenWidth - 1) * (bw(b) / 2), 0];
4964
5462
  } else {
@@ -4968,19 +5466,19 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
4968
5466
  );
4969
5467
  bindings.push(
4970
5468
  {
4971
- parameter: P4.MouthOpen,
5469
+ parameter: P6.MouthOpen,
4972
5470
  channel: "scaleY",
4973
5471
  from: 0,
4974
5472
  to: roundTo(grow, 0.01)
4975
5473
  },
4976
5474
  {
4977
- parameter: P4.MouthOpen,
5475
+ parameter: P6.MouthOpen,
4978
5476
  channel: "scaleX",
4979
5477
  from: 0,
4980
5478
  to: roundTo(AMPLITUDE.mouthOpenWidth - 1, 0.01)
4981
5479
  },
4982
5480
  {
4983
- parameter: P4.MouthOpen,
5481
+ parameter: P6.MouthOpen,
4984
5482
  channel: "translateY",
4985
5483
  from: 0,
4986
5484
  to: -roundTo(grow * bh(b) / 2, 0.01)
@@ -4992,7 +5490,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
4992
5490
  case "mouth_open":
4993
5491
  warps.push(mouthForm(mesh, b), mouthOpenGrow(mesh, b));
4994
5492
  bindings.push({
4995
- parameter: P4.MouthOpen,
5493
+ parameter: P6.MouthOpen,
4996
5494
  channel: "opacity",
4997
5495
  from: 0,
4998
5496
  to: 1
@@ -5002,7 +5500,7 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5002
5500
  case "blush_L":
5003
5501
  case "blush_R":
5004
5502
  bindings.push({
5005
- parameter: P4.Cheek,
5503
+ parameter: P6.Cheek,
5006
5504
  channel: "opacity",
5007
5505
  from: AMPLITUDE.blushRest,
5008
5506
  to: 1
@@ -5013,8 +5511,8 @@ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
5013
5511
  if (byRole.has("hair_front")) {
5014
5512
  const amp = style.sway * AMPLITUDE.sway * hh;
5015
5513
  warps.push(
5016
- hairSway(P4.HairSwayX, mesh, b, amp),
5017
- hairSway(P4.HairSwayZ, mesh, b, amp)
5514
+ hairSway(P6.HairSwayX, mesh, b, amp),
5515
+ hairSway(P6.HairSwayZ, mesh, b, amp)
5018
5516
  );
5019
5517
  }
5020
5518
  warps.push(tiltHang(mesh, b, pivot, hairHangWeight(frame.chinY, b.y0)));
@@ -5087,33 +5585,24 @@ function meshScale(role, frame) {
5087
5585
  return Infinity;
5088
5586
  }
5089
5587
  }
5090
- function deformers(frame, grids, body) {
5588
+ function deformers(frame, grids, bodyWarp, arms) {
5091
5589
  const out = [];
5092
- if (body !== void 0) {
5093
- out.push({
5094
- id: "bodyDeformer",
5095
- pivot: { x: roundTo(cx(body), 0.01), y: roundTo(body.y1, 0.01) },
5096
- bindings: [
5097
- {
5098
- parameter: P4.Breath,
5099
- channel: "translateY",
5100
- from: 0,
5101
- to: roundTo(AMPLITUDE.breathBody * frame.hh, 0.01)
5102
- }
5103
- ]
5104
- });
5105
- }
5590
+ if (bodyWarp !== void 0) out.push(bodyWarp);
5591
+ for (const [role, g] of arms) out.push(...armDeformers(role, g));
5592
+ const hasBody = bodyWarp !== void 0;
5593
+ const roll = headOwnRoll(hasBody);
5106
5594
  out.push({
5107
5595
  id: "headDeformer",
5596
+ ...hasBody ? { parent: BODY_WARP_ID } : {},
5108
5597
  pivot: { x: roundTo(frame.axisX, 0.01), y: roundTo(frame.chinY, 0.01) },
5109
5598
  bindings: [
5110
5599
  // AngleZ is clockwise-positive (Live2D's); a rotation is CCW-positive.
5111
- { parameter: P4.AngleZ, channel: "rotate", from: ROLL_DEG, to: -ROLL_DEG },
5600
+ { parameter: P6.AngleZ, channel: "rotate", from: roll, to: -roll },
5112
5601
  {
5113
- parameter: P4.Breath,
5602
+ parameter: P6.Breath,
5114
5603
  channel: "translateY",
5115
5604
  from: 0,
5116
- to: roundTo(AMPLITUDE.breathHead * frame.hh, 0.01)
5605
+ to: roundTo(headOwnBreath(hasBody) * frame.hh, 0.01)
5117
5606
  }
5118
5607
  ]
5119
5608
  });
@@ -5128,8 +5617,8 @@ function deformers(frame, grids, body) {
5128
5617
  points: g.lattice.points
5129
5618
  },
5130
5619
  warp2d: {
5131
- parameter: P4.AngleX,
5132
- parameterY: P4.AngleY,
5620
+ parameter: P6.AngleX,
5621
+ parameterY: P6.AngleY,
5133
5622
  valuesX: X_STOPS,
5134
5623
  valuesY: Y_STOPS,
5135
5624
  keyforms2d: g.keyforms.map((offsets) => ({ offsets }))
@@ -5142,16 +5631,16 @@ function hairPhysics() {
5142
5631
  return [
5143
5632
  {
5144
5633
  id: "hairSway",
5145
- input: { parameter: P4.AngleX, weight: 1 },
5146
- output: { parameter: P4.HairSwayX, scale: 5 },
5634
+ input: { parameter: P6.AngleX, weight: 1 },
5635
+ output: { parameter: P6.HairSwayX, scale: 5 },
5147
5636
  mass: 1,
5148
5637
  stiffness: 30,
5149
5638
  damping: 3
5150
5639
  },
5151
5640
  {
5152
5641
  id: "hairTilt",
5153
- input: { parameter: P4.AngleZ, weight: 1 },
5154
- output: { parameter: P4.HairSwayZ, scale: 8 },
5642
+ input: { parameter: P6.AngleZ, weight: 1 },
5643
+ output: { parameter: P6.HairSwayZ, scale: 8 },
5155
5644
  mass: 1,
5156
5645
  stiffness: 25,
5157
5646
  damping: 2.5
@@ -5161,32 +5650,46 @@ function hairPhysics() {
5161
5650
  function declareParameters(roles) {
5162
5651
  const out = [];
5163
5652
  const add = (id, name, min, max, def) => out.push({ id, name, min, max, default: def });
5164
- add(P4.AngleX, "Head Angle", -30, 30, 0);
5165
- add(P4.AngleY, "Head Angle Y", -30, 30, 0);
5166
- add(P4.AngleZ, "Head Angle Z", -30, 30, 0);
5167
- add(P4.EyeOpenLeft, "Eye L", 0, 1, 1);
5168
- add(P4.EyeOpenRight, "Eye R", 0, 1, 1);
5653
+ add(P6.AngleX, "Head Angle", -30, 30, 0);
5654
+ add(P6.AngleY, "Head Angle Y", -30, 30, 0);
5655
+ add(P6.AngleZ, "Head Angle Z", -30, 30, 0);
5656
+ if (roles.has("body")) {
5657
+ add(P6.BodyAngleX, "Body Angle X", -10, 10, 0);
5658
+ add(P6.BodyAngleY, "Body Angle Y", -10, 10, 0);
5659
+ add(P6.BodyAngleZ, "Body Angle Z", -10, 10, 0);
5660
+ }
5661
+ const [armMin, armMax] = ARM_RANGE;
5662
+ if (roles.has("arm_L")) {
5663
+ add(P6.ArmLeft, "Arm L", armMin, armMax, 0);
5664
+ add(P6.ElbowLeft, "Elbow L", armMin, armMax, 0);
5665
+ }
5666
+ if (roles.has("arm_R")) {
5667
+ add(P6.ArmRight, "Arm R", armMin, armMax, 0);
5668
+ add(P6.ElbowRight, "Elbow R", armMin, armMax, 0);
5669
+ }
5670
+ add(P6.EyeOpenLeft, "Eye L", 0, 1, 1);
5671
+ add(P6.EyeOpenRight, "Eye R", 0, 1, 1);
5169
5672
  if ([...roles].some((r) => /^(iris|pupil|highlight)_/.test(r))) {
5170
- add(P4.EyeballX, "Gaze X", -1, 1, 0);
5171
- add(P4.EyeballY, "Gaze Y", -1, 1, 0);
5673
+ add(P6.EyeballX, "Gaze X", -1, 1, 0);
5674
+ add(P6.EyeballY, "Gaze Y", -1, 1, 0);
5172
5675
  }
5173
- add(P4.MouthOpen, "Mouth Open", 0, 1, 0);
5174
- add(P4.MouthForm, "Mouth Form", -1, 1, 0);
5676
+ add(P6.MouthOpen, "Mouth Open", 0, 1, 0);
5677
+ add(P6.MouthForm, "Mouth Form", -1, 1, 0);
5175
5678
  if (roles.has("brow_L")) {
5176
- add(P4.BrowLeftY, "Brow L Y", -1, 1, 0);
5177
- add(P4.BrowLeftAngle, "Brow L Angle", -1, 1, 0);
5679
+ add(P6.BrowLeftY, "Brow L Y", -1, 1, 0);
5680
+ add(P6.BrowLeftAngle, "Brow L Angle", -1, 1, 0);
5178
5681
  }
5179
5682
  if (roles.has("brow_R")) {
5180
- add(P4.BrowRightY, "Brow R Y", -1, 1, 0);
5181
- add(P4.BrowRightAngle, "Brow R Angle", -1, 1, 0);
5683
+ add(P6.BrowRightY, "Brow R Y", -1, 1, 0);
5684
+ add(P6.BrowRightAngle, "Brow R Angle", -1, 1, 0);
5182
5685
  }
5183
5686
  if (roles.has("blush_L") || roles.has("blush_R")) {
5184
- add(P4.Cheek, "Cheek", 0, 1, 0);
5687
+ add(P6.Cheek, "Cheek", 0, 1, 0);
5185
5688
  }
5186
- add(P4.Breath, "Breath", 0, 1, 0);
5689
+ add(P6.Breath, "Breath", 0, 1, 0);
5187
5690
  if (roles.has("hair_front")) {
5188
- add(P4.HairSwayX, "Hair Sway X", -SWAY_RANGE, SWAY_RANGE, 0);
5189
- add(P4.HairSwayZ, "Hair Sway Z", -SWAY_RANGE, SWAY_RANGE, 0);
5691
+ add(P6.HairSwayX, "Hair Sway X", -SWAY_RANGE, SWAY_RANGE, 0);
5692
+ add(P6.HairSwayZ, "Hair Sway Z", -SWAY_RANGE, SWAY_RANGE, 0);
5190
5693
  }
5191
5694
  return out;
5192
5695
  }
@@ -5204,6 +5707,7 @@ export {
5204
5707
  DeletePhysicsRig,
5205
5708
  EditorDocument,
5206
5709
  HEAD_BAND,
5710
+ LayerGeometryError,
5207
5711
  SPECK_CORE_FRACTION,
5208
5712
  SetDeformerBindings,
5209
5713
  SetDeformerParent,
@@ -5222,6 +5726,7 @@ export {
5222
5726
  SetPhysicsRig,
5223
5727
  TurnTargetError,
5224
5728
  UV_INSET_PX,
5729
+ armGeometry,
5225
5730
  captureBindingEndpoint,
5226
5731
  computeGridOffsets,
5227
5732
  createDefaultMatrixDeformer,
@@ -5238,6 +5743,7 @@ export {
5238
5743
  isSpeckCore,
5239
5744
  packAtlas,
5240
5745
  parseLayerRoles,
5746
+ partIdsOfRole,
5241
5747
  upsertGridKeyform,
5242
5748
  uvRectFor,
5243
5749
  validateDeformerDelete,