@ikijs/editor 0.29.0 → 0.30.1
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/README.md +25 -19
- package/dist/index.d.mts +68 -10
- package/dist/index.d.ts +68 -10
- package/dist/index.js +430 -187
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +430 -187
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1563,6 +1563,8 @@ var ROW_RUN_ROLES = /* @__PURE__ */ new Set([
|
|
|
1563
1563
|
"face",
|
|
1564
1564
|
"hair_front",
|
|
1565
1565
|
"hair_back",
|
|
1566
|
+
"hair_side_L",
|
|
1567
|
+
"hair_side_R",
|
|
1566
1568
|
"body",
|
|
1567
1569
|
"arm_L",
|
|
1568
1570
|
"arm_R",
|
|
@@ -1992,8 +1994,9 @@ function deriveProfile(v) {
|
|
|
1992
1994
|
/** The mouth's far corner rises (its near end drops) by this much: it
|
|
1993
1995
|
* stays level. */
|
|
1994
1996
|
mouthTiltDeg: 0,
|
|
1995
|
-
/** The front hair (bangs and
|
|
1996
|
-
* its
|
|
1997
|
+
/** The front hair (its bangs and its own side strands; a `hair_side_*` lock
|
|
1998
|
+
* takes it only down to its root, `hairSideField`) rides the face at this
|
|
1999
|
+
* share of its translation. */
|
|
1997
2000
|
hairFollow: v.hairFollow,
|
|
1998
2001
|
/** The head's outline at the eye row holds: where the front hair draws
|
|
1999
2002
|
* it, its outer edge follows the face this much on a row where no back
|
|
@@ -2209,169 +2212,6 @@ function meshPoints(mesh, box) {
|
|
|
2209
2212
|
return out;
|
|
2210
2213
|
}
|
|
2211
2214
|
|
|
2212
|
-
// src/auto-rig/roles.ts
|
|
2213
|
-
import { StandardParameter as P } from "@ikijs/format";
|
|
2214
|
-
var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
|
|
2215
|
-
var isLipRole = (role) => LIP_ROLES.includes(role);
|
|
2216
|
-
var LIP_INSIDE_ROLES = ["mouth_tongue", "mouth_teeth"];
|
|
2217
|
-
var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
|
|
2218
|
-
var SHUT_BY = new Map([
|
|
2219
|
-
["eye_L", P.EyeOpenLeft],
|
|
2220
|
-
["lash_lower_L", P.EyeOpenLeft],
|
|
2221
|
-
["eye_R", P.EyeOpenRight],
|
|
2222
|
-
["lash_lower_R", P.EyeOpenRight],
|
|
2223
|
-
...[...LIP_ROLES, ...LIP_INSIDE_ROLES].map((id) => [
|
|
2224
|
-
id,
|
|
2225
|
-
P.MouthOpen
|
|
2226
|
-
])
|
|
2227
|
-
]);
|
|
2228
|
-
var ROLE_TABLE = [
|
|
2229
|
-
{ role: "hair_back", family: "hair_back" },
|
|
2230
|
-
{ role: "body", family: "body" },
|
|
2231
|
-
{ role: "arm_L", family: "arm", parts: ["elbow_L", "forearm_L", "arm_L"] },
|
|
2232
|
-
{ role: "arm_R", family: "arm", parts: ["elbow_R", "forearm_R", "arm_R"] },
|
|
2233
|
-
{ role: "face", family: "face", required: true },
|
|
2234
|
-
{ role: "blush_L", family: "face" },
|
|
2235
|
-
{ role: "blush_R", family: "face" },
|
|
2236
|
-
{ role: "nose", family: "nose" },
|
|
2237
|
-
{ role: "mouth", family: "mouth" },
|
|
2238
|
-
{ role: "mouth_open", family: "mouth" },
|
|
2239
|
-
// The lip set, back to front: the interior, the tongue and the teeth clipped
|
|
2240
|
-
// to it, the lower lip's skin, the upper lip and its line over both. It
|
|
2241
|
-
// folds open instead of crossfading (`mouth.ts`), so it stands in for
|
|
2242
|
-
// `mouth`, never beside it.
|
|
2243
|
-
{ role: "mouth_inner", family: "mouth" },
|
|
2244
|
-
{ role: "mouth_tongue", family: "mouth" },
|
|
2245
|
-
{ role: "mouth_teeth", family: "mouth" },
|
|
2246
|
-
{ role: "lip_lower", family: "mouth" },
|
|
2247
|
-
{ role: "lip_upper", family: "mouth" },
|
|
2248
|
-
{ role: "eye_L", family: "eye_L", required: true },
|
|
2249
|
-
{ role: "eye_R", family: "eye_R", required: true },
|
|
2250
|
-
{ role: "iris_L", family: "eye_L" },
|
|
2251
|
-
{ role: "iris_R", family: "eye_R" },
|
|
2252
|
-
{ role: "pupil_L", family: "eye_L" },
|
|
2253
|
-
{ role: "pupil_R", family: "eye_R" },
|
|
2254
|
-
{ role: "highlight_L", family: "eye_L" },
|
|
2255
|
-
{ role: "highlight_R", family: "eye_R" },
|
|
2256
|
-
{ role: "lash_lower_L", family: "eye_L" },
|
|
2257
|
-
{ role: "lash_lower_R", family: "eye_R" },
|
|
2258
|
-
{ role: "lash_L", family: "eye_L" },
|
|
2259
|
-
{ role: "lash_R", family: "eye_R" },
|
|
2260
|
-
{ role: "brow_L", family: "brow_L" },
|
|
2261
|
-
{ role: "brow_R", family: "brow_R" },
|
|
2262
|
-
{ role: "hair_front", family: "hair_front" },
|
|
2263
|
-
// Drawn above the face and the hair: a raised hand passes them.
|
|
2264
|
-
{ role: "forearm_pose_L", family: "forearm_pose" },
|
|
2265
|
-
{ role: "forearm_pose_R", family: "forearm_pose" }
|
|
2266
|
-
];
|
|
2267
|
-
var REQUIRED_ROLES = ROLE_TABLE.filter(
|
|
2268
|
-
(r) => r.required
|
|
2269
|
-
).map((r) => r.role);
|
|
2270
|
-
var SPEC_BY_ROLE = new Map(ROLE_TABLE.map((r, i) => [r.role, { ...r, i }]));
|
|
2271
|
-
function roleSpec(role) {
|
|
2272
|
-
const spec = SPEC_BY_ROLE.get(role);
|
|
2273
|
-
if (spec === void 0) throw new Error(`auto-rig: unknown role "${role}"`);
|
|
2274
|
-
return spec;
|
|
2275
|
-
}
|
|
2276
|
-
function partIdsOfRole(role) {
|
|
2277
|
-
return roleSpec(role).parts ?? [role];
|
|
2278
|
-
}
|
|
2279
|
-
var ROLE_BY_PART = new Map(
|
|
2280
|
-
ROLE_TABLE.flatMap((r) => (r.parts ?? []).map((p) => [p, r.role]))
|
|
2281
|
-
);
|
|
2282
|
-
function roleOfPart(partId) {
|
|
2283
|
-
return ROLE_BY_PART.get(partId) ?? partId;
|
|
2284
|
-
}
|
|
2285
|
-
var ALIASES = {
|
|
2286
|
-
eyebrow_L: "brow_L",
|
|
2287
|
-
eyebrow_R: "brow_R",
|
|
2288
|
-
eye_white_L: "eye_L",
|
|
2289
|
-
eye_white_R: "eye_R"
|
|
2290
|
-
};
|
|
2291
|
-
function roleOf(fileName) {
|
|
2292
|
-
const role = fileName.replace(/\.[^.]+$/, "").toLowerCase().replace(/[-\s]+/g, "_").replace(/_([lr])$/, (_, s) => `_${s.toUpperCase()}`);
|
|
2293
|
-
return ALIASES[role] ?? role;
|
|
2294
|
-
}
|
|
2295
|
-
function checkArmsHaveBody(roles) {
|
|
2296
|
-
const has = new Set(roles);
|
|
2297
|
-
if (has.has("body")) return;
|
|
2298
|
-
const arm = ROLE_TABLE.find((r) => r.family === "arm" && has.has(r.role));
|
|
2299
|
-
if (arm !== void 0) {
|
|
2300
|
-
throw new Error(
|
|
2301
|
-
`auto-rig: ${arm.role} needs a body layer (an arm hangs from the body's shoulder)`
|
|
2302
|
-
);
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
function checkMouth(roles) {
|
|
2306
|
-
const has = new Set(roles);
|
|
2307
|
-
const present = LIP_ROLES.filter((r) => has.has(r));
|
|
2308
|
-
const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
|
|
2309
|
-
if (inside.length > 0 && present.length === 0) {
|
|
2310
|
-
throw new Error(
|
|
2311
|
-
`auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
|
|
2312
|
-
);
|
|
2313
|
-
}
|
|
2314
|
-
if (present.length === 0) {
|
|
2315
|
-
if (!has.has("mouth")) {
|
|
2316
|
-
throw new Error(
|
|
2317
|
-
`auto-rig: missing required role mouth (or the lip set ${LIP_ROLES.join(", ")})`
|
|
2318
|
-
);
|
|
2319
|
-
}
|
|
2320
|
-
return;
|
|
2321
|
-
}
|
|
2322
|
-
if (present.length < LIP_ROLES.length) {
|
|
2323
|
-
throw new Error(
|
|
2324
|
-
`auto-rig: the lip set is partial: missing ${LIP_ROLES.filter((r) => !has.has(r)).join(", ")}`
|
|
2325
|
-
);
|
|
2326
|
-
}
|
|
2327
|
-
const mixed = ["mouth", "mouth_open"].filter((r) => has.has(r));
|
|
2328
|
-
if (mixed.length > 0) {
|
|
2329
|
-
throw new Error(
|
|
2330
|
-
`auto-rig: the lip set cannot be mixed with ${mixed.join(", ")} (the lip set replaces the mouth drawings; use one or the other)`
|
|
2331
|
-
);
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
function checkPosesHaveArms(roles) {
|
|
2335
|
-
const has = new Set(roles);
|
|
2336
|
-
for (const side of ["L", "R"]) {
|
|
2337
|
-
if (has.has(`forearm_pose_${side}`) && !has.has(`arm_${side}`)) {
|
|
2338
|
-
throw new Error(
|
|
2339
|
-
`auto-rig: forearm_pose_${side} needs an arm_${side} layer (a pose forearm hangs from its arm's elbow)`
|
|
2340
|
-
);
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
function parseLayerRoles(fileNames) {
|
|
2345
|
-
const seen = /* @__PURE__ */ new Map();
|
|
2346
|
-
const out = [];
|
|
2347
|
-
for (const fileName of fileNames) {
|
|
2348
|
-
const role = roleOf(fileName);
|
|
2349
|
-
if (!SPEC_BY_ROLE.has(role)) {
|
|
2350
|
-
throw new Error(
|
|
2351
|
-
`auto-rig: "${fileName}" names no known role (expected one of ${ROLE_TABLE.map((r) => r.role).join(", ")})`
|
|
2352
|
-
);
|
|
2353
|
-
}
|
|
2354
|
-
const prior = seen.get(role);
|
|
2355
|
-
if (prior !== void 0) {
|
|
2356
|
-
throw new Error(
|
|
2357
|
-
`auto-rig: role "${role}" is named twice ("${prior}" and "${fileName}")`
|
|
2358
|
-
);
|
|
2359
|
-
}
|
|
2360
|
-
seen.set(role, fileName);
|
|
2361
|
-
out.push({ role, fileName });
|
|
2362
|
-
}
|
|
2363
|
-
const missing = REQUIRED_ROLES.filter((r) => !seen.has(r));
|
|
2364
|
-
if (missing.length > 0) {
|
|
2365
|
-
throw new Error(
|
|
2366
|
-
`auto-rig: missing required role${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`
|
|
2367
|
-
);
|
|
2368
|
-
}
|
|
2369
|
-
checkMouth(seen.keys());
|
|
2370
|
-
checkArmsHaveBody(seen.keys());
|
|
2371
|
-
checkPosesHaveArms(seen.keys());
|
|
2372
|
-
return out;
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
2215
|
// src/auto-rig/mouth.ts
|
|
2376
2216
|
import {
|
|
2377
2217
|
StandardParameter as P3
|
|
@@ -2379,11 +2219,12 @@ import {
|
|
|
2379
2219
|
|
|
2380
2220
|
// src/auto-rig/drivers.ts
|
|
2381
2221
|
import {
|
|
2382
|
-
StandardParameter as
|
|
2222
|
+
StandardParameter as P
|
|
2383
2223
|
} from "@ikijs/format";
|
|
2384
2224
|
var SWAY_RANGE = 20;
|
|
2385
2225
|
var SWAY_CURVE = 1.4;
|
|
2386
2226
|
var HAIR_HANG = 0.35;
|
|
2227
|
+
var LOCK_HANG = 0.45;
|
|
2387
2228
|
var Z_STOPS = [-30, 0, 30];
|
|
2388
2229
|
function localWarp(parameter, mesh, box, values, f) {
|
|
2389
2230
|
const pts = meshPoints(mesh, box);
|
|
@@ -2458,13 +2299,13 @@ function gazeBindings(irisW, share = 1) {
|
|
|
2458
2299
|
const gy = AMPLITUDE.gazeY * irisW * share;
|
|
2459
2300
|
return [
|
|
2460
2301
|
{
|
|
2461
|
-
parameter:
|
|
2302
|
+
parameter: P.EyeballX,
|
|
2462
2303
|
channel: "translateX",
|
|
2463
2304
|
from: -round1(gx),
|
|
2464
2305
|
to: round1(gx)
|
|
2465
2306
|
},
|
|
2466
2307
|
{
|
|
2467
|
-
parameter:
|
|
2308
|
+
parameter: P.EyeballY,
|
|
2468
2309
|
channel: "translateY",
|
|
2469
2310
|
from: -round1(gy),
|
|
2470
2311
|
to: round1(gy)
|
|
@@ -2478,8 +2319,8 @@ function browReach(hh) {
|
|
|
2478
2319
|
return AMPLITUDE.brow * hh;
|
|
2479
2320
|
}
|
|
2480
2321
|
function browBindings(side, reach) {
|
|
2481
|
-
const y = side === "L" ?
|
|
2482
|
-
const a = side === "L" ?
|
|
2322
|
+
const y = side === "L" ? P.BrowLeftY : P.BrowRightY;
|
|
2323
|
+
const a = side === "L" ? P.BrowLeftAngle : P.BrowRightAngle;
|
|
2483
2324
|
return [
|
|
2484
2325
|
{
|
|
2485
2326
|
parameter: y,
|
|
@@ -2498,7 +2339,7 @@ var mouthFrameOf = (box) => ({
|
|
|
2498
2339
|
});
|
|
2499
2340
|
function mouthForm(mesh, box, frame = mouthFrameOf(box)) {
|
|
2500
2341
|
const { c, half, amp: a } = frame;
|
|
2501
|
-
return localWarp(
|
|
2342
|
+
return localWarp(P.MouthForm, mesh, box, [-1, 0, 1], ([x], v) => {
|
|
2502
2343
|
if (v === 0) return [0, 0];
|
|
2503
2344
|
const u = (x - c) / half;
|
|
2504
2345
|
const widen = v > 0 ? 0.05 : 0.03;
|
|
@@ -2508,7 +2349,7 @@ function mouthForm(mesh, box, frame = mouthFrameOf(box)) {
|
|
|
2508
2349
|
function mouthOpenGrow(mesh, box) {
|
|
2509
2350
|
const c = cx(box);
|
|
2510
2351
|
return localWarp(
|
|
2511
|
-
|
|
2352
|
+
P.MouthOpen,
|
|
2512
2353
|
mesh,
|
|
2513
2354
|
box,
|
|
2514
2355
|
[0, 1],
|
|
@@ -2517,25 +2358,38 @@ function mouthOpenGrow(mesh, box) {
|
|
|
2517
2358
|
}
|
|
2518
2359
|
function mouthWiden(mesh, box, frame = mouthFrameOf(box)) {
|
|
2519
2360
|
const c = frame.c;
|
|
2520
|
-
return localWarp(
|
|
2361
|
+
return localWarp(P.MouthOpen, mesh, box, [0, 1], ([x], v) => [
|
|
2521
2362
|
v * (AMPLITUDE.mouthOpenWidth - 1) * (x - c),
|
|
2522
2363
|
0
|
|
2523
2364
|
]);
|
|
2524
2365
|
}
|
|
2366
|
+
function swayAt(b, y) {
|
|
2367
|
+
return Math.min(1, Math.max(0, (b.y1 - y) / bh(b))) ** SWAY_CURVE;
|
|
2368
|
+
}
|
|
2525
2369
|
function hairSway(param, mesh, box, amp) {
|
|
2370
|
+
return localWarp(
|
|
2371
|
+
param,
|
|
2372
|
+
mesh,
|
|
2373
|
+
box,
|
|
2374
|
+
[-SWAY_RANGE, 0, SWAY_RANGE],
|
|
2375
|
+
([, y], v) => [v / SWAY_RANGE * amp * swayAt(box, y), 0]
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
function lockSway(param, mesh, box, amp, attach) {
|
|
2526
2379
|
return localWarp(
|
|
2527
2380
|
param,
|
|
2528
2381
|
mesh,
|
|
2529
2382
|
box,
|
|
2530
2383
|
[-SWAY_RANGE, 0, SWAY_RANGE],
|
|
2531
2384
|
([, y], v) => {
|
|
2532
|
-
const
|
|
2533
|
-
|
|
2385
|
+
const w = attach.weight(y);
|
|
2386
|
+
const share = (1 - w) * swayAt(attach.box, y) + w * swayAt(box, y);
|
|
2387
|
+
return [v / SWAY_RANGE * amp * share, 0];
|
|
2534
2388
|
}
|
|
2535
2389
|
);
|
|
2536
2390
|
}
|
|
2537
2391
|
function tiltHang(mesh, box, pivot, weight, deg = ROLL_DEG) {
|
|
2538
|
-
return localWarp(
|
|
2392
|
+
return localWarp(P.AngleZ, mesh, box, Z_STOPS, ([x, y], v, i) => {
|
|
2539
2393
|
const g = weight(y, i);
|
|
2540
2394
|
if (g === 0 || v === 0) return [0, 0];
|
|
2541
2395
|
const a = -g * rollOf(v, deg);
|
|
@@ -2551,8 +2405,191 @@ function hairHangWeight(pivotY, bottom) {
|
|
|
2551
2405
|
if (bottom >= pivotY) return () => 0;
|
|
2552
2406
|
return (y) => HAIR_HANG * smoothstep(pivotY, bottom, y);
|
|
2553
2407
|
}
|
|
2408
|
+
function lockHangWeight(free) {
|
|
2409
|
+
return (y) => LOCK_HANG * free(y);
|
|
2410
|
+
}
|
|
2554
2411
|
var round1 = (v) => roundTo(v, 0.1);
|
|
2555
2412
|
|
|
2413
|
+
// src/auto-rig/roles.ts
|
|
2414
|
+
import { StandardParameter as P2 } from "@ikijs/format";
|
|
2415
|
+
var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
|
|
2416
|
+
var isLipRole = (role) => LIP_ROLES.includes(role);
|
|
2417
|
+
var LIP_INSIDE_ROLES = ["mouth_tongue", "mouth_teeth"];
|
|
2418
|
+
var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
|
|
2419
|
+
var SHUT_BY = new Map([
|
|
2420
|
+
["eye_L", P2.EyeOpenLeft],
|
|
2421
|
+
["lash_lower_L", P2.EyeOpenLeft],
|
|
2422
|
+
["eye_R", P2.EyeOpenRight],
|
|
2423
|
+
["lash_lower_R", P2.EyeOpenRight],
|
|
2424
|
+
...[...LIP_ROLES, ...LIP_INSIDE_ROLES].map((id) => [
|
|
2425
|
+
id,
|
|
2426
|
+
P2.MouthOpen
|
|
2427
|
+
])
|
|
2428
|
+
]);
|
|
2429
|
+
var ROLE_TABLE = [
|
|
2430
|
+
{ role: "hair_back", family: "hair_back" },
|
|
2431
|
+
{ role: "body", family: "body" },
|
|
2432
|
+
{ role: "arm_L", family: "arm", parts: ["elbow_L", "forearm_L", "arm_L"] },
|
|
2433
|
+
{ role: "arm_R", family: "arm", parts: ["elbow_R", "forearm_R", "arm_R"] },
|
|
2434
|
+
{ role: "face", family: "face", required: true },
|
|
2435
|
+
{ role: "blush_L", family: "face" },
|
|
2436
|
+
{ role: "blush_R", family: "face" },
|
|
2437
|
+
{ role: "nose", family: "nose" },
|
|
2438
|
+
{ role: "mouth", family: "mouth" },
|
|
2439
|
+
{ role: "mouth_open", family: "mouth" },
|
|
2440
|
+
// The lip set, back to front: the interior, the tongue and the teeth clipped
|
|
2441
|
+
// to it, the lower lip's skin, the upper lip and its line over both. It
|
|
2442
|
+
// folds open instead of crossfading (`mouth.ts`), so it stands in for
|
|
2443
|
+
// `mouth`, never beside it.
|
|
2444
|
+
{ role: "mouth_inner", family: "mouth" },
|
|
2445
|
+
{ role: "mouth_tongue", family: "mouth" },
|
|
2446
|
+
{ role: "mouth_teeth", family: "mouth" },
|
|
2447
|
+
{ role: "lip_lower", family: "mouth" },
|
|
2448
|
+
{ role: "lip_upper", family: "mouth" },
|
|
2449
|
+
{ role: "eye_L", family: "eye_L", required: true },
|
|
2450
|
+
{ role: "eye_R", family: "eye_R", required: true },
|
|
2451
|
+
{ role: "iris_L", family: "eye_L" },
|
|
2452
|
+
{ role: "iris_R", family: "eye_R" },
|
|
2453
|
+
{ role: "pupil_L", family: "eye_L" },
|
|
2454
|
+
{ role: "pupil_R", family: "eye_R" },
|
|
2455
|
+
{ role: "highlight_L", family: "eye_L" },
|
|
2456
|
+
{ role: "highlight_R", family: "eye_R" },
|
|
2457
|
+
{ role: "lash_lower_L", family: "eye_L" },
|
|
2458
|
+
{ role: "lash_lower_R", family: "eye_R" },
|
|
2459
|
+
{ role: "lash_L", family: "eye_L" },
|
|
2460
|
+
{ role: "lash_R", family: "eye_R" },
|
|
2461
|
+
{ role: "brow_L", family: "brow_L" },
|
|
2462
|
+
{ role: "brow_R", family: "brow_R" },
|
|
2463
|
+
// The front side locks: over the brows and lashes, under the bangs, which
|
|
2464
|
+
// hide their roots.
|
|
2465
|
+
{ role: "hair_side_L", family: "hair_side" },
|
|
2466
|
+
{ role: "hair_side_R", family: "hair_side" },
|
|
2467
|
+
{ role: "hair_front", family: "hair_front" },
|
|
2468
|
+
// Drawn above the face and the hair: a raised hand passes them.
|
|
2469
|
+
{ role: "forearm_pose_L", family: "forearm_pose" },
|
|
2470
|
+
{ role: "forearm_pose_R", family: "forearm_pose" }
|
|
2471
|
+
];
|
|
2472
|
+
var REQUIRED_ROLES = ROLE_TABLE.filter(
|
|
2473
|
+
(r) => r.required
|
|
2474
|
+
).map((r) => r.role);
|
|
2475
|
+
var SPEC_BY_ROLE = new Map(ROLE_TABLE.map((r, i) => [r.role, { ...r, i }]));
|
|
2476
|
+
function roleSpec(role) {
|
|
2477
|
+
const spec = SPEC_BY_ROLE.get(role);
|
|
2478
|
+
if (spec === void 0) throw new Error(`auto-rig: unknown role "${role}"`);
|
|
2479
|
+
return spec;
|
|
2480
|
+
}
|
|
2481
|
+
function partIdsOfRole(role) {
|
|
2482
|
+
return roleSpec(role).parts ?? [role];
|
|
2483
|
+
}
|
|
2484
|
+
var ROLE_BY_PART = new Map(
|
|
2485
|
+
ROLE_TABLE.flatMap((r) => (r.parts ?? []).map((p) => [p, r.role]))
|
|
2486
|
+
);
|
|
2487
|
+
function roleOfPart(partId) {
|
|
2488
|
+
return ROLE_BY_PART.get(partId) ?? partId;
|
|
2489
|
+
}
|
|
2490
|
+
var ALIASES = {
|
|
2491
|
+
eyebrow_L: "brow_L",
|
|
2492
|
+
eyebrow_R: "brow_R",
|
|
2493
|
+
eye_white_L: "eye_L",
|
|
2494
|
+
eye_white_R: "eye_R"
|
|
2495
|
+
};
|
|
2496
|
+
function roleOf(fileName) {
|
|
2497
|
+
const role = fileName.replace(/\.[^.]+$/, "").toLowerCase().replace(/[-\s]+/g, "_").replace(/_([lr])$/, (_, s) => `_${s.toUpperCase()}`);
|
|
2498
|
+
return ALIASES[role] ?? role;
|
|
2499
|
+
}
|
|
2500
|
+
function checkArmsHaveBody(roles) {
|
|
2501
|
+
const has = new Set(roles);
|
|
2502
|
+
if (has.has("body")) return;
|
|
2503
|
+
const arm = ROLE_TABLE.find((r) => r.family === "arm" && has.has(r.role));
|
|
2504
|
+
if (arm !== void 0) {
|
|
2505
|
+
throw new Error(
|
|
2506
|
+
`auto-rig: ${arm.role} needs a body layer (an arm hangs from the body's shoulder)`
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
function checkSidesHaveFront(roles) {
|
|
2511
|
+
const has = new Set(roles);
|
|
2512
|
+
if (has.has("hair_front")) return;
|
|
2513
|
+
const side = ROLE_TABLE.find(
|
|
2514
|
+
(r) => r.family === "hair_side" && has.has(r.role)
|
|
2515
|
+
);
|
|
2516
|
+
if (side !== void 0) {
|
|
2517
|
+
throw new Error(
|
|
2518
|
+
`auto-rig: ${side.role} needs a hair_front layer (a side lock's root hides under the bangs)`
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
function checkMouth(roles) {
|
|
2523
|
+
const has = new Set(roles);
|
|
2524
|
+
const present = LIP_ROLES.filter((r) => has.has(r));
|
|
2525
|
+
const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
|
|
2526
|
+
if (inside.length > 0 && present.length === 0) {
|
|
2527
|
+
throw new Error(
|
|
2528
|
+
`auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
if (present.length === 0) {
|
|
2532
|
+
if (!has.has("mouth")) {
|
|
2533
|
+
throw new Error(
|
|
2534
|
+
`auto-rig: missing required role mouth (or the lip set ${LIP_ROLES.join(", ")})`
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (present.length < LIP_ROLES.length) {
|
|
2540
|
+
throw new Error(
|
|
2541
|
+
`auto-rig: the lip set is partial: missing ${LIP_ROLES.filter((r) => !has.has(r)).join(", ")}`
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
const mixed = ["mouth", "mouth_open"].filter((r) => has.has(r));
|
|
2545
|
+
if (mixed.length > 0) {
|
|
2546
|
+
throw new Error(
|
|
2547
|
+
`auto-rig: the lip set cannot be mixed with ${mixed.join(", ")} (the lip set replaces the mouth drawings; use one or the other)`
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
function checkPosesHaveArms(roles) {
|
|
2552
|
+
const has = new Set(roles);
|
|
2553
|
+
for (const side of ["L", "R"]) {
|
|
2554
|
+
if (has.has(`forearm_pose_${side}`) && !has.has(`arm_${side}`)) {
|
|
2555
|
+
throw new Error(
|
|
2556
|
+
`auto-rig: forearm_pose_${side} needs an arm_${side} layer (a pose forearm hangs from its arm's elbow)`
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
function parseLayerRoles(fileNames) {
|
|
2562
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2563
|
+
const out = [];
|
|
2564
|
+
for (const fileName of fileNames) {
|
|
2565
|
+
const role = roleOf(fileName);
|
|
2566
|
+
if (!SPEC_BY_ROLE.has(role)) {
|
|
2567
|
+
throw new Error(
|
|
2568
|
+
`auto-rig: "${fileName}" names no known role (expected one of ${ROLE_TABLE.map((r) => r.role).join(", ")})`
|
|
2569
|
+
);
|
|
2570
|
+
}
|
|
2571
|
+
const prior = seen.get(role);
|
|
2572
|
+
if (prior !== void 0) {
|
|
2573
|
+
throw new Error(
|
|
2574
|
+
`auto-rig: role "${role}" is named twice ("${prior}" and "${fileName}")`
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
seen.set(role, fileName);
|
|
2578
|
+
out.push({ role, fileName });
|
|
2579
|
+
}
|
|
2580
|
+
const missing = REQUIRED_ROLES.filter((r) => !seen.has(r));
|
|
2581
|
+
if (missing.length > 0) {
|
|
2582
|
+
throw new Error(
|
|
2583
|
+
`auto-rig: missing required role${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
checkMouth(seen.keys());
|
|
2587
|
+
checkArmsHaveBody(seen.keys());
|
|
2588
|
+
checkPosesHaveArms(seen.keys());
|
|
2589
|
+
checkSidesHaveFront(seen.keys());
|
|
2590
|
+
return out;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2556
2593
|
// src/auto-rig/mouth.ts
|
|
2557
2594
|
var UPPER_SHARE = 0.4;
|
|
2558
2595
|
var CLOSED_SAG_MAX = 0.1;
|
|
@@ -2948,6 +2985,7 @@ function buildHeadFrame(layers, opts) {
|
|
|
2948
2985
|
let paintedHalfAt = () => plateHalf;
|
|
2949
2986
|
let neck;
|
|
2950
2987
|
let ears;
|
|
2988
|
+
let headHalfAt = (y) => paintedHalfAt(y);
|
|
2951
2989
|
let headMax = plateHalf;
|
|
2952
2990
|
let chinY = face.y0;
|
|
2953
2991
|
let cutAt = () => face.y0 - 1;
|
|
@@ -2966,6 +3004,12 @@ function buildHeadFrame(layers, opts) {
|
|
|
2966
3004
|
const t = clamp(r - i, 0, 1);
|
|
2967
3005
|
return prof[i] + (prof[j] - prof[i]) * t;
|
|
2968
3006
|
};
|
|
3007
|
+
const readRows = (rows) => (y) => {
|
|
3008
|
+
const r = canvasH / 2 - y - faceLayer.bbox.y - 0.5;
|
|
3009
|
+
const i = clamp(Math.floor(r), 0, rows.length - 1);
|
|
3010
|
+
const j = Math.min(i + 1, rows.length - 1);
|
|
3011
|
+
return rows[i] + (rows[j] - rows[i]) * clamp(r - i, 0, 1);
|
|
3012
|
+
};
|
|
2969
3013
|
const rawRows = raw;
|
|
2970
3014
|
const earless = (chin) => {
|
|
2971
3015
|
const rowOf = (y) => Math.round(canvasH / 2 - y - faceLayer.bbox.y - 0.5);
|
|
@@ -3066,6 +3110,7 @@ function buildHeadFrame(layers, opts) {
|
|
|
3066
3110
|
canvasH / 2 - nk.topY - faceLayer.bbox.y - 0.5
|
|
3067
3111
|
);
|
|
3068
3112
|
const head = earless(chinY);
|
|
3113
|
+
headHalfAt = readRows(head);
|
|
3069
3114
|
headMax = Math.max(...head);
|
|
3070
3115
|
const outlineAt = (d) => {
|
|
3071
3116
|
for (let r = neckRow; r >= 0; r--) {
|
|
@@ -3108,6 +3153,7 @@ function buildHeadFrame(layers, opts) {
|
|
|
3108
3153
|
};
|
|
3109
3154
|
} else {
|
|
3110
3155
|
const head = earless(chinY);
|
|
3156
|
+
headHalfAt = readRows(head);
|
|
3111
3157
|
headMax = Math.max(...head);
|
|
3112
3158
|
if (ears !== void 0) {
|
|
3113
3159
|
const bottomOf = (d) => {
|
|
@@ -3155,6 +3201,7 @@ function buildHeadFrame(layers, opts) {
|
|
|
3155
3201
|
chinY,
|
|
3156
3202
|
hh,
|
|
3157
3203
|
paintedHalfAt,
|
|
3204
|
+
headHalfAt,
|
|
3158
3205
|
wMax,
|
|
3159
3206
|
headMax,
|
|
3160
3207
|
hasProfile: profile !== null,
|
|
@@ -3172,6 +3219,96 @@ function buildHeadFrame(layers, opts) {
|
|
|
3172
3219
|
};
|
|
3173
3220
|
}
|
|
3174
3221
|
|
|
3222
|
+
// src/auto-rig/hair-side-room.ts
|
|
3223
|
+
var round3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
3224
|
+
var signed = (v) => round3(v) + 0;
|
|
3225
|
+
function headEdge(ctx, land2, y, side, ax) {
|
|
3226
|
+
const { frame } = ctx;
|
|
3227
|
+
return land2("face", frame.axisX + side * frame.headHalfAt(y), y, ax);
|
|
3228
|
+
}
|
|
3229
|
+
var LOCKS = ["hair_side_L", "hair_side_R"];
|
|
3230
|
+
function hairSideRoom(ctx, land2, boxes) {
|
|
3231
|
+
const out = {};
|
|
3232
|
+
for (const role of LOCKS) {
|
|
3233
|
+
const box = boxes[role];
|
|
3234
|
+
if (box === void 0) continue;
|
|
3235
|
+
const side = (box.x0 + box.x1) / 2 < ctx.frame.axisX ? -1 : 1;
|
|
3236
|
+
const key = side < 0 ? "left" : "right";
|
|
3237
|
+
if (out[key] !== void 0) continue;
|
|
3238
|
+
out[key] = lockRoom(ctx, land2, boxes, role, box, side);
|
|
3239
|
+
}
|
|
3240
|
+
return out.left || out.right ? out : void 0;
|
|
3241
|
+
}
|
|
3242
|
+
function lockRoom(ctx, land2, boxes, role, box, side) {
|
|
3243
|
+
const { frame } = ctx;
|
|
3244
|
+
if (frame.hairRuns(role, box.y1 - 0.5) === void 0) {
|
|
3245
|
+
return { role, unavailable: "no rowRuns" };
|
|
3246
|
+
}
|
|
3247
|
+
const room = { role };
|
|
3248
|
+
const stops = farStops(side);
|
|
3249
|
+
let cover;
|
|
3250
|
+
for (let i = 0; i < Math.ceil(box.y1 - box.y0); i++) {
|
|
3251
|
+
const y2 = box.y1 - i - 0.5;
|
|
3252
|
+
if (y2 < frame.face.y0 || y2 > frame.face.y1) continue;
|
|
3253
|
+
const outer2 = outerEdge(frame.hairRuns(role, y2) ?? [], frame.axisX, side);
|
|
3254
|
+
if (outer2 === void 0) continue;
|
|
3255
|
+
const rest = side * (outer2 - frame.axisX) - frame.headHalfAt(y2);
|
|
3256
|
+
if (rest < 0) continue;
|
|
3257
|
+
for (const ax of stops) {
|
|
3258
|
+
const px = side * (land2(role, outer2, y2, ax) - headEdge(ctx, land2, y2, side, ax));
|
|
3259
|
+
if (cover === void 0 || px < cover.px)
|
|
3260
|
+
cover = { deg: ax, px, y: y2, rest };
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
if (cover !== void 0) {
|
|
3264
|
+
room.cover = {
|
|
3265
|
+
deg: cover.deg,
|
|
3266
|
+
px: signed(cover.px),
|
|
3267
|
+
y: round3(cover.y),
|
|
3268
|
+
rest: signed(cover.rest)
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
const eye = side < 0 ? ctx.eyeBoxes.left : ctx.eyeBoxes.right;
|
|
3272
|
+
const eyeRole = (side < 0 ? ctx.left : ctx.right).role.replace(
|
|
3273
|
+
/^iris_/,
|
|
3274
|
+
"eye_"
|
|
3275
|
+
);
|
|
3276
|
+
const y = cy(eye);
|
|
3277
|
+
const runs = frame.hairRuns(role, y) ?? [];
|
|
3278
|
+
const outer = outerEdge(runs, frame.axisX, side);
|
|
3279
|
+
if (outer === void 0) return room;
|
|
3280
|
+
const inner = outer - side * runReach(runs, outer, side).inward;
|
|
3281
|
+
const lashRole = eyeRole.replace(/^eye_/, "lash_");
|
|
3282
|
+
const lash = boxes[lashRole];
|
|
3283
|
+
let tail;
|
|
3284
|
+
for (const ax of stops) {
|
|
3285
|
+
const lock = land2(role, inner, y, ax);
|
|
3286
|
+
const corner = land2(eyeRole, side < 0 ? eye.x0 : eye.x1, y, ax);
|
|
3287
|
+
const eyeToLock = side * (lock - corner);
|
|
3288
|
+
if (tail !== void 0 && eyeToLock >= tail.eyeToLock) continue;
|
|
3289
|
+
tail = {
|
|
3290
|
+
deg: ax,
|
|
3291
|
+
eyeToLock,
|
|
3292
|
+
eyeToPlate: side * (headEdge(ctx, land2, y, side, ax) - corner)
|
|
3293
|
+
};
|
|
3294
|
+
if (lash !== void 0) {
|
|
3295
|
+
const tip = land2(lashRole, side < 0 ? lash.x0 : lash.x1, y, ax);
|
|
3296
|
+
tail.lashToLock = side * (lock - tip);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
if (tail !== void 0) {
|
|
3300
|
+
room.tail = {
|
|
3301
|
+
deg: tail.deg,
|
|
3302
|
+
eyeToLock: signed(tail.eyeToLock),
|
|
3303
|
+
...tail.lashToLock !== void 0 && {
|
|
3304
|
+
lashToLock: signed(tail.lashToLock)
|
|
3305
|
+
},
|
|
3306
|
+
eyeToPlate: signed(tail.eyeToPlate)
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
return room;
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3175
3312
|
// src/auto-rig/fields.ts
|
|
3176
3313
|
var NOSE_TILT_DEG = 6;
|
|
3177
3314
|
function nodDy(v, ay, hh) {
|
|
@@ -3425,6 +3562,48 @@ function hairFrontField(m) {
|
|
|
3425
3562
|
return [rx, by + crownBlend(m, y) * (ry - by)];
|
|
3426
3563
|
};
|
|
3427
3564
|
}
|
|
3565
|
+
var SIDE_ROOT_BLEND = 0.2;
|
|
3566
|
+
function sideRootBlend(f, role, box) {
|
|
3567
|
+
let rootEnd = box.y1 - (box.y1 - box.y0) / 4;
|
|
3568
|
+
if (f.hairRuns(role, box.y1 - 0.5) !== void 0 && f.hairRuns("hair_front", box.y1 - 0.5) !== void 0) {
|
|
3569
|
+
const shows = (y) => {
|
|
3570
|
+
const lock = f.hairRuns(role, y);
|
|
3571
|
+
const front = f.hairRuns("hair_front", y);
|
|
3572
|
+
for (let k = 0; k < lock.length; k += 2) {
|
|
3573
|
+
for (let px = lock[k]; px < lock[k + 1]; px++) {
|
|
3574
|
+
if (!paints(front, px)) return true;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
return false;
|
|
3578
|
+
};
|
|
3579
|
+
const rows = Array.from(
|
|
3580
|
+
{ length: Math.round(box.y1 - box.y0) },
|
|
3581
|
+
(_, i2) => i2
|
|
3582
|
+
);
|
|
3583
|
+
const i = rows.findIndex((i2) => shows(box.y1 - i2 - 0.5));
|
|
3584
|
+
rootEnd = i < 0 ? box.y0 : box.y1 - i;
|
|
3585
|
+
}
|
|
3586
|
+
const end = rootEnd - SIDE_ROOT_BLEND * f.hh;
|
|
3587
|
+
return { rootEnd, weight: (y) => smoothstep(rootEnd, end, y) };
|
|
3588
|
+
}
|
|
3589
|
+
function hairSideField(m) {
|
|
3590
|
+
const front = hairFrontField(m);
|
|
3591
|
+
const face = faceField(m);
|
|
3592
|
+
const locks = ["hair_side_L", "hair_side_R"].flatMap((role) => {
|
|
3593
|
+
const box = m.boxes[role];
|
|
3594
|
+
return box === void 0 ? [] : [{ box, ...sideRootBlend(m.frame, role, box) }];
|
|
3595
|
+
});
|
|
3596
|
+
return (x, y, ax, ay) => {
|
|
3597
|
+
const lock = locks.find((l) => x >= l.box.x0 && x <= l.box.x1);
|
|
3598
|
+
if (lock === void 0) return face(x, y, ax, ay);
|
|
3599
|
+
const t = lock.weight(y);
|
|
3600
|
+
if (t === 0) return front(x, y, ax, ay);
|
|
3601
|
+
if (t === 1) return face(x, y, ax, ay);
|
|
3602
|
+
const [fx, fy] = front(x, y, ax, ay);
|
|
3603
|
+
const [gx, gy] = face(x, y, ax, ay);
|
|
3604
|
+
return [fx * (1 - t) + gx * t, fy * (1 - t) + gy * t];
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3428
3607
|
function crownBlend(m, y) {
|
|
3429
3608
|
const top = m.frame.face.y1;
|
|
3430
3609
|
return 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
|
|
@@ -3845,6 +4024,8 @@ function familyField(m, family) {
|
|
|
3845
4024
|
return mouthField(m);
|
|
3846
4025
|
case "hair_front":
|
|
3847
4026
|
return hairFrontField(m);
|
|
4027
|
+
case "hair_side":
|
|
4028
|
+
return hairSideField(m);
|
|
3848
4029
|
case "hair_back":
|
|
3849
4030
|
return hairField(m, m.hairBack, NOD.hairBack);
|
|
3850
4031
|
case "body":
|
|
@@ -4001,16 +4182,16 @@ function strandOverlaps(ctx, land2, holdBase) {
|
|
|
4001
4182
|
if (px <= 0) continue;
|
|
4002
4183
|
out[key] = {
|
|
4003
4184
|
deg,
|
|
4004
|
-
px:
|
|
4005
|
-
hh:
|
|
4006
|
-
restPx:
|
|
4185
|
+
px: round32(px),
|
|
4186
|
+
hh: round32(px) / holdBase,
|
|
4187
|
+
restPx: round32(restPx),
|
|
4007
4188
|
// A fringe spanning the face has no side to hold the iris clear of.
|
|
4008
4189
|
held: strand.runFace !== null && px <= restPx + STRAND_SLACK
|
|
4009
4190
|
};
|
|
4010
4191
|
}
|
|
4011
4192
|
return out.left || out.right ? out : void 0;
|
|
4012
4193
|
}
|
|
4013
|
-
var
|
|
4194
|
+
var round32 = (v) => Math.round(v * 1e3) / 1e3;
|
|
4014
4195
|
var STRAND_SLACK = 0.5;
|
|
4015
4196
|
function bisect(f, lo, hi, it = 50) {
|
|
4016
4197
|
for (let i = 0; i < it; i++) {
|
|
@@ -4225,7 +4406,7 @@ function turnReport(ctx, fit, rendered) {
|
|
|
4225
4406
|
const plate = depth(m.face);
|
|
4226
4407
|
const w = frame.paintedHalfAt(frame.eyeY);
|
|
4227
4408
|
const report = {
|
|
4228
|
-
radius:
|
|
4409
|
+
radius: round32(w * w / (2 * Math.max(1e-3, eye - plate))),
|
|
4229
4410
|
holdBase: hb,
|
|
4230
4411
|
depths: {
|
|
4231
4412
|
eye: round4(eye / hb),
|
|
@@ -4241,6 +4422,8 @@ function turnReport(ctx, fit, rendered) {
|
|
|
4241
4422
|
};
|
|
4242
4423
|
const overlap = strandOverlaps(ctx, rendered, hb);
|
|
4243
4424
|
if (overlap !== void 0) report.strandOverlap = overlap;
|
|
4425
|
+
const hairSide = hairSideRoom(ctx, rendered, m.boxes);
|
|
4426
|
+
if (hairSide !== void 0) report.hairSide = hairSide;
|
|
4244
4427
|
return report;
|
|
4245
4428
|
}
|
|
4246
4429
|
var round4 = (v) => Math.round(v * 1e4) / 1e4;
|
|
@@ -4396,7 +4579,7 @@ var RIG_PLAN_FIELDS = [
|
|
|
4396
4579
|
fitBy: null,
|
|
4397
4580
|
provisional: false,
|
|
4398
4581
|
migratedFrom: "outlineFollow",
|
|
4399
|
-
what: "the front hair's outer edge's share where it draws the head's outline (0: the outline holds, though where back hair paints behind it, it may ride further, as far as that back hair reaches; at hair.follow the side
|
|
4582
|
+
what: "the front hair's outer edge's share where it draws the head's outline (0: the outline holds, though where back hair paints behind it, it may ride further, as far as that back hair reaches; at hair.follow the front hair's side strands ride whole)"
|
|
4400
4583
|
},
|
|
4401
4584
|
{
|
|
4402
4585
|
path: "hair.sway",
|
|
@@ -5462,6 +5645,7 @@ function checkLayers(layers, canvas) {
|
|
|
5462
5645
|
}
|
|
5463
5646
|
checkArmsHaveBody(seen);
|
|
5464
5647
|
checkPosesHaveArms(seen);
|
|
5648
|
+
checkSidesHaveFront(seen);
|
|
5465
5649
|
}
|
|
5466
5650
|
function checkHeadEdges(edges, byRole) {
|
|
5467
5651
|
if (edges === void 0) return;
|
|
@@ -6188,6 +6372,10 @@ var MESH_CELLS = {
|
|
|
6188
6372
|
face: { px: 28, min: 4, max: 24 },
|
|
6189
6373
|
// The front hair eases from riding the face to holding the outline.
|
|
6190
6374
|
hair_front: { px: 32, min: 3, max: 24 },
|
|
6375
|
+
// A side lock's rows come from `sideLockGrid`: even ones, plus the root
|
|
6376
|
+
// band's (`sideRootBlend`), which a long lock's coarse cells would straddle.
|
|
6377
|
+
hair_side_L: { px: 20, min: 3, max: 24 },
|
|
6378
|
+
hair_side_R: { px: 20, min: 3, max: 24 },
|
|
6191
6379
|
hair_back: { px: 56, min: 3, max: 18 }
|
|
6192
6380
|
};
|
|
6193
6381
|
var NECK_CELLS = { px: 20, min: 3, max: 12 };
|
|
@@ -6239,7 +6427,18 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
6239
6427
|
const solveFrame = buildHeadFrame(layers, frameOptions);
|
|
6240
6428
|
const ctx = solveContext(solveFrame, byRole, turnOptions, hasNose);
|
|
6241
6429
|
const boxes = Object.fromEntries(
|
|
6242
|
-
[
|
|
6430
|
+
[
|
|
6431
|
+
"eye_L",
|
|
6432
|
+
"eye_R",
|
|
6433
|
+
"iris_L",
|
|
6434
|
+
"iris_R",
|
|
6435
|
+
"brow_L",
|
|
6436
|
+
"brow_R",
|
|
6437
|
+
"hair_side_L",
|
|
6438
|
+
"hair_side_R",
|
|
6439
|
+
"lash_L",
|
|
6440
|
+
"lash_R"
|
|
6441
|
+
].filter(has).map((r) => [r, box(r)])
|
|
6243
6442
|
);
|
|
6244
6443
|
const landmarks = {
|
|
6245
6444
|
boxes,
|
|
@@ -6444,6 +6643,8 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
|
|
|
6444
6643
|
headStart = fm.headStart;
|
|
6445
6644
|
} else if (spec.role === "hair_front") {
|
|
6446
6645
|
mesh = columnMesh(b, frontGrid);
|
|
6646
|
+
} else if (spec.role === "hair_side_L" || spec.role === "hair_side_R") {
|
|
6647
|
+
mesh = columnMesh(b, sideLockGrid(b, frame, spec.role));
|
|
6447
6648
|
} else if (lips !== void 0 && (isLipRole(spec.role) || isLipInsideRole(spec.role))) {
|
|
6448
6649
|
({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
|
|
6449
6650
|
} else if (spec.family !== "body") {
|
|
@@ -6458,6 +6659,7 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
|
|
|
6458
6659
|
const side = spec.role.endsWith("_L") ? "L" : "R";
|
|
6459
6660
|
const eyeOpen = side === "L" ? P10.EyeOpenLeft : P10.EyeOpenRight;
|
|
6460
6661
|
const hh = frame.hh;
|
|
6662
|
+
const swayAmp = plan.hair.sway * plan.profile.AMPLITUDE.sway * hh;
|
|
6461
6663
|
switch (spec.role) {
|
|
6462
6664
|
case "face":
|
|
6463
6665
|
if (region?.some((r) => r === "neck")) {
|
|
@@ -6594,18 +6796,34 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
|
|
|
6594
6796
|
case "hair_front":
|
|
6595
6797
|
case "hair_back":
|
|
6596
6798
|
if (byRole.has("hair_front")) {
|
|
6597
|
-
const amp = plan.hair.sway * plan.profile.AMPLITUDE.sway * hh;
|
|
6598
6799
|
if (spec.role === "hair_front") {
|
|
6599
6800
|
warps.push(
|
|
6600
|
-
hairSway(P10.HairSwayX, mesh, b,
|
|
6601
|
-
hairSway(P10.HairSwayZ, mesh, b,
|
|
6801
|
+
hairSway(P10.HairSwayX, mesh, b, swayAmp),
|
|
6802
|
+
hairSway(P10.HairSwayZ, mesh, b, swayAmp)
|
|
6602
6803
|
);
|
|
6603
6804
|
} else {
|
|
6604
|
-
warps.push(hairSway(P10.HairBackSway, mesh, b,
|
|
6805
|
+
warps.push(hairSway(P10.HairBackSway, mesh, b, swayAmp));
|
|
6605
6806
|
}
|
|
6606
6807
|
}
|
|
6607
6808
|
warps.push(tiltHang(mesh, b, pivot, hairHangWeight(frame.chinY, b.y0)));
|
|
6608
6809
|
break;
|
|
6810
|
+
case "hair_side_L":
|
|
6811
|
+
case "hair_side_R": {
|
|
6812
|
+
const { rootEnd, weight } = sideRootBlend(frame, spec.role, b);
|
|
6813
|
+
const attach = { box: box("hair_front"), weight };
|
|
6814
|
+
warps.push(
|
|
6815
|
+
lockSway(P10.HairSwayX, mesh, b, swayAmp, attach),
|
|
6816
|
+
lockSway(P10.HairSwayZ, mesh, b, swayAmp, attach),
|
|
6817
|
+
// Under a roll it swings about its root, where it leaves the bangs.
|
|
6818
|
+
tiltHang(
|
|
6819
|
+
mesh,
|
|
6820
|
+
b,
|
|
6821
|
+
[(b.x0 + b.x1) / 2, rootEnd],
|
|
6822
|
+
lockHangWeight(weight)
|
|
6823
|
+
)
|
|
6824
|
+
);
|
|
6825
|
+
break;
|
|
6826
|
+
}
|
|
6609
6827
|
}
|
|
6610
6828
|
if (warps.length > 0) part.warps = warps;
|
|
6611
6829
|
if (bindings.length > 0) part.bindings = bindings;
|
|
@@ -6662,9 +6880,34 @@ function hairFrontGrid(b, frame, boxes) {
|
|
|
6662
6880
|
}
|
|
6663
6881
|
return { xs, ys };
|
|
6664
6882
|
}
|
|
6883
|
+
var BLEND_ROWS = 3;
|
|
6884
|
+
function sideLockGrid(b, frame, role) {
|
|
6885
|
+
const cells = MESH_CELLS[role];
|
|
6886
|
+
const px = Math.min(cells.px, meshScale(role, frame));
|
|
6887
|
+
const cols = cellsFor(bw(b), px, cells.min, cells.max);
|
|
6888
|
+
const rows = cellsFor(bh(b), px, cells.min, cells.max);
|
|
6889
|
+
const xs = Array.from(
|
|
6890
|
+
{ length: cols + 1 },
|
|
6891
|
+
(_, c) => b.x0 + bw(b) * c / cols
|
|
6892
|
+
);
|
|
6893
|
+
const ys = Array.from(
|
|
6894
|
+
{ length: rows + 1 },
|
|
6895
|
+
(_, r) => b.y1 - bh(b) * r / rows
|
|
6896
|
+
);
|
|
6897
|
+
const { rootEnd } = sideRootBlend(frame, role, b);
|
|
6898
|
+
const end = rootEnd - SIDE_ROOT_BLEND * frame.hh;
|
|
6899
|
+
for (let i = 0; i <= BLEND_ROWS + 1; i++) {
|
|
6900
|
+
const y = rootEnd + (end - rootEnd) * i / (BLEND_ROWS + 1);
|
|
6901
|
+
if (y > b.y0 + 0.25 && y < b.y1 - 0.25 && ys.every((v) => Math.abs(v - y) > 0.25))
|
|
6902
|
+
ys.push(y);
|
|
6903
|
+
}
|
|
6904
|
+
return { xs, ys: ys.sort((p, q) => q - p) };
|
|
6905
|
+
}
|
|
6665
6906
|
function meshScale(role, frame) {
|
|
6666
6907
|
switch (role) {
|
|
6667
6908
|
case "hair_front":
|
|
6909
|
+
case "hair_side_L":
|
|
6910
|
+
case "hair_side_R":
|
|
6668
6911
|
case "hair_back":
|
|
6669
6912
|
return Math.max(2, (frame.shellRight - frame.shellLeft) / 9);
|
|
6670
6913
|
default:
|