@ikijs/editor 0.28.0 → 0.30.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/README.md +25 -19
- package/dist/index.d.mts +68 -10
- package/dist/index.d.ts +68 -10
- package/dist/index.js +550 -382
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +550 -382
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1652,6 +1652,8 @@ var ROW_RUN_ROLES = /* @__PURE__ */ new Set([
|
|
|
1652
1652
|
"face",
|
|
1653
1653
|
"hair_front",
|
|
1654
1654
|
"hair_back",
|
|
1655
|
+
"hair_side_L",
|
|
1656
|
+
"hair_side_R",
|
|
1655
1657
|
"body",
|
|
1656
1658
|
"arm_L",
|
|
1657
1659
|
"arm_R",
|
|
@@ -2081,8 +2083,9 @@ function deriveProfile(v) {
|
|
|
2081
2083
|
/** The mouth's far corner rises (its near end drops) by this much: it
|
|
2082
2084
|
* stays level. */
|
|
2083
2085
|
mouthTiltDeg: 0,
|
|
2084
|
-
/** The front hair (bangs and
|
|
2085
|
-
* its
|
|
2086
|
+
/** The front hair (its bangs and its own side strands; a `hair_side_*` lock
|
|
2087
|
+
* takes it only down to its root, `hairSideField`) rides the face at this
|
|
2088
|
+
* share of its translation. */
|
|
2086
2089
|
hairFollow: v.hairFollow,
|
|
2087
2090
|
/** The head's outline at the eye row holds: where the front hair draws
|
|
2088
2091
|
* it, its outer edge follows the face this much on a row where no back
|
|
@@ -2298,177 +2301,15 @@ function meshPoints(mesh, box) {
|
|
|
2298
2301
|
return out;
|
|
2299
2302
|
}
|
|
2300
2303
|
|
|
2301
|
-
// src/auto-rig/roles.ts
|
|
2302
|
-
var import_format3 = require("@ikijs/format");
|
|
2303
|
-
var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
|
|
2304
|
-
var isLipRole = (role) => LIP_ROLES.includes(role);
|
|
2305
|
-
var LIP_INSIDE_ROLES = ["mouth_tongue", "mouth_teeth"];
|
|
2306
|
-
var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
|
|
2307
|
-
var SHUT_BY = new Map([
|
|
2308
|
-
["eye_L", import_format3.StandardParameter.EyeOpenLeft],
|
|
2309
|
-
["lash_lower_L", import_format3.StandardParameter.EyeOpenLeft],
|
|
2310
|
-
["eye_R", import_format3.StandardParameter.EyeOpenRight],
|
|
2311
|
-
["lash_lower_R", import_format3.StandardParameter.EyeOpenRight],
|
|
2312
|
-
...[...LIP_ROLES, ...LIP_INSIDE_ROLES].map((id) => [
|
|
2313
|
-
id,
|
|
2314
|
-
import_format3.StandardParameter.MouthOpen
|
|
2315
|
-
])
|
|
2316
|
-
]);
|
|
2317
|
-
var ROLE_TABLE = [
|
|
2318
|
-
{ role: "hair_back", family: "hair_back" },
|
|
2319
|
-
{ role: "body", family: "body" },
|
|
2320
|
-
{ role: "arm_L", family: "arm", parts: ["elbow_L", "forearm_L", "arm_L"] },
|
|
2321
|
-
{ role: "arm_R", family: "arm", parts: ["elbow_R", "forearm_R", "arm_R"] },
|
|
2322
|
-
{ role: "face", family: "face", required: true },
|
|
2323
|
-
{ role: "blush_L", family: "face" },
|
|
2324
|
-
{ role: "blush_R", family: "face" },
|
|
2325
|
-
{ role: "nose", family: "nose" },
|
|
2326
|
-
{ role: "mouth", family: "mouth" },
|
|
2327
|
-
{ role: "mouth_open", family: "mouth" },
|
|
2328
|
-
// The lip set, back to front: the interior, the tongue and the teeth clipped
|
|
2329
|
-
// to it, the lower lip's skin, the upper lip and its line over both. It
|
|
2330
|
-
// folds open instead of crossfading (`mouth.ts`), so it stands in for
|
|
2331
|
-
// `mouth`, never beside it.
|
|
2332
|
-
{ role: "mouth_inner", family: "mouth" },
|
|
2333
|
-
{ role: "mouth_tongue", family: "mouth" },
|
|
2334
|
-
{ role: "mouth_teeth", family: "mouth" },
|
|
2335
|
-
{ role: "lip_lower", family: "mouth" },
|
|
2336
|
-
{ role: "lip_upper", family: "mouth" },
|
|
2337
|
-
{ role: "eye_L", family: "eye_L", required: true },
|
|
2338
|
-
{ role: "eye_R", family: "eye_R", required: true },
|
|
2339
|
-
{ role: "iris_L", family: "eye_L" },
|
|
2340
|
-
{ role: "iris_R", family: "eye_R" },
|
|
2341
|
-
{ role: "pupil_L", family: "eye_L" },
|
|
2342
|
-
{ role: "pupil_R", family: "eye_R" },
|
|
2343
|
-
{ role: "highlight_L", family: "eye_L" },
|
|
2344
|
-
{ role: "highlight_R", family: "eye_R" },
|
|
2345
|
-
{ role: "lash_lower_L", family: "eye_L" },
|
|
2346
|
-
{ role: "lash_lower_R", family: "eye_R" },
|
|
2347
|
-
{ role: "lash_L", family: "eye_L" },
|
|
2348
|
-
{ role: "lash_R", family: "eye_R" },
|
|
2349
|
-
{ role: "brow_L", family: "brow_L" },
|
|
2350
|
-
{ role: "brow_R", family: "brow_R" },
|
|
2351
|
-
{ role: "hair_front", family: "hair_front" },
|
|
2352
|
-
// Drawn above the face and the hair: a raised hand passes them.
|
|
2353
|
-
{ role: "forearm_pose_L", family: "forearm_pose" },
|
|
2354
|
-
{ role: "forearm_pose_R", family: "forearm_pose" }
|
|
2355
|
-
];
|
|
2356
|
-
var REQUIRED_ROLES = ROLE_TABLE.filter(
|
|
2357
|
-
(r) => r.required
|
|
2358
|
-
).map((r) => r.role);
|
|
2359
|
-
var SPEC_BY_ROLE = new Map(ROLE_TABLE.map((r, i) => [r.role, { ...r, i }]));
|
|
2360
|
-
function roleSpec(role) {
|
|
2361
|
-
const spec = SPEC_BY_ROLE.get(role);
|
|
2362
|
-
if (spec === void 0) throw new Error(`auto-rig: unknown role "${role}"`);
|
|
2363
|
-
return spec;
|
|
2364
|
-
}
|
|
2365
|
-
function partIdsOfRole(role) {
|
|
2366
|
-
return roleSpec(role).parts ?? [role];
|
|
2367
|
-
}
|
|
2368
|
-
var ROLE_BY_PART = new Map(
|
|
2369
|
-
ROLE_TABLE.flatMap((r) => (r.parts ?? []).map((p) => [p, r.role]))
|
|
2370
|
-
);
|
|
2371
|
-
function roleOfPart(partId) {
|
|
2372
|
-
return ROLE_BY_PART.get(partId) ?? partId;
|
|
2373
|
-
}
|
|
2374
|
-
var ALIASES = {
|
|
2375
|
-
eyebrow_L: "brow_L",
|
|
2376
|
-
eyebrow_R: "brow_R",
|
|
2377
|
-
eye_white_L: "eye_L",
|
|
2378
|
-
eye_white_R: "eye_R"
|
|
2379
|
-
};
|
|
2380
|
-
function roleOf(fileName) {
|
|
2381
|
-
const role = fileName.replace(/\.[^.]+$/, "").toLowerCase().replace(/[-\s]+/g, "_").replace(/_([lr])$/, (_, s) => `_${s.toUpperCase()}`);
|
|
2382
|
-
return ALIASES[role] ?? role;
|
|
2383
|
-
}
|
|
2384
|
-
function checkArmsHaveBody(roles) {
|
|
2385
|
-
const has = new Set(roles);
|
|
2386
|
-
if (has.has("body")) return;
|
|
2387
|
-
const arm = ROLE_TABLE.find((r) => r.family === "arm" && has.has(r.role));
|
|
2388
|
-
if (arm !== void 0) {
|
|
2389
|
-
throw new Error(
|
|
2390
|
-
`auto-rig: ${arm.role} needs a body layer (an arm hangs from the body's shoulder)`
|
|
2391
|
-
);
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
function checkMouth(roles) {
|
|
2395
|
-
const has = new Set(roles);
|
|
2396
|
-
const present = LIP_ROLES.filter((r) => has.has(r));
|
|
2397
|
-
const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
|
|
2398
|
-
if (inside.length > 0 && present.length === 0) {
|
|
2399
|
-
throw new Error(
|
|
2400
|
-
`auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
|
|
2401
|
-
);
|
|
2402
|
-
}
|
|
2403
|
-
if (present.length === 0) {
|
|
2404
|
-
if (!has.has("mouth")) {
|
|
2405
|
-
throw new Error(
|
|
2406
|
-
`auto-rig: missing required role mouth (or the lip set ${LIP_ROLES.join(", ")})`
|
|
2407
|
-
);
|
|
2408
|
-
}
|
|
2409
|
-
return;
|
|
2410
|
-
}
|
|
2411
|
-
if (present.length < LIP_ROLES.length) {
|
|
2412
|
-
throw new Error(
|
|
2413
|
-
`auto-rig: the lip set is partial: missing ${LIP_ROLES.filter((r) => !has.has(r)).join(", ")}`
|
|
2414
|
-
);
|
|
2415
|
-
}
|
|
2416
|
-
const mixed = ["mouth", "mouth_open"].filter((r) => has.has(r));
|
|
2417
|
-
if (mixed.length > 0) {
|
|
2418
|
-
throw new Error(
|
|
2419
|
-
`auto-rig: the lip set cannot be mixed with ${mixed.join(", ")} (the lip set replaces the mouth drawings; use one or the other)`
|
|
2420
|
-
);
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
function checkPosesHaveArms(roles) {
|
|
2424
|
-
const has = new Set(roles);
|
|
2425
|
-
for (const side of ["L", "R"]) {
|
|
2426
|
-
if (has.has(`forearm_pose_${side}`) && !has.has(`arm_${side}`)) {
|
|
2427
|
-
throw new Error(
|
|
2428
|
-
`auto-rig: forearm_pose_${side} needs an arm_${side} layer (a pose forearm hangs from its arm's elbow)`
|
|
2429
|
-
);
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
}
|
|
2433
|
-
function parseLayerRoles(fileNames) {
|
|
2434
|
-
const seen = /* @__PURE__ */ new Map();
|
|
2435
|
-
const out = [];
|
|
2436
|
-
for (const fileName of fileNames) {
|
|
2437
|
-
const role = roleOf(fileName);
|
|
2438
|
-
if (!SPEC_BY_ROLE.has(role)) {
|
|
2439
|
-
throw new Error(
|
|
2440
|
-
`auto-rig: "${fileName}" names no known role (expected one of ${ROLE_TABLE.map((r) => r.role).join(", ")})`
|
|
2441
|
-
);
|
|
2442
|
-
}
|
|
2443
|
-
const prior = seen.get(role);
|
|
2444
|
-
if (prior !== void 0) {
|
|
2445
|
-
throw new Error(
|
|
2446
|
-
`auto-rig: role "${role}" is named twice ("${prior}" and "${fileName}")`
|
|
2447
|
-
);
|
|
2448
|
-
}
|
|
2449
|
-
seen.set(role, fileName);
|
|
2450
|
-
out.push({ role, fileName });
|
|
2451
|
-
}
|
|
2452
|
-
const missing = REQUIRED_ROLES.filter((r) => !seen.has(r));
|
|
2453
|
-
if (missing.length > 0) {
|
|
2454
|
-
throw new Error(
|
|
2455
|
-
`auto-rig: missing required role${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`
|
|
2456
|
-
);
|
|
2457
|
-
}
|
|
2458
|
-
checkMouth(seen.keys());
|
|
2459
|
-
checkArmsHaveBody(seen.keys());
|
|
2460
|
-
checkPosesHaveArms(seen.keys());
|
|
2461
|
-
return out;
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
2304
|
// src/auto-rig/mouth.ts
|
|
2465
2305
|
var import_format5 = require("@ikijs/format");
|
|
2466
2306
|
|
|
2467
2307
|
// src/auto-rig/drivers.ts
|
|
2468
|
-
var
|
|
2308
|
+
var import_format3 = require("@ikijs/format");
|
|
2469
2309
|
var SWAY_RANGE = 20;
|
|
2470
2310
|
var SWAY_CURVE = 1.4;
|
|
2471
2311
|
var HAIR_HANG = 0.35;
|
|
2312
|
+
var LOCK_HANG_SPAN = 1;
|
|
2472
2313
|
var Z_STOPS = [-30, 0, 30];
|
|
2473
2314
|
function localWarp(parameter, mesh, box, values, f) {
|
|
2474
2315
|
const pts = meshPoints(mesh, box);
|
|
@@ -2543,13 +2384,13 @@ function gazeBindings(irisW, share = 1) {
|
|
|
2543
2384
|
const gy = AMPLITUDE.gazeY * irisW * share;
|
|
2544
2385
|
return [
|
|
2545
2386
|
{
|
|
2546
|
-
parameter:
|
|
2387
|
+
parameter: import_format3.StandardParameter.EyeballX,
|
|
2547
2388
|
channel: "translateX",
|
|
2548
2389
|
from: -round1(gx),
|
|
2549
2390
|
to: round1(gx)
|
|
2550
2391
|
},
|
|
2551
2392
|
{
|
|
2552
|
-
parameter:
|
|
2393
|
+
parameter: import_format3.StandardParameter.EyeballY,
|
|
2553
2394
|
channel: "translateY",
|
|
2554
2395
|
from: -round1(gy),
|
|
2555
2396
|
to: round1(gy)
|
|
@@ -2563,8 +2404,8 @@ function browReach(hh) {
|
|
|
2563
2404
|
return AMPLITUDE.brow * hh;
|
|
2564
2405
|
}
|
|
2565
2406
|
function browBindings(side, reach) {
|
|
2566
|
-
const y = side === "L" ?
|
|
2567
|
-
const a = side === "L" ?
|
|
2407
|
+
const y = side === "L" ? import_format3.StandardParameter.BrowLeftY : import_format3.StandardParameter.BrowRightY;
|
|
2408
|
+
const a = side === "L" ? import_format3.StandardParameter.BrowLeftAngle : import_format3.StandardParameter.BrowRightAngle;
|
|
2568
2409
|
return [
|
|
2569
2410
|
{
|
|
2570
2411
|
parameter: y,
|
|
@@ -2583,7 +2424,7 @@ var mouthFrameOf = (box) => ({
|
|
|
2583
2424
|
});
|
|
2584
2425
|
function mouthForm(mesh, box, frame = mouthFrameOf(box)) {
|
|
2585
2426
|
const { c, half, amp: a } = frame;
|
|
2586
|
-
return localWarp(
|
|
2427
|
+
return localWarp(import_format3.StandardParameter.MouthForm, mesh, box, [-1, 0, 1], ([x], v) => {
|
|
2587
2428
|
if (v === 0) return [0, 0];
|
|
2588
2429
|
const u = (x - c) / half;
|
|
2589
2430
|
const widen = v > 0 ? 0.05 : 0.03;
|
|
@@ -2593,7 +2434,7 @@ function mouthForm(mesh, box, frame = mouthFrameOf(box)) {
|
|
|
2593
2434
|
function mouthOpenGrow(mesh, box) {
|
|
2594
2435
|
const c = cx(box);
|
|
2595
2436
|
return localWarp(
|
|
2596
|
-
|
|
2437
|
+
import_format3.StandardParameter.MouthOpen,
|
|
2597
2438
|
mesh,
|
|
2598
2439
|
box,
|
|
2599
2440
|
[0, 1],
|
|
@@ -2602,25 +2443,38 @@ function mouthOpenGrow(mesh, box) {
|
|
|
2602
2443
|
}
|
|
2603
2444
|
function mouthWiden(mesh, box, frame = mouthFrameOf(box)) {
|
|
2604
2445
|
const c = frame.c;
|
|
2605
|
-
return localWarp(
|
|
2446
|
+
return localWarp(import_format3.StandardParameter.MouthOpen, mesh, box, [0, 1], ([x], v) => [
|
|
2606
2447
|
v * (AMPLITUDE.mouthOpenWidth - 1) * (x - c),
|
|
2607
2448
|
0
|
|
2608
2449
|
]);
|
|
2609
2450
|
}
|
|
2451
|
+
function swayAt(b, y) {
|
|
2452
|
+
return Math.min(1, Math.max(0, (b.y1 - y) / bh(b))) ** SWAY_CURVE;
|
|
2453
|
+
}
|
|
2610
2454
|
function hairSway(param, mesh, box, amp) {
|
|
2455
|
+
return localWarp(
|
|
2456
|
+
param,
|
|
2457
|
+
mesh,
|
|
2458
|
+
box,
|
|
2459
|
+
[-SWAY_RANGE, 0, SWAY_RANGE],
|
|
2460
|
+
([, y], v) => [v / SWAY_RANGE * amp * swayAt(box, y), 0]
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
function lockSway(param, mesh, box, amp, attach) {
|
|
2611
2464
|
return localWarp(
|
|
2612
2465
|
param,
|
|
2613
2466
|
mesh,
|
|
2614
2467
|
box,
|
|
2615
2468
|
[-SWAY_RANGE, 0, SWAY_RANGE],
|
|
2616
2469
|
([, y], v) => {
|
|
2617
|
-
const
|
|
2618
|
-
|
|
2470
|
+
const w = attach.weight(y);
|
|
2471
|
+
const share = (1 - w) * swayAt(attach.box, y) + w * swayAt(box, y);
|
|
2472
|
+
return [v / SWAY_RANGE * amp * share, 0];
|
|
2619
2473
|
}
|
|
2620
2474
|
);
|
|
2621
2475
|
}
|
|
2622
2476
|
function tiltHang(mesh, box, pivot, weight, deg = ROLL_DEG) {
|
|
2623
|
-
return localWarp(
|
|
2477
|
+
return localWarp(import_format3.StandardParameter.AngleZ, mesh, box, Z_STOPS, ([x, y], v, i) => {
|
|
2624
2478
|
const g = weight(y, i);
|
|
2625
2479
|
if (g === 0 || v === 0) return [0, 0];
|
|
2626
2480
|
const a = -g * rollOf(v, deg);
|
|
@@ -2636,8 +2490,191 @@ function hairHangWeight(pivotY, bottom) {
|
|
|
2636
2490
|
if (bottom >= pivotY) return () => 0;
|
|
2637
2491
|
return (y) => HAIR_HANG * smoothstep(pivotY, bottom, y);
|
|
2638
2492
|
}
|
|
2493
|
+
function lockHangWeight(pivotY, bottom, hh) {
|
|
2494
|
+
return hairHangWeight(pivotY, Math.min(bottom, pivotY - LOCK_HANG_SPAN * hh));
|
|
2495
|
+
}
|
|
2639
2496
|
var round1 = (v) => roundTo(v, 0.1);
|
|
2640
2497
|
|
|
2498
|
+
// src/auto-rig/roles.ts
|
|
2499
|
+
var import_format4 = require("@ikijs/format");
|
|
2500
|
+
var LIP_ROLES = ["mouth_inner", "lip_lower", "lip_upper"];
|
|
2501
|
+
var isLipRole = (role) => LIP_ROLES.includes(role);
|
|
2502
|
+
var LIP_INSIDE_ROLES = ["mouth_tongue", "mouth_teeth"];
|
|
2503
|
+
var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
|
|
2504
|
+
var SHUT_BY = new Map([
|
|
2505
|
+
["eye_L", import_format4.StandardParameter.EyeOpenLeft],
|
|
2506
|
+
["lash_lower_L", import_format4.StandardParameter.EyeOpenLeft],
|
|
2507
|
+
["eye_R", import_format4.StandardParameter.EyeOpenRight],
|
|
2508
|
+
["lash_lower_R", import_format4.StandardParameter.EyeOpenRight],
|
|
2509
|
+
...[...LIP_ROLES, ...LIP_INSIDE_ROLES].map((id) => [
|
|
2510
|
+
id,
|
|
2511
|
+
import_format4.StandardParameter.MouthOpen
|
|
2512
|
+
])
|
|
2513
|
+
]);
|
|
2514
|
+
var ROLE_TABLE = [
|
|
2515
|
+
{ role: "hair_back", family: "hair_back" },
|
|
2516
|
+
{ role: "body", family: "body" },
|
|
2517
|
+
{ role: "arm_L", family: "arm", parts: ["elbow_L", "forearm_L", "arm_L"] },
|
|
2518
|
+
{ role: "arm_R", family: "arm", parts: ["elbow_R", "forearm_R", "arm_R"] },
|
|
2519
|
+
{ role: "face", family: "face", required: true },
|
|
2520
|
+
{ role: "blush_L", family: "face" },
|
|
2521
|
+
{ role: "blush_R", family: "face" },
|
|
2522
|
+
{ role: "nose", family: "nose" },
|
|
2523
|
+
{ role: "mouth", family: "mouth" },
|
|
2524
|
+
{ role: "mouth_open", family: "mouth" },
|
|
2525
|
+
// The lip set, back to front: the interior, the tongue and the teeth clipped
|
|
2526
|
+
// to it, the lower lip's skin, the upper lip and its line over both. It
|
|
2527
|
+
// folds open instead of crossfading (`mouth.ts`), so it stands in for
|
|
2528
|
+
// `mouth`, never beside it.
|
|
2529
|
+
{ role: "mouth_inner", family: "mouth" },
|
|
2530
|
+
{ role: "mouth_tongue", family: "mouth" },
|
|
2531
|
+
{ role: "mouth_teeth", family: "mouth" },
|
|
2532
|
+
{ role: "lip_lower", family: "mouth" },
|
|
2533
|
+
{ role: "lip_upper", family: "mouth" },
|
|
2534
|
+
{ role: "eye_L", family: "eye_L", required: true },
|
|
2535
|
+
{ role: "eye_R", family: "eye_R", required: true },
|
|
2536
|
+
{ role: "iris_L", family: "eye_L" },
|
|
2537
|
+
{ role: "iris_R", family: "eye_R" },
|
|
2538
|
+
{ role: "pupil_L", family: "eye_L" },
|
|
2539
|
+
{ role: "pupil_R", family: "eye_R" },
|
|
2540
|
+
{ role: "highlight_L", family: "eye_L" },
|
|
2541
|
+
{ role: "highlight_R", family: "eye_R" },
|
|
2542
|
+
{ role: "lash_lower_L", family: "eye_L" },
|
|
2543
|
+
{ role: "lash_lower_R", family: "eye_R" },
|
|
2544
|
+
{ role: "lash_L", family: "eye_L" },
|
|
2545
|
+
{ role: "lash_R", family: "eye_R" },
|
|
2546
|
+
{ role: "brow_L", family: "brow_L" },
|
|
2547
|
+
{ role: "brow_R", family: "brow_R" },
|
|
2548
|
+
// The front side locks: over the brows and lashes, under the bangs, which
|
|
2549
|
+
// hide their roots.
|
|
2550
|
+
{ role: "hair_side_L", family: "hair_side" },
|
|
2551
|
+
{ role: "hair_side_R", family: "hair_side" },
|
|
2552
|
+
{ role: "hair_front", family: "hair_front" },
|
|
2553
|
+
// Drawn above the face and the hair: a raised hand passes them.
|
|
2554
|
+
{ role: "forearm_pose_L", family: "forearm_pose" },
|
|
2555
|
+
{ role: "forearm_pose_R", family: "forearm_pose" }
|
|
2556
|
+
];
|
|
2557
|
+
var REQUIRED_ROLES = ROLE_TABLE.filter(
|
|
2558
|
+
(r) => r.required
|
|
2559
|
+
).map((r) => r.role);
|
|
2560
|
+
var SPEC_BY_ROLE = new Map(ROLE_TABLE.map((r, i) => [r.role, { ...r, i }]));
|
|
2561
|
+
function roleSpec(role) {
|
|
2562
|
+
const spec = SPEC_BY_ROLE.get(role);
|
|
2563
|
+
if (spec === void 0) throw new Error(`auto-rig: unknown role "${role}"`);
|
|
2564
|
+
return spec;
|
|
2565
|
+
}
|
|
2566
|
+
function partIdsOfRole(role) {
|
|
2567
|
+
return roleSpec(role).parts ?? [role];
|
|
2568
|
+
}
|
|
2569
|
+
var ROLE_BY_PART = new Map(
|
|
2570
|
+
ROLE_TABLE.flatMap((r) => (r.parts ?? []).map((p) => [p, r.role]))
|
|
2571
|
+
);
|
|
2572
|
+
function roleOfPart(partId) {
|
|
2573
|
+
return ROLE_BY_PART.get(partId) ?? partId;
|
|
2574
|
+
}
|
|
2575
|
+
var ALIASES = {
|
|
2576
|
+
eyebrow_L: "brow_L",
|
|
2577
|
+
eyebrow_R: "brow_R",
|
|
2578
|
+
eye_white_L: "eye_L",
|
|
2579
|
+
eye_white_R: "eye_R"
|
|
2580
|
+
};
|
|
2581
|
+
function roleOf(fileName) {
|
|
2582
|
+
const role = fileName.replace(/\.[^.]+$/, "").toLowerCase().replace(/[-\s]+/g, "_").replace(/_([lr])$/, (_, s) => `_${s.toUpperCase()}`);
|
|
2583
|
+
return ALIASES[role] ?? role;
|
|
2584
|
+
}
|
|
2585
|
+
function checkArmsHaveBody(roles) {
|
|
2586
|
+
const has = new Set(roles);
|
|
2587
|
+
if (has.has("body")) return;
|
|
2588
|
+
const arm = ROLE_TABLE.find((r) => r.family === "arm" && has.has(r.role));
|
|
2589
|
+
if (arm !== void 0) {
|
|
2590
|
+
throw new Error(
|
|
2591
|
+
`auto-rig: ${arm.role} needs a body layer (an arm hangs from the body's shoulder)`
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
function checkSidesHaveFront(roles) {
|
|
2596
|
+
const has = new Set(roles);
|
|
2597
|
+
if (has.has("hair_front")) return;
|
|
2598
|
+
const side = ROLE_TABLE.find(
|
|
2599
|
+
(r) => r.family === "hair_side" && has.has(r.role)
|
|
2600
|
+
);
|
|
2601
|
+
if (side !== void 0) {
|
|
2602
|
+
throw new Error(
|
|
2603
|
+
`auto-rig: ${side.role} needs a hair_front layer (a side lock's root hides under the bangs)`
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
function checkMouth(roles) {
|
|
2608
|
+
const has = new Set(roles);
|
|
2609
|
+
const present = LIP_ROLES.filter((r) => has.has(r));
|
|
2610
|
+
const inside = LIP_INSIDE_ROLES.filter((r) => has.has(r));
|
|
2611
|
+
if (inside.length > 0 && present.length === 0) {
|
|
2612
|
+
throw new Error(
|
|
2613
|
+
`auto-rig: ${inside[0]} needs the lip set ${LIP_ROLES.join(", ")} (it is cut from the interior and clipped to it)`
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
if (present.length === 0) {
|
|
2617
|
+
if (!has.has("mouth")) {
|
|
2618
|
+
throw new Error(
|
|
2619
|
+
`auto-rig: missing required role mouth (or the lip set ${LIP_ROLES.join(", ")})`
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
if (present.length < LIP_ROLES.length) {
|
|
2625
|
+
throw new Error(
|
|
2626
|
+
`auto-rig: the lip set is partial: missing ${LIP_ROLES.filter((r) => !has.has(r)).join(", ")}`
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
const mixed = ["mouth", "mouth_open"].filter((r) => has.has(r));
|
|
2630
|
+
if (mixed.length > 0) {
|
|
2631
|
+
throw new Error(
|
|
2632
|
+
`auto-rig: the lip set cannot be mixed with ${mixed.join(", ")} (the lip set replaces the mouth drawings; use one or the other)`
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
function checkPosesHaveArms(roles) {
|
|
2637
|
+
const has = new Set(roles);
|
|
2638
|
+
for (const side of ["L", "R"]) {
|
|
2639
|
+
if (has.has(`forearm_pose_${side}`) && !has.has(`arm_${side}`)) {
|
|
2640
|
+
throw new Error(
|
|
2641
|
+
`auto-rig: forearm_pose_${side} needs an arm_${side} layer (a pose forearm hangs from its arm's elbow)`
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
function parseLayerRoles(fileNames) {
|
|
2647
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2648
|
+
const out = [];
|
|
2649
|
+
for (const fileName of fileNames) {
|
|
2650
|
+
const role = roleOf(fileName);
|
|
2651
|
+
if (!SPEC_BY_ROLE.has(role)) {
|
|
2652
|
+
throw new Error(
|
|
2653
|
+
`auto-rig: "${fileName}" names no known role (expected one of ${ROLE_TABLE.map((r) => r.role).join(", ")})`
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
const prior = seen.get(role);
|
|
2657
|
+
if (prior !== void 0) {
|
|
2658
|
+
throw new Error(
|
|
2659
|
+
`auto-rig: role "${role}" is named twice ("${prior}" and "${fileName}")`
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
seen.set(role, fileName);
|
|
2663
|
+
out.push({ role, fileName });
|
|
2664
|
+
}
|
|
2665
|
+
const missing = REQUIRED_ROLES.filter((r) => !seen.has(r));
|
|
2666
|
+
if (missing.length > 0) {
|
|
2667
|
+
throw new Error(
|
|
2668
|
+
`auto-rig: missing required role${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
checkMouth(seen.keys());
|
|
2672
|
+
checkArmsHaveBody(seen.keys());
|
|
2673
|
+
checkPosesHaveArms(seen.keys());
|
|
2674
|
+
checkSidesHaveFront(seen.keys());
|
|
2675
|
+
return out;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2641
2678
|
// src/auto-rig/mouth.ts
|
|
2642
2679
|
var UPPER_SHARE = 0.4;
|
|
2643
2680
|
var CLOSED_SAG_MAX = 0.1;
|
|
@@ -3033,6 +3070,7 @@ function buildHeadFrame(layers, opts) {
|
|
|
3033
3070
|
let paintedHalfAt = () => plateHalf;
|
|
3034
3071
|
let neck;
|
|
3035
3072
|
let ears;
|
|
3073
|
+
let headHalfAt = (y) => paintedHalfAt(y);
|
|
3036
3074
|
let headMax = plateHalf;
|
|
3037
3075
|
let chinY = face.y0;
|
|
3038
3076
|
let cutAt = () => face.y0 - 1;
|
|
@@ -3051,6 +3089,12 @@ function buildHeadFrame(layers, opts) {
|
|
|
3051
3089
|
const t = clamp(r - i, 0, 1);
|
|
3052
3090
|
return prof[i] + (prof[j] - prof[i]) * t;
|
|
3053
3091
|
};
|
|
3092
|
+
const readRows = (rows) => (y) => {
|
|
3093
|
+
const r = canvasH / 2 - y - faceLayer.bbox.y - 0.5;
|
|
3094
|
+
const i = clamp(Math.floor(r), 0, rows.length - 1);
|
|
3095
|
+
const j = Math.min(i + 1, rows.length - 1);
|
|
3096
|
+
return rows[i] + (rows[j] - rows[i]) * clamp(r - i, 0, 1);
|
|
3097
|
+
};
|
|
3054
3098
|
const rawRows = raw;
|
|
3055
3099
|
const earless = (chin) => {
|
|
3056
3100
|
const rowOf = (y) => Math.round(canvasH / 2 - y - faceLayer.bbox.y - 0.5);
|
|
@@ -3151,6 +3195,7 @@ function buildHeadFrame(layers, opts) {
|
|
|
3151
3195
|
canvasH / 2 - nk.topY - faceLayer.bbox.y - 0.5
|
|
3152
3196
|
);
|
|
3153
3197
|
const head = earless(chinY);
|
|
3198
|
+
headHalfAt = readRows(head);
|
|
3154
3199
|
headMax = Math.max(...head);
|
|
3155
3200
|
const outlineAt = (d) => {
|
|
3156
3201
|
for (let r = neckRow; r >= 0; r--) {
|
|
@@ -3193,6 +3238,7 @@ function buildHeadFrame(layers, opts) {
|
|
|
3193
3238
|
};
|
|
3194
3239
|
} else {
|
|
3195
3240
|
const head = earless(chinY);
|
|
3241
|
+
headHalfAt = readRows(head);
|
|
3196
3242
|
headMax = Math.max(...head);
|
|
3197
3243
|
if (ears !== void 0) {
|
|
3198
3244
|
const bottomOf = (d) => {
|
|
@@ -3240,6 +3286,7 @@ function buildHeadFrame(layers, opts) {
|
|
|
3240
3286
|
chinY,
|
|
3241
3287
|
hh,
|
|
3242
3288
|
paintedHalfAt,
|
|
3289
|
+
headHalfAt,
|
|
3243
3290
|
wMax,
|
|
3244
3291
|
headMax,
|
|
3245
3292
|
hasProfile: profile !== null,
|
|
@@ -3257,6 +3304,96 @@ function buildHeadFrame(layers, opts) {
|
|
|
3257
3304
|
};
|
|
3258
3305
|
}
|
|
3259
3306
|
|
|
3307
|
+
// src/auto-rig/hair-side-room.ts
|
|
3308
|
+
var round3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
3309
|
+
var signed = (v) => round3(v) + 0;
|
|
3310
|
+
function headEdge(ctx, land2, y, side, ax) {
|
|
3311
|
+
const { frame } = ctx;
|
|
3312
|
+
return land2("face", frame.axisX + side * frame.headHalfAt(y), y, ax);
|
|
3313
|
+
}
|
|
3314
|
+
var LOCKS = ["hair_side_L", "hair_side_R"];
|
|
3315
|
+
function hairSideRoom(ctx, land2, boxes) {
|
|
3316
|
+
const out = {};
|
|
3317
|
+
for (const role of LOCKS) {
|
|
3318
|
+
const box = boxes[role];
|
|
3319
|
+
if (box === void 0) continue;
|
|
3320
|
+
const side = (box.x0 + box.x1) / 2 < ctx.frame.axisX ? -1 : 1;
|
|
3321
|
+
const key = side < 0 ? "left" : "right";
|
|
3322
|
+
if (out[key] !== void 0) continue;
|
|
3323
|
+
out[key] = lockRoom(ctx, land2, boxes, role, box, side);
|
|
3324
|
+
}
|
|
3325
|
+
return out.left || out.right ? out : void 0;
|
|
3326
|
+
}
|
|
3327
|
+
function lockRoom(ctx, land2, boxes, role, box, side) {
|
|
3328
|
+
const { frame } = ctx;
|
|
3329
|
+
if (frame.hairRuns(role, box.y1 - 0.5) === void 0) {
|
|
3330
|
+
return { role, unavailable: "no rowRuns" };
|
|
3331
|
+
}
|
|
3332
|
+
const room = { role };
|
|
3333
|
+
const stops = farStops(side);
|
|
3334
|
+
let cover;
|
|
3335
|
+
for (let i = 0; i < Math.ceil(box.y1 - box.y0); i++) {
|
|
3336
|
+
const y2 = box.y1 - i - 0.5;
|
|
3337
|
+
if (y2 < frame.face.y0 || y2 > frame.face.y1) continue;
|
|
3338
|
+
const outer2 = outerEdge(frame.hairRuns(role, y2) ?? [], frame.axisX, side);
|
|
3339
|
+
if (outer2 === void 0) continue;
|
|
3340
|
+
const rest = side * (outer2 - frame.axisX) - frame.headHalfAt(y2);
|
|
3341
|
+
if (rest < 0) continue;
|
|
3342
|
+
for (const ax of stops) {
|
|
3343
|
+
const px = side * (land2(role, outer2, y2, ax) - headEdge(ctx, land2, y2, side, ax));
|
|
3344
|
+
if (cover === void 0 || px < cover.px)
|
|
3345
|
+
cover = { deg: ax, px, y: y2, rest };
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
if (cover !== void 0) {
|
|
3349
|
+
room.cover = {
|
|
3350
|
+
deg: cover.deg,
|
|
3351
|
+
px: signed(cover.px),
|
|
3352
|
+
y: round3(cover.y),
|
|
3353
|
+
rest: signed(cover.rest)
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
const eye = side < 0 ? ctx.eyeBoxes.left : ctx.eyeBoxes.right;
|
|
3357
|
+
const eyeRole = (side < 0 ? ctx.left : ctx.right).role.replace(
|
|
3358
|
+
/^iris_/,
|
|
3359
|
+
"eye_"
|
|
3360
|
+
);
|
|
3361
|
+
const y = cy(eye);
|
|
3362
|
+
const runs = frame.hairRuns(role, y) ?? [];
|
|
3363
|
+
const outer = outerEdge(runs, frame.axisX, side);
|
|
3364
|
+
if (outer === void 0) return room;
|
|
3365
|
+
const inner = outer - side * runReach(runs, outer, side).inward;
|
|
3366
|
+
const lashRole = eyeRole.replace(/^eye_/, "lash_");
|
|
3367
|
+
const lash = boxes[lashRole];
|
|
3368
|
+
let tail;
|
|
3369
|
+
for (const ax of stops) {
|
|
3370
|
+
const lock = land2(role, inner, y, ax);
|
|
3371
|
+
const corner = land2(eyeRole, side < 0 ? eye.x0 : eye.x1, y, ax);
|
|
3372
|
+
const eyeToLock = side * (lock - corner);
|
|
3373
|
+
if (tail !== void 0 && eyeToLock >= tail.eyeToLock) continue;
|
|
3374
|
+
tail = {
|
|
3375
|
+
deg: ax,
|
|
3376
|
+
eyeToLock,
|
|
3377
|
+
eyeToPlate: side * (headEdge(ctx, land2, y, side, ax) - corner)
|
|
3378
|
+
};
|
|
3379
|
+
if (lash !== void 0) {
|
|
3380
|
+
const tip = land2(lashRole, side < 0 ? lash.x0 : lash.x1, y, ax);
|
|
3381
|
+
tail.lashToLock = side * (lock - tip);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
if (tail !== void 0) {
|
|
3385
|
+
room.tail = {
|
|
3386
|
+
deg: tail.deg,
|
|
3387
|
+
eyeToLock: signed(tail.eyeToLock),
|
|
3388
|
+
...tail.lashToLock !== void 0 && {
|
|
3389
|
+
lashToLock: signed(tail.lashToLock)
|
|
3390
|
+
},
|
|
3391
|
+
eyeToPlate: signed(tail.eyeToPlate)
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
return room;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3260
3397
|
// src/auto-rig/fields.ts
|
|
3261
3398
|
var NOSE_TILT_DEG = 6;
|
|
3262
3399
|
function nodDy(v, ay, hh) {
|
|
@@ -3458,12 +3595,12 @@ function hairFrontBends(frame, boxes) {
|
|
|
3458
3595
|
}
|
|
3459
3596
|
function hairFrontField(m) {
|
|
3460
3597
|
const f = m.frame;
|
|
3461
|
-
const back = hairField(m, m.hairBack, NOD.hairBack);
|
|
3462
3598
|
const cap = { up: NOD.hairFrontTop.up, down: capSlide(m) };
|
|
3463
3599
|
const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
|
|
3464
3600
|
(e) => e !== void 0
|
|
3465
3601
|
);
|
|
3466
3602
|
const { inner, left, right } = hairFrontBends(f, m.boxes);
|
|
3603
|
+
const capTop = m.hairFrontGrid?.ys.find((y) => y <= f.face.y1) ?? f.face.y1;
|
|
3467
3604
|
const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
|
|
3468
3605
|
const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
|
|
3469
3606
|
const coreAt = (x, y, s) => {
|
|
@@ -3496,22 +3633,60 @@ function hairFrontField(m) {
|
|
|
3496
3633
|
);
|
|
3497
3634
|
const followOf = (x, y, s) => followAt(x, y, s) + (ride === void 0 ? 0 : ride.lift(x, y, s));
|
|
3498
3635
|
const drift = Math.abs(m.hairBack);
|
|
3499
|
-
const crown = crownRide(m, cap, (x, y, s) => followOf(x, y, s) + drift);
|
|
3500
3636
|
const boundOf = (x, y, s) => {
|
|
3501
3637
|
if (ride === void 0) return followOf(x, y, s);
|
|
3502
|
-
const b = ride.blend(y, s);
|
|
3638
|
+
const b = ride.blend(x, y, s);
|
|
3503
3639
|
return (1 - b) * (followOf(x, y, s) + ride.cut(x, y, s)) - b * drift;
|
|
3504
3640
|
};
|
|
3505
3641
|
return (x, y, ax, ay) => {
|
|
3506
3642
|
const s = Math.sign(ax);
|
|
3507
3643
|
const a = Math.abs(ax) / 30;
|
|
3508
|
-
const rx = s * a * boundOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
|
|
3644
|
+
const rx = s * a * boundOf(x, Math.min(y, capTop), s) + a * (m.shellScale - 1) * (x - f.axisX);
|
|
3509
3645
|
const ry = nodDy(NOD.hairFront, ay, f.hh);
|
|
3510
|
-
const bx = back(x, y, ax, ay)[0];
|
|
3511
3646
|
const by = nodDy(cap, ay, f.hh);
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3647
|
+
return [rx, by + crownBlend(m, y) * (ry - by)];
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
var SIDE_ROOT_BLEND = 0.2;
|
|
3651
|
+
function sideRootBlend(f, role, box) {
|
|
3652
|
+
let rootEnd = box.y1 - (box.y1 - box.y0) / 4;
|
|
3653
|
+
if (f.hairRuns(role, box.y1 - 0.5) !== void 0 && f.hairRuns("hair_front", box.y1 - 0.5) !== void 0) {
|
|
3654
|
+
const shows = (y) => {
|
|
3655
|
+
const lock = f.hairRuns(role, y);
|
|
3656
|
+
const front = f.hairRuns("hair_front", y);
|
|
3657
|
+
for (let k = 0; k < lock.length; k += 2) {
|
|
3658
|
+
for (let px = lock[k]; px < lock[k + 1]; px++) {
|
|
3659
|
+
if (!paints(front, px)) return true;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
return false;
|
|
3663
|
+
};
|
|
3664
|
+
const rows = Array.from(
|
|
3665
|
+
{ length: Math.round(box.y1 - box.y0) },
|
|
3666
|
+
(_, i2) => i2
|
|
3667
|
+
);
|
|
3668
|
+
const i = rows.findIndex((i2) => shows(box.y1 - i2 - 0.5));
|
|
3669
|
+
rootEnd = i < 0 ? box.y0 : box.y1 - i;
|
|
3670
|
+
}
|
|
3671
|
+
const end = rootEnd - SIDE_ROOT_BLEND * f.hh;
|
|
3672
|
+
return { rootEnd, weight: (y) => smoothstep(rootEnd, end, y) };
|
|
3673
|
+
}
|
|
3674
|
+
function hairSideField(m) {
|
|
3675
|
+
const front = hairFrontField(m);
|
|
3676
|
+
const face = faceField(m);
|
|
3677
|
+
const locks = ["hair_side_L", "hair_side_R"].flatMap((role) => {
|
|
3678
|
+
const box = m.boxes[role];
|
|
3679
|
+
return box === void 0 ? [] : [{ box, ...sideRootBlend(m.frame, role, box) }];
|
|
3680
|
+
});
|
|
3681
|
+
return (x, y, ax, ay) => {
|
|
3682
|
+
const lock = locks.find((l) => x >= l.box.x0 && x <= l.box.x1);
|
|
3683
|
+
if (lock === void 0) return face(x, y, ax, ay);
|
|
3684
|
+
const t = lock.weight(y);
|
|
3685
|
+
if (t === 0) return front(x, y, ax, ay);
|
|
3686
|
+
if (t === 1) return face(x, y, ax, ay);
|
|
3687
|
+
const [fx, fy] = front(x, y, ax, ay);
|
|
3688
|
+
const [gx, gy] = face(x, y, ax, ay);
|
|
3689
|
+
return [fx * (1 - t) + gx * t, fy * (1 - t) + gy * t];
|
|
3515
3690
|
};
|
|
3516
3691
|
}
|
|
3517
3692
|
function crownBlend(m, y) {
|
|
@@ -3648,17 +3823,17 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3648
3823
|
sides.forEach((side, k) => {
|
|
3649
3824
|
const edge = outerEdge(front, f.axisX, side);
|
|
3650
3825
|
if (edge === void 0) return;
|
|
3651
|
-
let
|
|
3826
|
+
let outward2 = Infinity;
|
|
3652
3827
|
let inward = Infinity;
|
|
3653
3828
|
for (let j = Math.floor(i - above); j < Math.ceil(i + 1 + below); j++) {
|
|
3654
3829
|
const c = runReach(backRows[j - j0], edge, side);
|
|
3655
|
-
|
|
3830
|
+
outward2 = Math.min(outward2, c.outward);
|
|
3656
3831
|
inward = Math.min(inward, c.inward);
|
|
3657
3832
|
}
|
|
3658
3833
|
offset[k][i] = side * (edge - f.axisX);
|
|
3659
|
-
reachOut[k][i] =
|
|
3834
|
+
reachOut[k][i] = outward2;
|
|
3660
3835
|
reachIn[k][i] = inward;
|
|
3661
|
-
if (i < capRows && runReach(backRows[i - j0], edge, side).inward > 0)
|
|
3836
|
+
if (i < capRows && y <= f.face.y1 && runReach(backRows[i - j0], edge, side).inward > 0)
|
|
3662
3837
|
bound[k][i] = 1;
|
|
3663
3838
|
});
|
|
3664
3839
|
}
|
|
@@ -3668,12 +3843,12 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3668
3843
|
const [i0, i1] = rowWindow(ys, r, count);
|
|
3669
3844
|
sides.forEach((side, k) => {
|
|
3670
3845
|
let d = Infinity;
|
|
3671
|
-
let
|
|
3846
|
+
let outward2 = Infinity;
|
|
3672
3847
|
let inward = Infinity;
|
|
3673
3848
|
for (let i = i0; i < i1; i++) {
|
|
3674
3849
|
if (Number.isNaN(offset[k][i])) continue;
|
|
3675
3850
|
d = Math.min(d, offset[k][i]);
|
|
3676
|
-
|
|
3851
|
+
outward2 = Math.min(outward2, reachOut[k][i]);
|
|
3677
3852
|
inward = Math.min(inward, reachIn[k][i]);
|
|
3678
3853
|
}
|
|
3679
3854
|
end[r * 2 + k] = d;
|
|
@@ -3682,7 +3857,7 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3682
3857
|
sides.forEach((s, j) => {
|
|
3683
3858
|
const allow = Math.min(
|
|
3684
3859
|
coreAt(x, ys[r], s),
|
|
3685
|
-
m.shellScale * (s === side ?
|
|
3860
|
+
m.shellScale * (s === side ? outward2 : inward) - drift
|
|
3686
3861
|
);
|
|
3687
3862
|
lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
|
|
3688
3863
|
});
|
|
@@ -3753,73 +3928,117 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3753
3928
|
return quarter.map((q) => scale * q);
|
|
3754
3929
|
};
|
|
3755
3930
|
const cuts = new Float64Array(n * 2 * nc);
|
|
3756
|
-
const blend = new Float64Array(n * 2);
|
|
3931
|
+
const blend = new Float64Array(n * 2 * nc);
|
|
3932
|
+
const motion = new Array(n * 2);
|
|
3933
|
+
const outward = sides.map((side) => {
|
|
3934
|
+
const cols = xs.map((_, c) => c).filter((c) => side * (xs[c] - f.axisX) >= 0);
|
|
3935
|
+
return side < 0 ? cols.reverse() : cols;
|
|
3936
|
+
});
|
|
3757
3937
|
const wr = ys.map((y) => crownBlend(m, y));
|
|
3758
3938
|
for (let r = 0; r <= last && capRows > 0; r++) {
|
|
3759
|
-
|
|
3760
|
-
if (w <= 0) continue;
|
|
3939
|
+
if (wr[r] < 1) continue;
|
|
3761
3940
|
const edges = [];
|
|
3941
|
+
const copied = r > 0 && wr[r - 1] < 1;
|
|
3762
3942
|
const [i0, i1] = rowWindow(ys, r, capRows);
|
|
3763
3943
|
for (let i = i0; i < i1; i++) {
|
|
3944
|
+
const y = top - i - 0.5;
|
|
3764
3945
|
sides.forEach((side, k) => {
|
|
3765
3946
|
if (bound[k][i] === 0) return;
|
|
3766
3947
|
const x = f.axisX + side * offset[k][i];
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3948
|
+
for (const at2 of [
|
|
3949
|
+
readAt(r, x, y),
|
|
3950
|
+
copied && y > ys[r] ? readAt(r - 1, x, y) : void 0
|
|
3951
|
+
]) {
|
|
3952
|
+
if (at2 === void 0) continue;
|
|
3953
|
+
edges.push({
|
|
3954
|
+
side,
|
|
3955
|
+
x,
|
|
3956
|
+
at: at2,
|
|
3957
|
+
outward: reachOut[k][i],
|
|
3958
|
+
inward: reachIn[k][i]
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3776
3961
|
});
|
|
3777
3962
|
}
|
|
3778
3963
|
if (edges.length === 0) continue;
|
|
3779
3964
|
sides.forEach((s, j) => {
|
|
3780
3965
|
const today = xs.map((x) => followAt(x, ys[r], s) + liftAt(x, ys[r], s));
|
|
3781
3966
|
const after = today.slice();
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
for (let c = 0; c < nc; c++) after[c] -= cut[c];
|
|
3788
|
-
}
|
|
3967
|
+
for (const side of sides) {
|
|
3968
|
+
const mine = edges.filter((e) => e.side === side);
|
|
3969
|
+
if (mine.length === 0) continue;
|
|
3970
|
+
const cut = capCut(r, s, side, today, mine);
|
|
3971
|
+
for (let c = 0; c < nc; c++) after[c] -= cut[c];
|
|
3789
3972
|
}
|
|
3790
|
-
|
|
3973
|
+
const b = blend.subarray((r * 2 + j) * nc, (r * 2 + j + 1) * nc);
|
|
3791
3974
|
for (const e of edges) {
|
|
3792
|
-
const rel = w * (along(after, e.at) + drift);
|
|
3793
3975
|
const hi = m.shellScale * ahead(e, s);
|
|
3794
3976
|
const lo = -m.shellScale * behind(e, s);
|
|
3795
|
-
|
|
3796
|
-
|
|
3977
|
+
const { c, t } = gridCol(xs, e.at);
|
|
3978
|
+
for (const [cc, weight] of [
|
|
3979
|
+
[c, 1 - t],
|
|
3980
|
+
[c + 1, t]
|
|
3981
|
+
]) {
|
|
3982
|
+
if (weight <= 0) continue;
|
|
3983
|
+
const rel = after[cc] + drift;
|
|
3984
|
+
if (rel > hi) b[cc] = Math.max(b[cc], 1 - hi / rel);
|
|
3985
|
+
else if (rel < lo) b[cc] = Math.max(b[cc], 1 - lo / rel);
|
|
3986
|
+
}
|
|
3797
3987
|
}
|
|
3798
|
-
|
|
3988
|
+
motion[r * 2 + j] = after.map((v) => v + drift);
|
|
3799
3989
|
for (let c = 0; c < nc; c++)
|
|
3800
3990
|
cuts[(r * 2 + j) * nc + c] = after[c] - today[c];
|
|
3801
3991
|
});
|
|
3802
3992
|
}
|
|
3803
|
-
for (let
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3993
|
+
for (let r = 0; r < n; r++) {
|
|
3994
|
+
sides.forEach((s, j) => {
|
|
3995
|
+
motion[r * 2 + j] ??= xs.map(
|
|
3996
|
+
(x) => followAt(x, ys[r], s) + liftAt(x, ys[r], s) + drift
|
|
3997
|
+
);
|
|
3998
|
+
});
|
|
3808
3999
|
}
|
|
4000
|
+
const at = (r, j, c) => (r * 2 + j) * nc + c;
|
|
3809
4001
|
let zero = last + 1;
|
|
3810
4002
|
while (zero < n && ys[zero] > eyeTop) zero++;
|
|
3811
4003
|
while (zero > last + 1 && zero < n && ys[zero] < irisTop - 0.5) zero--;
|
|
3812
4004
|
const endY = zero < n ? ys[zero] : Math.min(Math.max(eyeTop, irisTop - 0.5), ys[n - 1] - 1);
|
|
3813
|
-
for (let
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
blend[
|
|
3820
|
-
|
|
4005
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
4006
|
+
const was = blend.slice();
|
|
4007
|
+
for (let j = 0; j < 2; j++) {
|
|
4008
|
+
for (let r = n - 2; r >= 0; r--) {
|
|
4009
|
+
if (r > last && wr[r] >= 1) continue;
|
|
4010
|
+
for (let c = 0; c < nc; c++)
|
|
4011
|
+
blend[at(r, j, c)] = Math.max(
|
|
4012
|
+
blend[at(r, j, c)],
|
|
4013
|
+
blend[at(r + 1, j, c)]
|
|
4014
|
+
);
|
|
3821
4015
|
}
|
|
3822
4016
|
}
|
|
4017
|
+
for (let j = 0; j < 2 && last < n - 1; j++) {
|
|
4018
|
+
for (let r = 1; r < zero; r++) {
|
|
4019
|
+
for (let q = 0; q < r && q <= last; q++) {
|
|
4020
|
+
if (wr[q] < 1) continue;
|
|
4021
|
+
const k = (ys[r] - endY) / (ys[q] - endY);
|
|
4022
|
+
for (let c = 0; c < nc; c++)
|
|
4023
|
+
blend[at(r, j, c)] = Math.max(
|
|
4024
|
+
blend[at(r, j, c)],
|
|
4025
|
+
blend[at(q, j, c)] * k
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
for (let r = 0; r < n; r++) {
|
|
4031
|
+
sides.forEach(
|
|
4032
|
+
(s, j) => spreadBlend(
|
|
4033
|
+
blend.subarray(at(r, j, 0), at(r, j, nc)),
|
|
4034
|
+
motion[r * 2 + j],
|
|
4035
|
+
xs,
|
|
4036
|
+
outward,
|
|
4037
|
+
s
|
|
4038
|
+
)
|
|
4039
|
+
);
|
|
4040
|
+
}
|
|
4041
|
+
if (blend.every((v, i) => v === was[i])) break;
|
|
3823
4042
|
}
|
|
3824
4043
|
const cutRow = (r, j, x) => r > last ? 0 : along(cuts.subarray((r * 2 + j) * nc, (r * 2 + j + 1) * nc), x);
|
|
3825
4044
|
return {
|
|
@@ -3832,158 +4051,47 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3832
4051
|
const { r, t } = gridRow(ys, y, true);
|
|
3833
4052
|
return cutRow(r, j, x) * (1 - t) + cutRow(r + 1, j, x) * t;
|
|
3834
4053
|
},
|
|
3835
|
-
blend: (y, s) => {
|
|
4054
|
+
blend: (x, y, s) => {
|
|
3836
4055
|
if (s === 0) return 0;
|
|
3837
4056
|
const j = s < 0 ? 0 : 1;
|
|
3838
4057
|
const { r, t } = gridRow(ys, y, true);
|
|
3839
|
-
|
|
4058
|
+
const row = (q) => along(blend.subarray(at(q, j, 0), at(q, j, nc)), x);
|
|
4059
|
+
return row(r) * (1 - t) + row(r + 1) * t;
|
|
3840
4060
|
}
|
|
3841
4061
|
};
|
|
3842
4062
|
}
|
|
3843
|
-
function
|
|
3844
|
-
const
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
const backAt = (j) => {
|
|
3859
|
-
let runs = backRows.get(j);
|
|
3860
|
-
if (runs === void 0) {
|
|
3861
|
-
runs = f.hairRuns("hair_back", top - j - 0.5);
|
|
3862
|
-
backRows.set(j, runs);
|
|
3863
|
-
}
|
|
3864
|
-
return runs;
|
|
3865
|
-
};
|
|
3866
|
-
const faceAt = (j) => {
|
|
3867
|
-
const y = top - j - 0.5;
|
|
3868
|
-
return f.faceRuns(y) ?? (y > f.face.y0 && y < f.face.y1 ? [f.face.x0, f.face.x1] : []);
|
|
3869
|
-
};
|
|
3870
|
-
const passed = (i, dys) => [
|
|
3871
|
-
Math.floor(i - Math.max(0, ...dys)),
|
|
3872
|
-
Math.ceil(i + 1 - Math.min(0, ...dys))
|
|
3873
|
-
];
|
|
3874
|
-
const rel = new Float64Array(n * xs.length * 2).fill(NaN);
|
|
3875
|
-
const relOf = (r, c, k) => {
|
|
3876
|
-
const at = (r * xs.length + c) * 2 + k;
|
|
3877
|
-
if (Number.isNaN(rel[at])) rel[at] = relAt(xs[c], ys[r], k === 0 ? -1 : 1);
|
|
3878
|
-
return rel[at];
|
|
3879
|
-
};
|
|
3880
|
-
const cellR = (x, y, k) => {
|
|
3881
|
-
const { c } = gridCol(xs, x);
|
|
3882
|
-
const { r } = gridRow(ys, y);
|
|
3883
|
-
return Math.max(
|
|
3884
|
-
relOf(r, c, k),
|
|
3885
|
-
relOf(r, c + 1, k),
|
|
3886
|
-
relOf(r + 1, c, k),
|
|
3887
|
-
relOf(r + 1, c + 1, k)
|
|
3888
|
-
);
|
|
3889
|
-
};
|
|
3890
|
-
const someAngle = (pairs) => {
|
|
3891
|
-
let lo = 0;
|
|
3892
|
-
let hi = 1;
|
|
3893
|
-
for (const [c0, c1] of pairs) {
|
|
3894
|
-
if (c1 === 0) {
|
|
3895
|
-
if (c0 <= 0) return false;
|
|
3896
|
-
} else if (c1 > 0) lo = Math.max(lo, -c0 / c1);
|
|
3897
|
-
else hi = Math.min(hi, -c0 / c1);
|
|
3898
|
-
}
|
|
3899
|
-
return lo < hi;
|
|
4063
|
+
function spreadBlend(b, a, xs, outward, s) {
|
|
4064
|
+
const fit = (ci, co, g) => {
|
|
4065
|
+
const quarter = Math.abs(xs[co] - xs[ci]) / 4;
|
|
4066
|
+
const today = g * (a[co] - a[ci]);
|
|
4067
|
+
const [lo, hi] = [
|
|
4068
|
+
Math.min(today, 0) - quarter,
|
|
4069
|
+
Math.max(today, 0) + quarter
|
|
4070
|
+
];
|
|
4071
|
+
const ga = g * a[ci];
|
|
4072
|
+
if (ga === 0) return;
|
|
4073
|
+
const outer = g * (1 - b[co]) * a[co];
|
|
4074
|
+
const [b0, b1] = [1 - (outer - hi) / ga, 1 - (outer - lo) / ga];
|
|
4075
|
+
const least = Math.max(b[ci], Math.min(b0, b1));
|
|
4076
|
+
if (least <= Math.min(1, Math.max(b0, b1))) b[ci] = least;
|
|
4077
|
+
else if (Math.min(b0, b1) > 1) b[ci] = 1;
|
|
3900
4078
|
};
|
|
3901
|
-
const
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
const [b0, b1] = passed(
|
|
3912
|
-
i,
|
|
3913
|
-
ays.map((ay) => nodDy(nod, ay, hh) - nodDy(NOD.hairBack, ay, hh))
|
|
3914
|
-
);
|
|
3915
|
-
const band = [];
|
|
3916
|
-
for (let j = b0; j < b1; j++) band.push(backAt(j));
|
|
3917
|
-
const faces = [];
|
|
3918
|
-
const [f0, f1] = passed(
|
|
3919
|
-
i,
|
|
3920
|
-
ays.map((ay) => nodDy(nod, ay, hh) - face(f.axisX, y, 0, ay)[1])
|
|
3921
|
-
);
|
|
3922
|
-
for (let j = f0; j < f1; j++)
|
|
3923
|
-
faces.push({ y: top - j - 0.5, runs: faceAt(j) });
|
|
3924
|
-
const front = f.hairRuns("hair_front", y);
|
|
3925
|
-
[-1, 1].forEach((s, side) => {
|
|
3926
|
-
let least = Infinity;
|
|
3927
|
-
const far = outerEdge(front, f.axisX, s);
|
|
3928
|
-
if (far !== void 0) {
|
|
3929
|
-
const R = cellR(far, y, side);
|
|
3930
|
-
const c = Math.min(...band.map((b) => runReach(b, far, s).outward));
|
|
3931
|
-
if (R > 0) least = Math.min(least, m.shellScale * c / R);
|
|
3932
|
-
}
|
|
3933
|
-
for (let t = s > 0 ? 0 : 1; t < front.length; t += 2) {
|
|
3934
|
-
const x = front[t];
|
|
3935
|
-
const R = cellR(x, y, side);
|
|
3936
|
-
if (R <= 0) continue;
|
|
3937
|
-
let c = Math.min(
|
|
3938
|
-
...band.map((b) => runReach(b, x, s > 0 ? -1 : 1).inward)
|
|
3939
|
-
);
|
|
3940
|
-
const sh = (m.shellScale - 1) * (x - f.axisX);
|
|
3941
|
-
const d0 = sh + s * m.hairBack;
|
|
3942
|
-
const d1 = d0 + s * R;
|
|
3943
|
-
const lo = Math.min(d0, d1);
|
|
3944
|
-
const hi = Math.max(d0, d1);
|
|
3945
|
-
const bares = faces.some(({ y: fy, runs }) => {
|
|
3946
|
-
for (let r = 0; r < runs.length; r += 2) {
|
|
3947
|
-
const e0 = runs[r];
|
|
3948
|
-
const e1 = runs[r + 1];
|
|
3949
|
-
const de0 = face(e0, fy, 30 * s, 0)[0];
|
|
3950
|
-
const de1 = face(e1, fy, 30 * s, 0)[0];
|
|
3951
|
-
if (someAngle([
|
|
3952
|
-
[e1 - x, de1 - lo],
|
|
3953
|
-
[x - e0, hi - de0]
|
|
3954
|
-
])) {
|
|
3955
|
-
return true;
|
|
3956
|
-
}
|
|
3957
|
-
}
|
|
3958
|
-
return false;
|
|
3959
|
-
});
|
|
3960
|
-
if (bares) c = 0;
|
|
3961
|
-
least = Math.min(least, m.shellScale * c / R);
|
|
3962
|
-
}
|
|
3963
|
-
slack[side][i] = least - w;
|
|
4079
|
+
for (const cols of outward) {
|
|
4080
|
+
for (let i = 1; i < cols.length; i++)
|
|
4081
|
+
b[cols[i]] = Math.max(b[cols[i]], b[cols[i - 1]]);
|
|
4082
|
+
}
|
|
4083
|
+
const [left, right] = outward.map((cols) => cols[0]);
|
|
4084
|
+
const across = left !== void 0 && right !== void 0 && left !== right;
|
|
4085
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
4086
|
+
outward.forEach((cols, k) => {
|
|
4087
|
+
const g = (k === 0 ? -1 : 1) * s;
|
|
4088
|
+
for (let i = cols.length - 1; i > 0; i--) fit(cols[i - 1], cols[i], g);
|
|
3964
4089
|
});
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
const out = wr.slice();
|
|
3969
|
-
for (let r = 0; r < n; r++) {
|
|
3970
|
-
if (wr[r] >= 1) continue;
|
|
3971
|
-
const [i0, i1] = rowWindow(ys, r, count);
|
|
3972
|
-
let least = Infinity;
|
|
3973
|
-
for (let i = i0; i < i1; i++) least = Math.min(least, slackAt(i, k));
|
|
3974
|
-
out[r] = wr[r] + clamp(least, 0, 1 - wr[r]);
|
|
4090
|
+
if (across) {
|
|
4091
|
+
if (b[left] <= b[right]) fit(left, right, s);
|
|
4092
|
+
else fit(right, left, -s);
|
|
3975
4093
|
}
|
|
3976
|
-
|
|
3977
|
-
if (wr[r] < 1) out[r] = Math.max(wr[r], Math.min(out[r], out[r + 1]));
|
|
3978
|
-
}
|
|
3979
|
-
return out.map((v, r) => v - wr[r]);
|
|
3980
|
-
});
|
|
3981
|
-
return (y, s) => {
|
|
3982
|
-
if (s === 0) return 0;
|
|
3983
|
-
const e = extra[s < 0 ? 0 : 1];
|
|
3984
|
-
const { r, t } = gridRow(ys, y, true);
|
|
3985
|
-
return e[r] * (1 - t) + e[r + 1] * t;
|
|
3986
|
-
};
|
|
4094
|
+
}
|
|
3987
4095
|
}
|
|
3988
4096
|
function familyField(m, family) {
|
|
3989
4097
|
switch (family) {
|
|
@@ -4001,6 +4109,8 @@ function familyField(m, family) {
|
|
|
4001
4109
|
return mouthField(m);
|
|
4002
4110
|
case "hair_front":
|
|
4003
4111
|
return hairFrontField(m);
|
|
4112
|
+
case "hair_side":
|
|
4113
|
+
return hairSideField(m);
|
|
4004
4114
|
case "hair_back":
|
|
4005
4115
|
return hairField(m, m.hairBack, NOD.hairBack);
|
|
4006
4116
|
case "body":
|
|
@@ -4157,16 +4267,16 @@ function strandOverlaps(ctx, land2, holdBase) {
|
|
|
4157
4267
|
if (px <= 0) continue;
|
|
4158
4268
|
out[key] = {
|
|
4159
4269
|
deg,
|
|
4160
|
-
px:
|
|
4161
|
-
hh:
|
|
4162
|
-
restPx:
|
|
4270
|
+
px: round32(px),
|
|
4271
|
+
hh: round32(px) / holdBase,
|
|
4272
|
+
restPx: round32(restPx),
|
|
4163
4273
|
// A fringe spanning the face has no side to hold the iris clear of.
|
|
4164
4274
|
held: strand.runFace !== null && px <= restPx + STRAND_SLACK
|
|
4165
4275
|
};
|
|
4166
4276
|
}
|
|
4167
4277
|
return out.left || out.right ? out : void 0;
|
|
4168
4278
|
}
|
|
4169
|
-
var
|
|
4279
|
+
var round32 = (v) => Math.round(v * 1e3) / 1e3;
|
|
4170
4280
|
var STRAND_SLACK = 0.5;
|
|
4171
4281
|
function bisect(f, lo, hi, it = 50) {
|
|
4172
4282
|
for (let i = 0; i < it; i++) {
|
|
@@ -4381,7 +4491,7 @@ function turnReport(ctx, fit, rendered) {
|
|
|
4381
4491
|
const plate = depth(m.face);
|
|
4382
4492
|
const w = frame.paintedHalfAt(frame.eyeY);
|
|
4383
4493
|
const report = {
|
|
4384
|
-
radius:
|
|
4494
|
+
radius: round32(w * w / (2 * Math.max(1e-3, eye - plate))),
|
|
4385
4495
|
holdBase: hb,
|
|
4386
4496
|
depths: {
|
|
4387
4497
|
eye: round4(eye / hb),
|
|
@@ -4397,6 +4507,8 @@ function turnReport(ctx, fit, rendered) {
|
|
|
4397
4507
|
};
|
|
4398
4508
|
const overlap = strandOverlaps(ctx, rendered, hb);
|
|
4399
4509
|
if (overlap !== void 0) report.strandOverlap = overlap;
|
|
4510
|
+
const hairSide = hairSideRoom(ctx, rendered, m.boxes);
|
|
4511
|
+
if (hairSide !== void 0) report.hairSide = hairSide;
|
|
4400
4512
|
return report;
|
|
4401
4513
|
}
|
|
4402
4514
|
var round4 = (v) => Math.round(v * 1e4) / 1e4;
|
|
@@ -4552,7 +4664,7 @@ var RIG_PLAN_FIELDS = [
|
|
|
4552
4664
|
fitBy: null,
|
|
4553
4665
|
provisional: false,
|
|
4554
4666
|
migratedFrom: "outlineFollow",
|
|
4555
|
-
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
|
|
4667
|
+
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)"
|
|
4556
4668
|
},
|
|
4557
4669
|
{
|
|
4558
4670
|
path: "hair.sway",
|
|
@@ -5606,6 +5718,7 @@ function checkLayers(layers, canvas) {
|
|
|
5606
5718
|
}
|
|
5607
5719
|
checkArmsHaveBody(seen);
|
|
5608
5720
|
checkPosesHaveArms(seen);
|
|
5721
|
+
checkSidesHaveFront(seen);
|
|
5609
5722
|
}
|
|
5610
5723
|
function checkHeadEdges(edges, byRole) {
|
|
5611
5724
|
if (edges === void 0) return;
|
|
@@ -6330,6 +6443,10 @@ var MESH_CELLS = {
|
|
|
6330
6443
|
face: { px: 28, min: 4, max: 24 },
|
|
6331
6444
|
// The front hair eases from riding the face to holding the outline.
|
|
6332
6445
|
hair_front: { px: 32, min: 3, max: 24 },
|
|
6446
|
+
// A side lock's rows come from `sideLockGrid`: even ones, plus the root
|
|
6447
|
+
// band's (`sideRootBlend`), which a long lock's coarse cells would straddle.
|
|
6448
|
+
hair_side_L: { px: 20, min: 3, max: 24 },
|
|
6449
|
+
hair_side_R: { px: 20, min: 3, max: 24 },
|
|
6333
6450
|
hair_back: { px: 56, min: 3, max: 18 }
|
|
6334
6451
|
};
|
|
6335
6452
|
var NECK_CELLS = { px: 20, min: 3, max: 12 };
|
|
@@ -6381,7 +6498,18 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
6381
6498
|
const solveFrame = buildHeadFrame(layers, frameOptions);
|
|
6382
6499
|
const ctx = solveContext(solveFrame, byRole, turnOptions, hasNose);
|
|
6383
6500
|
const boxes = Object.fromEntries(
|
|
6384
|
-
[
|
|
6501
|
+
[
|
|
6502
|
+
"eye_L",
|
|
6503
|
+
"eye_R",
|
|
6504
|
+
"iris_L",
|
|
6505
|
+
"iris_R",
|
|
6506
|
+
"brow_L",
|
|
6507
|
+
"brow_R",
|
|
6508
|
+
"hair_side_L",
|
|
6509
|
+
"hair_side_R",
|
|
6510
|
+
"lash_L",
|
|
6511
|
+
"lash_R"
|
|
6512
|
+
].filter(has).map((r) => [r, box(r)])
|
|
6385
6513
|
);
|
|
6386
6514
|
const landmarks = {
|
|
6387
6515
|
boxes,
|
|
@@ -6586,6 +6714,8 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
|
|
|
6586
6714
|
headStart = fm.headStart;
|
|
6587
6715
|
} else if (spec.role === "hair_front") {
|
|
6588
6716
|
mesh = columnMesh(b, frontGrid);
|
|
6717
|
+
} else if (spec.role === "hair_side_L" || spec.role === "hair_side_R") {
|
|
6718
|
+
mesh = columnMesh(b, sideLockGrid(b, frame, spec.role));
|
|
6589
6719
|
} else if (lips !== void 0 && (isLipRole(spec.role) || isLipInsideRole(spec.role))) {
|
|
6590
6720
|
({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
|
|
6591
6721
|
} else if (spec.family !== "body") {
|
|
@@ -6600,6 +6730,7 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
|
|
|
6600
6730
|
const side = spec.role.endsWith("_L") ? "L" : "R";
|
|
6601
6731
|
const eyeOpen = side === "L" ? import_format12.StandardParameter.EyeOpenLeft : import_format12.StandardParameter.EyeOpenRight;
|
|
6602
6732
|
const hh = frame.hh;
|
|
6733
|
+
const swayAmp = plan.hair.sway * plan.profile.AMPLITUDE.sway * hh;
|
|
6603
6734
|
switch (spec.role) {
|
|
6604
6735
|
case "face":
|
|
6605
6736
|
if (region?.some((r) => r === "neck")) {
|
|
@@ -6736,18 +6867,30 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
|
|
|
6736
6867
|
case "hair_front":
|
|
6737
6868
|
case "hair_back":
|
|
6738
6869
|
if (byRole.has("hair_front")) {
|
|
6739
|
-
const amp = plan.hair.sway * plan.profile.AMPLITUDE.sway * hh;
|
|
6740
6870
|
if (spec.role === "hair_front") {
|
|
6741
6871
|
warps.push(
|
|
6742
|
-
hairSway(import_format12.StandardParameter.HairSwayX, mesh, b,
|
|
6743
|
-
hairSway(import_format12.StandardParameter.HairSwayZ, mesh, b,
|
|
6872
|
+
hairSway(import_format12.StandardParameter.HairSwayX, mesh, b, swayAmp),
|
|
6873
|
+
hairSway(import_format12.StandardParameter.HairSwayZ, mesh, b, swayAmp)
|
|
6744
6874
|
);
|
|
6745
6875
|
} else {
|
|
6746
|
-
warps.push(hairSway(import_format12.StandardParameter.HairBackSway, mesh, b,
|
|
6876
|
+
warps.push(hairSway(import_format12.StandardParameter.HairBackSway, mesh, b, swayAmp));
|
|
6747
6877
|
}
|
|
6748
6878
|
}
|
|
6749
6879
|
warps.push(tiltHang(mesh, b, pivot, hairHangWeight(frame.chinY, b.y0)));
|
|
6750
6880
|
break;
|
|
6881
|
+
case "hair_side_L":
|
|
6882
|
+
case "hair_side_R": {
|
|
6883
|
+
const attach = {
|
|
6884
|
+
box: box("hair_front"),
|
|
6885
|
+
weight: sideRootBlend(frame, spec.role, b).weight
|
|
6886
|
+
};
|
|
6887
|
+
warps.push(
|
|
6888
|
+
lockSway(import_format12.StandardParameter.HairSwayX, mesh, b, swayAmp, attach),
|
|
6889
|
+
lockSway(import_format12.StandardParameter.HairSwayZ, mesh, b, swayAmp, attach),
|
|
6890
|
+
tiltHang(mesh, b, pivot, lockHangWeight(frame.chinY, b.y0, hh))
|
|
6891
|
+
);
|
|
6892
|
+
break;
|
|
6893
|
+
}
|
|
6751
6894
|
}
|
|
6752
6895
|
if (warps.length > 0) part.warps = warps;
|
|
6753
6896
|
if (bindings.length > 0) part.bindings = bindings;
|
|
@@ -6804,9 +6947,34 @@ function hairFrontGrid(b, frame, boxes) {
|
|
|
6804
6947
|
}
|
|
6805
6948
|
return { xs, ys };
|
|
6806
6949
|
}
|
|
6950
|
+
var BLEND_ROWS = 3;
|
|
6951
|
+
function sideLockGrid(b, frame, role) {
|
|
6952
|
+
const cells = MESH_CELLS[role];
|
|
6953
|
+
const px = Math.min(cells.px, meshScale(role, frame));
|
|
6954
|
+
const cols = cellsFor(bw(b), px, cells.min, cells.max);
|
|
6955
|
+
const rows = cellsFor(bh(b), px, cells.min, cells.max);
|
|
6956
|
+
const xs = Array.from(
|
|
6957
|
+
{ length: cols + 1 },
|
|
6958
|
+
(_, c) => b.x0 + bw(b) * c / cols
|
|
6959
|
+
);
|
|
6960
|
+
const ys = Array.from(
|
|
6961
|
+
{ length: rows + 1 },
|
|
6962
|
+
(_, r) => b.y1 - bh(b) * r / rows
|
|
6963
|
+
);
|
|
6964
|
+
const { rootEnd } = sideRootBlend(frame, role, b);
|
|
6965
|
+
const end = rootEnd - SIDE_ROOT_BLEND * frame.hh;
|
|
6966
|
+
for (let i = 0; i <= BLEND_ROWS + 1; i++) {
|
|
6967
|
+
const y = rootEnd + (end - rootEnd) * i / (BLEND_ROWS + 1);
|
|
6968
|
+
if (y > b.y0 + 0.25 && y < b.y1 - 0.25 && ys.every((v) => Math.abs(v - y) > 0.25))
|
|
6969
|
+
ys.push(y);
|
|
6970
|
+
}
|
|
6971
|
+
return { xs, ys: ys.sort((p, q) => q - p) };
|
|
6972
|
+
}
|
|
6807
6973
|
function meshScale(role, frame) {
|
|
6808
6974
|
switch (role) {
|
|
6809
6975
|
case "hair_front":
|
|
6976
|
+
case "hair_side_L":
|
|
6977
|
+
case "hair_side_R":
|
|
6810
6978
|
case "hair_back":
|
|
6811
6979
|
return Math.max(2, (frame.shellRight - frame.shellLeft) / 9);
|
|
6812
6980
|
default:
|