@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.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_SPAN = 1;
|
|
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(pivotY, bottom, hh) {
|
|
2409
|
+
return hairHangWeight(pivotY, Math.min(bottom, pivotY - LOCK_HANG_SPAN * hh));
|
|
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) {
|
|
@@ -3373,12 +3510,12 @@ function hairFrontBends(frame, boxes) {
|
|
|
3373
3510
|
}
|
|
3374
3511
|
function hairFrontField(m) {
|
|
3375
3512
|
const f = m.frame;
|
|
3376
|
-
const back = hairField(m, m.hairBack, NOD.hairBack);
|
|
3377
3513
|
const cap = { up: NOD.hairFrontTop.up, down: capSlide(m) };
|
|
3378
3514
|
const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
|
|
3379
3515
|
(e) => e !== void 0
|
|
3380
3516
|
);
|
|
3381
3517
|
const { inner, left, right } = hairFrontBends(f, m.boxes);
|
|
3518
|
+
const capTop = m.hairFrontGrid?.ys.find((y) => y <= f.face.y1) ?? f.face.y1;
|
|
3382
3519
|
const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
|
|
3383
3520
|
const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
|
|
3384
3521
|
const coreAt = (x, y, s) => {
|
|
@@ -3411,22 +3548,60 @@ function hairFrontField(m) {
|
|
|
3411
3548
|
);
|
|
3412
3549
|
const followOf = (x, y, s) => followAt(x, y, s) + (ride === void 0 ? 0 : ride.lift(x, y, s));
|
|
3413
3550
|
const drift = Math.abs(m.hairBack);
|
|
3414
|
-
const crown = crownRide(m, cap, (x, y, s) => followOf(x, y, s) + drift);
|
|
3415
3551
|
const boundOf = (x, y, s) => {
|
|
3416
3552
|
if (ride === void 0) return followOf(x, y, s);
|
|
3417
|
-
const b = ride.blend(y, s);
|
|
3553
|
+
const b = ride.blend(x, y, s);
|
|
3418
3554
|
return (1 - b) * (followOf(x, y, s) + ride.cut(x, y, s)) - b * drift;
|
|
3419
3555
|
};
|
|
3420
3556
|
return (x, y, ax, ay) => {
|
|
3421
3557
|
const s = Math.sign(ax);
|
|
3422
3558
|
const a = Math.abs(ax) / 30;
|
|
3423
|
-
const rx = s * a * boundOf(x, y, s) + a * (m.shellScale - 1) * (x - f.axisX);
|
|
3559
|
+
const rx = s * a * boundOf(x, Math.min(y, capTop), s) + a * (m.shellScale - 1) * (x - f.axisX);
|
|
3424
3560
|
const ry = nodDy(NOD.hairFront, ay, f.hh);
|
|
3425
|
-
const bx = back(x, y, ax, ay)[0];
|
|
3426
3561
|
const by = nodDy(cap, ay, f.hh);
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3562
|
+
return [rx, by + crownBlend(m, y) * (ry - by)];
|
|
3563
|
+
};
|
|
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];
|
|
3430
3605
|
};
|
|
3431
3606
|
}
|
|
3432
3607
|
function crownBlend(m, y) {
|
|
@@ -3563,17 +3738,17 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3563
3738
|
sides.forEach((side, k) => {
|
|
3564
3739
|
const edge = outerEdge(front, f.axisX, side);
|
|
3565
3740
|
if (edge === void 0) return;
|
|
3566
|
-
let
|
|
3741
|
+
let outward2 = Infinity;
|
|
3567
3742
|
let inward = Infinity;
|
|
3568
3743
|
for (let j = Math.floor(i - above); j < Math.ceil(i + 1 + below); j++) {
|
|
3569
3744
|
const c = runReach(backRows[j - j0], edge, side);
|
|
3570
|
-
|
|
3745
|
+
outward2 = Math.min(outward2, c.outward);
|
|
3571
3746
|
inward = Math.min(inward, c.inward);
|
|
3572
3747
|
}
|
|
3573
3748
|
offset[k][i] = side * (edge - f.axisX);
|
|
3574
|
-
reachOut[k][i] =
|
|
3749
|
+
reachOut[k][i] = outward2;
|
|
3575
3750
|
reachIn[k][i] = inward;
|
|
3576
|
-
if (i < capRows && runReach(backRows[i - j0], edge, side).inward > 0)
|
|
3751
|
+
if (i < capRows && y <= f.face.y1 && runReach(backRows[i - j0], edge, side).inward > 0)
|
|
3577
3752
|
bound[k][i] = 1;
|
|
3578
3753
|
});
|
|
3579
3754
|
}
|
|
@@ -3583,12 +3758,12 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3583
3758
|
const [i0, i1] = rowWindow(ys, r, count);
|
|
3584
3759
|
sides.forEach((side, k) => {
|
|
3585
3760
|
let d = Infinity;
|
|
3586
|
-
let
|
|
3761
|
+
let outward2 = Infinity;
|
|
3587
3762
|
let inward = Infinity;
|
|
3588
3763
|
for (let i = i0; i < i1; i++) {
|
|
3589
3764
|
if (Number.isNaN(offset[k][i])) continue;
|
|
3590
3765
|
d = Math.min(d, offset[k][i]);
|
|
3591
|
-
|
|
3766
|
+
outward2 = Math.min(outward2, reachOut[k][i]);
|
|
3592
3767
|
inward = Math.min(inward, reachIn[k][i]);
|
|
3593
3768
|
}
|
|
3594
3769
|
end[r * 2 + k] = d;
|
|
@@ -3597,7 +3772,7 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3597
3772
|
sides.forEach((s, j) => {
|
|
3598
3773
|
const allow = Math.min(
|
|
3599
3774
|
coreAt(x, ys[r], s),
|
|
3600
|
-
m.shellScale * (s === side ?
|
|
3775
|
+
m.shellScale * (s === side ? outward2 : inward) - drift
|
|
3601
3776
|
);
|
|
3602
3777
|
lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
|
|
3603
3778
|
});
|
|
@@ -3668,73 +3843,117 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3668
3843
|
return quarter.map((q) => scale * q);
|
|
3669
3844
|
};
|
|
3670
3845
|
const cuts = new Float64Array(n * 2 * nc);
|
|
3671
|
-
const blend = new Float64Array(n * 2);
|
|
3846
|
+
const blend = new Float64Array(n * 2 * nc);
|
|
3847
|
+
const motion = new Array(n * 2);
|
|
3848
|
+
const outward = sides.map((side) => {
|
|
3849
|
+
const cols = xs.map((_, c) => c).filter((c) => side * (xs[c] - f.axisX) >= 0);
|
|
3850
|
+
return side < 0 ? cols.reverse() : cols;
|
|
3851
|
+
});
|
|
3672
3852
|
const wr = ys.map((y) => crownBlend(m, y));
|
|
3673
3853
|
for (let r = 0; r <= last && capRows > 0; r++) {
|
|
3674
|
-
|
|
3675
|
-
if (w <= 0) continue;
|
|
3854
|
+
if (wr[r] < 1) continue;
|
|
3676
3855
|
const edges = [];
|
|
3856
|
+
const copied = r > 0 && wr[r - 1] < 1;
|
|
3677
3857
|
const [i0, i1] = rowWindow(ys, r, capRows);
|
|
3678
3858
|
for (let i = i0; i < i1; i++) {
|
|
3859
|
+
const y = top - i - 0.5;
|
|
3679
3860
|
sides.forEach((side, k) => {
|
|
3680
3861
|
if (bound[k][i] === 0) return;
|
|
3681
3862
|
const x = f.axisX + side * offset[k][i];
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3863
|
+
for (const at2 of [
|
|
3864
|
+
readAt(r, x, y),
|
|
3865
|
+
copied && y > ys[r] ? readAt(r - 1, x, y) : void 0
|
|
3866
|
+
]) {
|
|
3867
|
+
if (at2 === void 0) continue;
|
|
3868
|
+
edges.push({
|
|
3869
|
+
side,
|
|
3870
|
+
x,
|
|
3871
|
+
at: at2,
|
|
3872
|
+
outward: reachOut[k][i],
|
|
3873
|
+
inward: reachIn[k][i]
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3691
3876
|
});
|
|
3692
3877
|
}
|
|
3693
3878
|
if (edges.length === 0) continue;
|
|
3694
3879
|
sides.forEach((s, j) => {
|
|
3695
3880
|
const today = xs.map((x) => followAt(x, ys[r], s) + liftAt(x, ys[r], s));
|
|
3696
3881
|
const after = today.slice();
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
for (let c = 0; c < nc; c++) after[c] -= cut[c];
|
|
3703
|
-
}
|
|
3882
|
+
for (const side of sides) {
|
|
3883
|
+
const mine = edges.filter((e) => e.side === side);
|
|
3884
|
+
if (mine.length === 0) continue;
|
|
3885
|
+
const cut = capCut(r, s, side, today, mine);
|
|
3886
|
+
for (let c = 0; c < nc; c++) after[c] -= cut[c];
|
|
3704
3887
|
}
|
|
3705
|
-
|
|
3888
|
+
const b = blend.subarray((r * 2 + j) * nc, (r * 2 + j + 1) * nc);
|
|
3706
3889
|
for (const e of edges) {
|
|
3707
|
-
const rel = w * (along(after, e.at) + drift);
|
|
3708
3890
|
const hi = m.shellScale * ahead(e, s);
|
|
3709
3891
|
const lo = -m.shellScale * behind(e, s);
|
|
3710
|
-
|
|
3711
|
-
|
|
3892
|
+
const { c, t } = gridCol(xs, e.at);
|
|
3893
|
+
for (const [cc, weight] of [
|
|
3894
|
+
[c, 1 - t],
|
|
3895
|
+
[c + 1, t]
|
|
3896
|
+
]) {
|
|
3897
|
+
if (weight <= 0) continue;
|
|
3898
|
+
const rel = after[cc] + drift;
|
|
3899
|
+
if (rel > hi) b[cc] = Math.max(b[cc], 1 - hi / rel);
|
|
3900
|
+
else if (rel < lo) b[cc] = Math.max(b[cc], 1 - lo / rel);
|
|
3901
|
+
}
|
|
3712
3902
|
}
|
|
3713
|
-
|
|
3903
|
+
motion[r * 2 + j] = after.map((v) => v + drift);
|
|
3714
3904
|
for (let c = 0; c < nc; c++)
|
|
3715
3905
|
cuts[(r * 2 + j) * nc + c] = after[c] - today[c];
|
|
3716
3906
|
});
|
|
3717
3907
|
}
|
|
3718
|
-
for (let
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3908
|
+
for (let r = 0; r < n; r++) {
|
|
3909
|
+
sides.forEach((s, j) => {
|
|
3910
|
+
motion[r * 2 + j] ??= xs.map(
|
|
3911
|
+
(x) => followAt(x, ys[r], s) + liftAt(x, ys[r], s) + drift
|
|
3912
|
+
);
|
|
3913
|
+
});
|
|
3723
3914
|
}
|
|
3915
|
+
const at = (r, j, c) => (r * 2 + j) * nc + c;
|
|
3724
3916
|
let zero = last + 1;
|
|
3725
3917
|
while (zero < n && ys[zero] > eyeTop) zero++;
|
|
3726
3918
|
while (zero > last + 1 && zero < n && ys[zero] < irisTop - 0.5) zero--;
|
|
3727
3919
|
const endY = zero < n ? ys[zero] : Math.min(Math.max(eyeTop, irisTop - 0.5), ys[n - 1] - 1);
|
|
3728
|
-
for (let
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
blend[
|
|
3735
|
-
|
|
3920
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
3921
|
+
const was = blend.slice();
|
|
3922
|
+
for (let j = 0; j < 2; j++) {
|
|
3923
|
+
for (let r = n - 2; r >= 0; r--) {
|
|
3924
|
+
if (r > last && wr[r] >= 1) continue;
|
|
3925
|
+
for (let c = 0; c < nc; c++)
|
|
3926
|
+
blend[at(r, j, c)] = Math.max(
|
|
3927
|
+
blend[at(r, j, c)],
|
|
3928
|
+
blend[at(r + 1, j, c)]
|
|
3929
|
+
);
|
|
3736
3930
|
}
|
|
3737
3931
|
}
|
|
3932
|
+
for (let j = 0; j < 2 && last < n - 1; j++) {
|
|
3933
|
+
for (let r = 1; r < zero; r++) {
|
|
3934
|
+
for (let q = 0; q < r && q <= last; q++) {
|
|
3935
|
+
if (wr[q] < 1) continue;
|
|
3936
|
+
const k = (ys[r] - endY) / (ys[q] - endY);
|
|
3937
|
+
for (let c = 0; c < nc; c++)
|
|
3938
|
+
blend[at(r, j, c)] = Math.max(
|
|
3939
|
+
blend[at(r, j, c)],
|
|
3940
|
+
blend[at(q, j, c)] * k
|
|
3941
|
+
);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
for (let r = 0; r < n; r++) {
|
|
3946
|
+
sides.forEach(
|
|
3947
|
+
(s, j) => spreadBlend(
|
|
3948
|
+
blend.subarray(at(r, j, 0), at(r, j, nc)),
|
|
3949
|
+
motion[r * 2 + j],
|
|
3950
|
+
xs,
|
|
3951
|
+
outward,
|
|
3952
|
+
s
|
|
3953
|
+
)
|
|
3954
|
+
);
|
|
3955
|
+
}
|
|
3956
|
+
if (blend.every((v, i) => v === was[i])) break;
|
|
3738
3957
|
}
|
|
3739
3958
|
const cutRow = (r, j, x) => r > last ? 0 : along(cuts.subarray((r * 2 + j) * nc, (r * 2 + j + 1) * nc), x);
|
|
3740
3959
|
return {
|
|
@@ -3747,158 +3966,47 @@ function outerRide(m, inner, coreAt, followAt, capLimit, eyeTop, irisTop) {
|
|
|
3747
3966
|
const { r, t } = gridRow(ys, y, true);
|
|
3748
3967
|
return cutRow(r, j, x) * (1 - t) + cutRow(r + 1, j, x) * t;
|
|
3749
3968
|
},
|
|
3750
|
-
blend: (y, s) => {
|
|
3969
|
+
blend: (x, y, s) => {
|
|
3751
3970
|
if (s === 0) return 0;
|
|
3752
3971
|
const j = s < 0 ? 0 : 1;
|
|
3753
3972
|
const { r, t } = gridRow(ys, y, true);
|
|
3754
|
-
|
|
3973
|
+
const row = (q) => along(blend.subarray(at(q, j, 0), at(q, j, nc)), x);
|
|
3974
|
+
return row(r) * (1 - t) + row(r + 1) * t;
|
|
3755
3975
|
}
|
|
3756
3976
|
};
|
|
3757
3977
|
}
|
|
3758
|
-
function
|
|
3759
|
-
const
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
const backAt = (j) => {
|
|
3774
|
-
let runs = backRows.get(j);
|
|
3775
|
-
if (runs === void 0) {
|
|
3776
|
-
runs = f.hairRuns("hair_back", top - j - 0.5);
|
|
3777
|
-
backRows.set(j, runs);
|
|
3778
|
-
}
|
|
3779
|
-
return runs;
|
|
3780
|
-
};
|
|
3781
|
-
const faceAt = (j) => {
|
|
3782
|
-
const y = top - j - 0.5;
|
|
3783
|
-
return f.faceRuns(y) ?? (y > f.face.y0 && y < f.face.y1 ? [f.face.x0, f.face.x1] : []);
|
|
3784
|
-
};
|
|
3785
|
-
const passed = (i, dys) => [
|
|
3786
|
-
Math.floor(i - Math.max(0, ...dys)),
|
|
3787
|
-
Math.ceil(i + 1 - Math.min(0, ...dys))
|
|
3788
|
-
];
|
|
3789
|
-
const rel = new Float64Array(n * xs.length * 2).fill(NaN);
|
|
3790
|
-
const relOf = (r, c, k) => {
|
|
3791
|
-
const at = (r * xs.length + c) * 2 + k;
|
|
3792
|
-
if (Number.isNaN(rel[at])) rel[at] = relAt(xs[c], ys[r], k === 0 ? -1 : 1);
|
|
3793
|
-
return rel[at];
|
|
3794
|
-
};
|
|
3795
|
-
const cellR = (x, y, k) => {
|
|
3796
|
-
const { c } = gridCol(xs, x);
|
|
3797
|
-
const { r } = gridRow(ys, y);
|
|
3798
|
-
return Math.max(
|
|
3799
|
-
relOf(r, c, k),
|
|
3800
|
-
relOf(r, c + 1, k),
|
|
3801
|
-
relOf(r + 1, c, k),
|
|
3802
|
-
relOf(r + 1, c + 1, k)
|
|
3803
|
-
);
|
|
3804
|
-
};
|
|
3805
|
-
const someAngle = (pairs) => {
|
|
3806
|
-
let lo = 0;
|
|
3807
|
-
let hi = 1;
|
|
3808
|
-
for (const [c0, c1] of pairs) {
|
|
3809
|
-
if (c1 === 0) {
|
|
3810
|
-
if (c0 <= 0) return false;
|
|
3811
|
-
} else if (c1 > 0) lo = Math.max(lo, -c0 / c1);
|
|
3812
|
-
else hi = Math.min(hi, -c0 / c1);
|
|
3813
|
-
}
|
|
3814
|
-
return lo < hi;
|
|
3978
|
+
function spreadBlend(b, a, xs, outward, s) {
|
|
3979
|
+
const fit = (ci, co, g) => {
|
|
3980
|
+
const quarter = Math.abs(xs[co] - xs[ci]) / 4;
|
|
3981
|
+
const today = g * (a[co] - a[ci]);
|
|
3982
|
+
const [lo, hi] = [
|
|
3983
|
+
Math.min(today, 0) - quarter,
|
|
3984
|
+
Math.max(today, 0) + quarter
|
|
3985
|
+
];
|
|
3986
|
+
const ga = g * a[ci];
|
|
3987
|
+
if (ga === 0) return;
|
|
3988
|
+
const outer = g * (1 - b[co]) * a[co];
|
|
3989
|
+
const [b0, b1] = [1 - (outer - hi) / ga, 1 - (outer - lo) / ga];
|
|
3990
|
+
const least = Math.max(b[ci], Math.min(b0, b1));
|
|
3991
|
+
if (least <= Math.min(1, Math.max(b0, b1))) b[ci] = least;
|
|
3992
|
+
else if (Math.min(b0, b1) > 1) b[ci] = 1;
|
|
3815
3993
|
};
|
|
3816
|
-
const
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
const [b0, b1] = passed(
|
|
3827
|
-
i,
|
|
3828
|
-
ays.map((ay) => nodDy(nod, ay, hh) - nodDy(NOD.hairBack, ay, hh))
|
|
3829
|
-
);
|
|
3830
|
-
const band = [];
|
|
3831
|
-
for (let j = b0; j < b1; j++) band.push(backAt(j));
|
|
3832
|
-
const faces = [];
|
|
3833
|
-
const [f0, f1] = passed(
|
|
3834
|
-
i,
|
|
3835
|
-
ays.map((ay) => nodDy(nod, ay, hh) - face(f.axisX, y, 0, ay)[1])
|
|
3836
|
-
);
|
|
3837
|
-
for (let j = f0; j < f1; j++)
|
|
3838
|
-
faces.push({ y: top - j - 0.5, runs: faceAt(j) });
|
|
3839
|
-
const front = f.hairRuns("hair_front", y);
|
|
3840
|
-
[-1, 1].forEach((s, side) => {
|
|
3841
|
-
let least = Infinity;
|
|
3842
|
-
const far = outerEdge(front, f.axisX, s);
|
|
3843
|
-
if (far !== void 0) {
|
|
3844
|
-
const R = cellR(far, y, side);
|
|
3845
|
-
const c = Math.min(...band.map((b) => runReach(b, far, s).outward));
|
|
3846
|
-
if (R > 0) least = Math.min(least, m.shellScale * c / R);
|
|
3847
|
-
}
|
|
3848
|
-
for (let t = s > 0 ? 0 : 1; t < front.length; t += 2) {
|
|
3849
|
-
const x = front[t];
|
|
3850
|
-
const R = cellR(x, y, side);
|
|
3851
|
-
if (R <= 0) continue;
|
|
3852
|
-
let c = Math.min(
|
|
3853
|
-
...band.map((b) => runReach(b, x, s > 0 ? -1 : 1).inward)
|
|
3854
|
-
);
|
|
3855
|
-
const sh = (m.shellScale - 1) * (x - f.axisX);
|
|
3856
|
-
const d0 = sh + s * m.hairBack;
|
|
3857
|
-
const d1 = d0 + s * R;
|
|
3858
|
-
const lo = Math.min(d0, d1);
|
|
3859
|
-
const hi = Math.max(d0, d1);
|
|
3860
|
-
const bares = faces.some(({ y: fy, runs }) => {
|
|
3861
|
-
for (let r = 0; r < runs.length; r += 2) {
|
|
3862
|
-
const e0 = runs[r];
|
|
3863
|
-
const e1 = runs[r + 1];
|
|
3864
|
-
const de0 = face(e0, fy, 30 * s, 0)[0];
|
|
3865
|
-
const de1 = face(e1, fy, 30 * s, 0)[0];
|
|
3866
|
-
if (someAngle([
|
|
3867
|
-
[e1 - x, de1 - lo],
|
|
3868
|
-
[x - e0, hi - de0]
|
|
3869
|
-
])) {
|
|
3870
|
-
return true;
|
|
3871
|
-
}
|
|
3872
|
-
}
|
|
3873
|
-
return false;
|
|
3874
|
-
});
|
|
3875
|
-
if (bares) c = 0;
|
|
3876
|
-
least = Math.min(least, m.shellScale * c / R);
|
|
3877
|
-
}
|
|
3878
|
-
slack[side][i] = least - w;
|
|
3994
|
+
for (const cols of outward) {
|
|
3995
|
+
for (let i = 1; i < cols.length; i++)
|
|
3996
|
+
b[cols[i]] = Math.max(b[cols[i]], b[cols[i - 1]]);
|
|
3997
|
+
}
|
|
3998
|
+
const [left, right] = outward.map((cols) => cols[0]);
|
|
3999
|
+
const across = left !== void 0 && right !== void 0 && left !== right;
|
|
4000
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
4001
|
+
outward.forEach((cols, k) => {
|
|
4002
|
+
const g = (k === 0 ? -1 : 1) * s;
|
|
4003
|
+
for (let i = cols.length - 1; i > 0; i--) fit(cols[i - 1], cols[i], g);
|
|
3879
4004
|
});
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
const out = wr.slice();
|
|
3884
|
-
for (let r = 0; r < n; r++) {
|
|
3885
|
-
if (wr[r] >= 1) continue;
|
|
3886
|
-
const [i0, i1] = rowWindow(ys, r, count);
|
|
3887
|
-
let least = Infinity;
|
|
3888
|
-
for (let i = i0; i < i1; i++) least = Math.min(least, slackAt(i, k));
|
|
3889
|
-
out[r] = wr[r] + clamp(least, 0, 1 - wr[r]);
|
|
4005
|
+
if (across) {
|
|
4006
|
+
if (b[left] <= b[right]) fit(left, right, s);
|
|
4007
|
+
else fit(right, left, -s);
|
|
3890
4008
|
}
|
|
3891
|
-
|
|
3892
|
-
if (wr[r] < 1) out[r] = Math.max(wr[r], Math.min(out[r], out[r + 1]));
|
|
3893
|
-
}
|
|
3894
|
-
return out.map((v, r) => v - wr[r]);
|
|
3895
|
-
});
|
|
3896
|
-
return (y, s) => {
|
|
3897
|
-
if (s === 0) return 0;
|
|
3898
|
-
const e = extra[s < 0 ? 0 : 1];
|
|
3899
|
-
const { r, t } = gridRow(ys, y, true);
|
|
3900
|
-
return e[r] * (1 - t) + e[r + 1] * t;
|
|
3901
|
-
};
|
|
4009
|
+
}
|
|
3902
4010
|
}
|
|
3903
4011
|
function familyField(m, family) {
|
|
3904
4012
|
switch (family) {
|
|
@@ -3916,6 +4024,8 @@ function familyField(m, family) {
|
|
|
3916
4024
|
return mouthField(m);
|
|
3917
4025
|
case "hair_front":
|
|
3918
4026
|
return hairFrontField(m);
|
|
4027
|
+
case "hair_side":
|
|
4028
|
+
return hairSideField(m);
|
|
3919
4029
|
case "hair_back":
|
|
3920
4030
|
return hairField(m, m.hairBack, NOD.hairBack);
|
|
3921
4031
|
case "body":
|
|
@@ -4072,16 +4182,16 @@ function strandOverlaps(ctx, land2, holdBase) {
|
|
|
4072
4182
|
if (px <= 0) continue;
|
|
4073
4183
|
out[key] = {
|
|
4074
4184
|
deg,
|
|
4075
|
-
px:
|
|
4076
|
-
hh:
|
|
4077
|
-
restPx:
|
|
4185
|
+
px: round32(px),
|
|
4186
|
+
hh: round32(px) / holdBase,
|
|
4187
|
+
restPx: round32(restPx),
|
|
4078
4188
|
// A fringe spanning the face has no side to hold the iris clear of.
|
|
4079
4189
|
held: strand.runFace !== null && px <= restPx + STRAND_SLACK
|
|
4080
4190
|
};
|
|
4081
4191
|
}
|
|
4082
4192
|
return out.left || out.right ? out : void 0;
|
|
4083
4193
|
}
|
|
4084
|
-
var
|
|
4194
|
+
var round32 = (v) => Math.round(v * 1e3) / 1e3;
|
|
4085
4195
|
var STRAND_SLACK = 0.5;
|
|
4086
4196
|
function bisect(f, lo, hi, it = 50) {
|
|
4087
4197
|
for (let i = 0; i < it; i++) {
|
|
@@ -4296,7 +4406,7 @@ function turnReport(ctx, fit, rendered) {
|
|
|
4296
4406
|
const plate = depth(m.face);
|
|
4297
4407
|
const w = frame.paintedHalfAt(frame.eyeY);
|
|
4298
4408
|
const report = {
|
|
4299
|
-
radius:
|
|
4409
|
+
radius: round32(w * w / (2 * Math.max(1e-3, eye - plate))),
|
|
4300
4410
|
holdBase: hb,
|
|
4301
4411
|
depths: {
|
|
4302
4412
|
eye: round4(eye / hb),
|
|
@@ -4312,6 +4422,8 @@ function turnReport(ctx, fit, rendered) {
|
|
|
4312
4422
|
};
|
|
4313
4423
|
const overlap = strandOverlaps(ctx, rendered, hb);
|
|
4314
4424
|
if (overlap !== void 0) report.strandOverlap = overlap;
|
|
4425
|
+
const hairSide = hairSideRoom(ctx, rendered, m.boxes);
|
|
4426
|
+
if (hairSide !== void 0) report.hairSide = hairSide;
|
|
4315
4427
|
return report;
|
|
4316
4428
|
}
|
|
4317
4429
|
var round4 = (v) => Math.round(v * 1e4) / 1e4;
|
|
@@ -4467,7 +4579,7 @@ var RIG_PLAN_FIELDS = [
|
|
|
4467
4579
|
fitBy: null,
|
|
4468
4580
|
provisional: false,
|
|
4469
4581
|
migratedFrom: "outlineFollow",
|
|
4470
|
-
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)"
|
|
4471
4583
|
},
|
|
4472
4584
|
{
|
|
4473
4585
|
path: "hair.sway",
|
|
@@ -5533,6 +5645,7 @@ function checkLayers(layers, canvas) {
|
|
|
5533
5645
|
}
|
|
5534
5646
|
checkArmsHaveBody(seen);
|
|
5535
5647
|
checkPosesHaveArms(seen);
|
|
5648
|
+
checkSidesHaveFront(seen);
|
|
5536
5649
|
}
|
|
5537
5650
|
function checkHeadEdges(edges, byRole) {
|
|
5538
5651
|
if (edges === void 0) return;
|
|
@@ -6259,6 +6372,10 @@ var MESH_CELLS = {
|
|
|
6259
6372
|
face: { px: 28, min: 4, max: 24 },
|
|
6260
6373
|
// The front hair eases from riding the face to holding the outline.
|
|
6261
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 },
|
|
6262
6379
|
hair_back: { px: 56, min: 3, max: 18 }
|
|
6263
6380
|
};
|
|
6264
6381
|
var NECK_CELLS = { px: 20, min: 3, max: 12 };
|
|
@@ -6310,7 +6427,18 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
6310
6427
|
const solveFrame = buildHeadFrame(layers, frameOptions);
|
|
6311
6428
|
const ctx = solveContext(solveFrame, byRole, turnOptions, hasNose);
|
|
6312
6429
|
const boxes = Object.fromEntries(
|
|
6313
|
-
[
|
|
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)])
|
|
6314
6442
|
);
|
|
6315
6443
|
const landmarks = {
|
|
6316
6444
|
boxes,
|
|
@@ -6515,6 +6643,8 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
|
|
|
6515
6643
|
headStart = fm.headStart;
|
|
6516
6644
|
} else if (spec.role === "hair_front") {
|
|
6517
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));
|
|
6518
6648
|
} else if (lips !== void 0 && (isLipRole(spec.role) || isLipInsideRole(spec.role))) {
|
|
6519
6649
|
({ mesh, xs: knotXs } = mouthMesh(b, lips.knots));
|
|
6520
6650
|
} else if (spec.family !== "body") {
|
|
@@ -6529,6 +6659,7 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
|
|
|
6529
6659
|
const side = spec.role.endsWith("_L") ? "L" : "R";
|
|
6530
6660
|
const eyeOpen = side === "L" ? P10.EyeOpenLeft : P10.EyeOpenRight;
|
|
6531
6661
|
const hh = frame.hh;
|
|
6662
|
+
const swayAmp = plan.hair.sway * plan.profile.AMPLITUDE.sway * hh;
|
|
6532
6663
|
switch (spec.role) {
|
|
6533
6664
|
case "face":
|
|
6534
6665
|
if (region?.some((r) => r === "neck")) {
|
|
@@ -6665,18 +6796,30 @@ function buildPart(spec, b, order, frame, byRole, plan, frontGrid, lips) {
|
|
|
6665
6796
|
case "hair_front":
|
|
6666
6797
|
case "hair_back":
|
|
6667
6798
|
if (byRole.has("hair_front")) {
|
|
6668
|
-
const amp = plan.hair.sway * plan.profile.AMPLITUDE.sway * hh;
|
|
6669
6799
|
if (spec.role === "hair_front") {
|
|
6670
6800
|
warps.push(
|
|
6671
|
-
hairSway(P10.HairSwayX, mesh, b,
|
|
6672
|
-
hairSway(P10.HairSwayZ, mesh, b,
|
|
6801
|
+
hairSway(P10.HairSwayX, mesh, b, swayAmp),
|
|
6802
|
+
hairSway(P10.HairSwayZ, mesh, b, swayAmp)
|
|
6673
6803
|
);
|
|
6674
6804
|
} else {
|
|
6675
|
-
warps.push(hairSway(P10.HairBackSway, mesh, b,
|
|
6805
|
+
warps.push(hairSway(P10.HairBackSway, mesh, b, swayAmp));
|
|
6676
6806
|
}
|
|
6677
6807
|
}
|
|
6678
6808
|
warps.push(tiltHang(mesh, b, pivot, hairHangWeight(frame.chinY, b.y0)));
|
|
6679
6809
|
break;
|
|
6810
|
+
case "hair_side_L":
|
|
6811
|
+
case "hair_side_R": {
|
|
6812
|
+
const attach = {
|
|
6813
|
+
box: box("hair_front"),
|
|
6814
|
+
weight: sideRootBlend(frame, spec.role, b).weight
|
|
6815
|
+
};
|
|
6816
|
+
warps.push(
|
|
6817
|
+
lockSway(P10.HairSwayX, mesh, b, swayAmp, attach),
|
|
6818
|
+
lockSway(P10.HairSwayZ, mesh, b, swayAmp, attach),
|
|
6819
|
+
tiltHang(mesh, b, pivot, lockHangWeight(frame.chinY, b.y0, hh))
|
|
6820
|
+
);
|
|
6821
|
+
break;
|
|
6822
|
+
}
|
|
6680
6823
|
}
|
|
6681
6824
|
if (warps.length > 0) part.warps = warps;
|
|
6682
6825
|
if (bindings.length > 0) part.bindings = bindings;
|
|
@@ -6733,9 +6876,34 @@ function hairFrontGrid(b, frame, boxes) {
|
|
|
6733
6876
|
}
|
|
6734
6877
|
return { xs, ys };
|
|
6735
6878
|
}
|
|
6879
|
+
var BLEND_ROWS = 3;
|
|
6880
|
+
function sideLockGrid(b, frame, role) {
|
|
6881
|
+
const cells = MESH_CELLS[role];
|
|
6882
|
+
const px = Math.min(cells.px, meshScale(role, frame));
|
|
6883
|
+
const cols = cellsFor(bw(b), px, cells.min, cells.max);
|
|
6884
|
+
const rows = cellsFor(bh(b), px, cells.min, cells.max);
|
|
6885
|
+
const xs = Array.from(
|
|
6886
|
+
{ length: cols + 1 },
|
|
6887
|
+
(_, c) => b.x0 + bw(b) * c / cols
|
|
6888
|
+
);
|
|
6889
|
+
const ys = Array.from(
|
|
6890
|
+
{ length: rows + 1 },
|
|
6891
|
+
(_, r) => b.y1 - bh(b) * r / rows
|
|
6892
|
+
);
|
|
6893
|
+
const { rootEnd } = sideRootBlend(frame, role, b);
|
|
6894
|
+
const end = rootEnd - SIDE_ROOT_BLEND * frame.hh;
|
|
6895
|
+
for (let i = 0; i <= BLEND_ROWS + 1; i++) {
|
|
6896
|
+
const y = rootEnd + (end - rootEnd) * i / (BLEND_ROWS + 1);
|
|
6897
|
+
if (y > b.y0 + 0.25 && y < b.y1 - 0.25 && ys.every((v) => Math.abs(v - y) > 0.25))
|
|
6898
|
+
ys.push(y);
|
|
6899
|
+
}
|
|
6900
|
+
return { xs, ys: ys.sort((p, q) => q - p) };
|
|
6901
|
+
}
|
|
6736
6902
|
function meshScale(role, frame) {
|
|
6737
6903
|
switch (role) {
|
|
6738
6904
|
case "hair_front":
|
|
6905
|
+
case "hair_side_L":
|
|
6906
|
+
case "hair_side_R":
|
|
6739
6907
|
case "hair_back":
|
|
6740
6908
|
return Math.max(2, (frame.shellRight - frame.shellLeft) / 9);
|
|
6741
6909
|
default:
|