@ikijs/editor 0.6.0 → 0.7.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 +34 -6
- package/dist/index.d.mts +113 -6
- package/dist/index.d.ts +113 -6
- package/dist/index.js +520 -137
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +520 -137
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1839,6 +1839,125 @@ function validateLayerInputs(layers, canvas) {
|
|
|
1839
1839
|
}
|
|
1840
1840
|
}
|
|
1841
1841
|
}
|
|
1842
|
+
function validateStrandEdges(layers, strandEdges) {
|
|
1843
|
+
if (strandEdges === void 0) return;
|
|
1844
|
+
const where = "auto-rig: validateStrandEdges: strandEdges";
|
|
1845
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1846
|
+
if (!isPlainObject(strandEdges)) {
|
|
1847
|
+
throw new Error(
|
|
1848
|
+
`${where} must be a plain object with optional left / right sides`
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
for (const key of ["left", "right"]) {
|
|
1852
|
+
const raw = strandEdges[key];
|
|
1853
|
+
if (raw === void 0) continue;
|
|
1854
|
+
const path = `${where}.${key}`;
|
|
1855
|
+
if (!isPlainObject(raw)) {
|
|
1856
|
+
throw new Error(`${path} must be a plain object`);
|
|
1857
|
+
}
|
|
1858
|
+
for (const field of ["y", "irisOuter", "irisInner", "runOuter"]) {
|
|
1859
|
+
const v = raw[field];
|
|
1860
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1861
|
+
throw new Error(
|
|
1862
|
+
`${path}.${field} is ${String(v)}, not a finite number`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const rawFace = raw.runFace;
|
|
1867
|
+
if (rawFace !== null && (typeof rawFace !== "number" || !Number.isFinite(rawFace))) {
|
|
1868
|
+
throw new Error(
|
|
1869
|
+
`${path}.runFace is ${String(rawFace)}, not a finite number or null`
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
const { y, irisOuter, irisInner, runOuter, runFace } = raw;
|
|
1873
|
+
const cropOf = (role) => {
|
|
1874
|
+
const layer = layers.find((l) => l.role === role);
|
|
1875
|
+
if (layer === void 0) {
|
|
1876
|
+
throw new Error(
|
|
1877
|
+
`${path} needs a "${role}" layer to be measured on, and this layer set has none`
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
1881
|
+
return {
|
|
1882
|
+
role,
|
|
1883
|
+
x: t.x,
|
|
1884
|
+
left: t.x - layer.cropW / 2,
|
|
1885
|
+
right: t.x + layer.cropW / 2,
|
|
1886
|
+
bottom: t.y - layer.cropH / 2,
|
|
1887
|
+
top: t.y + layer.cropH / 2
|
|
1888
|
+
};
|
|
1889
|
+
};
|
|
1890
|
+
const hair = cropOf("hair_front");
|
|
1891
|
+
const [lowIris, highIris] = [cropOf("iris_L"), cropOf("iris_R")].sort(
|
|
1892
|
+
(a, b) => a.x - b.x
|
|
1893
|
+
);
|
|
1894
|
+
const sign = key === "left" ? -1 : 1;
|
|
1895
|
+
const iris = key === "left" ? lowIris : highIris;
|
|
1896
|
+
const other = key === "left" ? highIris : lowIris;
|
|
1897
|
+
const fail = (field, value, why) => new Error(`${path}.${field} (${value}) ${why}`);
|
|
1898
|
+
for (const crop of [iris, hair]) {
|
|
1899
|
+
if (y < crop.bottom || y > crop.top) {
|
|
1900
|
+
throw fail(
|
|
1901
|
+
"y",
|
|
1902
|
+
y,
|
|
1903
|
+
`lies outside the "${crop.role}" crop's rows (${crop.bottom}\u2026${crop.top})`
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
for (const [field, x] of [
|
|
1908
|
+
["irisOuter", irisOuter],
|
|
1909
|
+
["irisInner", irisInner]
|
|
1910
|
+
]) {
|
|
1911
|
+
if (x < iris.left || x > iris.right) {
|
|
1912
|
+
throw fail(
|
|
1913
|
+
field,
|
|
1914
|
+
x,
|
|
1915
|
+
`lies outside the "${iris.role}" crop's columns (${iris.left}\u2026${iris.right})`
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
if (sign * (irisOuter - irisInner) <= 0) {
|
|
1920
|
+
throw fail(
|
|
1921
|
+
"irisOuter",
|
|
1922
|
+
irisOuter,
|
|
1923
|
+
`must lie strictly outward of strandEdges.${key}.irisInner (${irisInner})`
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
for (const [field, x] of [
|
|
1927
|
+
["runOuter", runOuter],
|
|
1928
|
+
["runFace", runFace]
|
|
1929
|
+
]) {
|
|
1930
|
+
if (x !== null && (x < hair.left || x > hair.right)) {
|
|
1931
|
+
throw fail(
|
|
1932
|
+
field,
|
|
1933
|
+
x,
|
|
1934
|
+
`lies outside the "hair_front" crop's columns (${hair.left}\u2026${hair.right})`
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (sign * (runOuter - iris.x) <= 0) {
|
|
1939
|
+
throw fail(
|
|
1940
|
+
"runOuter",
|
|
1941
|
+
runOuter,
|
|
1942
|
+
`must lie strictly outward of the "${iris.role}" crop's centre (${iris.x})`
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
if (runFace !== null && sign * (runOuter - runFace) <= 0) {
|
|
1946
|
+
throw fail(
|
|
1947
|
+
"runOuter",
|
|
1948
|
+
runOuter,
|
|
1949
|
+
`must lie strictly outward of strandEdges.${key}.runFace (${runFace})`
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
if (runFace !== null && sign * (runFace - other.x) <= 0) {
|
|
1953
|
+
throw fail(
|
|
1954
|
+
"runFace",
|
|
1955
|
+
runFace,
|
|
1956
|
+
`must lie strictly on this side of the "${other.role}" crop's centre (${other.x}); a run that reaches it has no face-side end here, which is runFace null`
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1842
1961
|
function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
1843
1962
|
const pts = [];
|
|
1844
1963
|
for (let row = 0; row <= rows; row++) {
|
|
@@ -1995,21 +2114,26 @@ function turnSurface(spec) {
|
|
|
1995
2114
|
)
|
|
1996
2115
|
};
|
|
1997
2116
|
}
|
|
1998
|
-
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt) {
|
|
2117
|
+
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt, anchor) {
|
|
1999
2118
|
const STOPS = [...HEAD_TURN_STOPS];
|
|
2000
2119
|
const pointCount = grid.points.length / 2;
|
|
2001
2120
|
const keyforms2d = [];
|
|
2002
2121
|
for (const angleY of STOPS) {
|
|
2003
2122
|
for (const angleX of STOPS) {
|
|
2004
2123
|
const shift = shiftAt(angleX);
|
|
2124
|
+
const mapAt = (nodeY) => surface.mapAt(angleX, nodeY);
|
|
2005
2125
|
const offsets = [];
|
|
2006
2126
|
for (let i = 0; i < pointCount; i++) {
|
|
2007
2127
|
const x = grid.points[i * 2];
|
|
2008
2128
|
const y = grid.points[i * 2 + 1];
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2129
|
+
const nodDy = angleY === 0 ? 0 : surface.nodBendAt(y, angleY);
|
|
2130
|
+
if (angleX === 0) {
|
|
2131
|
+
offsets.push(0, nodDy);
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
const landed = groupNodeLanding(anchor, mapAt, angleX, shift, x, y);
|
|
2135
|
+
const turnDy = landed.y - y;
|
|
2136
|
+
offsets.push(landed.x - x, angleY === 0 ? turnDy : turnDy + nodDy);
|
|
2013
2137
|
}
|
|
2014
2138
|
keyforms2d.push({ offsets });
|
|
2015
2139
|
}
|
|
@@ -2129,6 +2253,22 @@ function carrierLandingX(carrier, nodeLanding, px, py) {
|
|
|
2129
2253
|
py
|
|
2130
2254
|
);
|
|
2131
2255
|
}
|
|
2256
|
+
function groupNodeLanding(anchor, mapAt, deg, shift, nodeX, nodeY) {
|
|
2257
|
+
if (anchor === void 0) {
|
|
2258
|
+
return { x: mapAt(nodeY).mapX(nodeX + shift), y: nodeY };
|
|
2259
|
+
}
|
|
2260
|
+
const anchorRow = mapAt(anchor.y);
|
|
2261
|
+
const pivotX = anchorRow.mapX(anchor.x + shift);
|
|
2262
|
+
const dx = mapAt(nodeY).mapX(nodeX) - anchorRow.mapX(anchor.x);
|
|
2263
|
+
const dy = nodeY - anchor.y;
|
|
2264
|
+
const phi = anchor.tiltDeg * deg / HEAD_TURN_MAX_DEG * (Math.PI / 180);
|
|
2265
|
+
const cos = Math.cos(phi);
|
|
2266
|
+
const sin = Math.sin(phi);
|
|
2267
|
+
return {
|
|
2268
|
+
x: pivotX + cos * dx - sin * dy,
|
|
2269
|
+
y: anchor.y + sin * dx + cos * dy
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2132
2272
|
function rowMapsOf(surface) {
|
|
2133
2273
|
const maps = /* @__PURE__ */ new Map();
|
|
2134
2274
|
return (deg, nodeY) => {
|
|
@@ -2282,11 +2422,12 @@ function eyeRowOf(landmarks, fallback) {
|
|
|
2282
2422
|
}
|
|
2283
2423
|
var TURN_BISECT_STEPS = 40;
|
|
2284
2424
|
var TURN_DEPTH_EPS = 1e-6;
|
|
2285
|
-
function landmarkLandingX(landmark, mapAt, shift, px, py) {
|
|
2286
|
-
|
|
2425
|
+
function landmarkLandingX(landmark, mapAt, deg, shift, px, py) {
|
|
2426
|
+
const { carrier } = landmark;
|
|
2427
|
+
if (carrier === void 0) return mapAt(py).mapX(px + shift);
|
|
2287
2428
|
return carrierLandingX(
|
|
2288
|
-
|
|
2289
|
-
(nodeX, nodeY) =>
|
|
2429
|
+
carrier,
|
|
2430
|
+
(nodeX, nodeY) => groupNodeLanding(carrier.anchor, mapAt, deg, shift, nodeX, nodeY).x,
|
|
2290
2431
|
px,
|
|
2291
2432
|
py
|
|
2292
2433
|
);
|
|
@@ -2295,22 +2436,47 @@ function familyReachPx(landmarks, plateEdgeX) {
|
|
|
2295
2436
|
const farEdge = Math.min(...landmarks.map((l) => l.x - l.w / 2));
|
|
2296
2437
|
return Math.max(0, farEdge - plateEdgeX);
|
|
2297
2438
|
}
|
|
2298
|
-
function
|
|
2439
|
+
function familyDepthCap(landmarks, plateEdgeX, unit) {
|
|
2440
|
+
return familyReachPx(landmarks, plateEdgeX) / unit;
|
|
2441
|
+
}
|
|
2442
|
+
function solveTurnDepthSigned(target, landmarks, unit, mapAt, plateEdgeX, maxDepth) {
|
|
2299
2443
|
if (landmarks.length === 0) {
|
|
2300
2444
|
throw new Error("auto-rig: solveTurnDepth: no landmark to slide");
|
|
2301
2445
|
}
|
|
2302
|
-
const
|
|
2446
|
+
const plateCap = familyDepthCap(landmarks, plateEdgeX, unit);
|
|
2447
|
+
const cap = maxDepth === void 0 ? plateCap : Math.min(plateCap, maxDepth);
|
|
2303
2448
|
const achieved = (d) => landmarks.reduce(
|
|
2304
|
-
(sum, l) => sum + (landmarkLandingX(
|
|
2449
|
+
(sum, l) => sum + (landmarkLandingX(
|
|
2450
|
+
l,
|
|
2451
|
+
mapAt,
|
|
2452
|
+
-HEAD_TURN_MAX_DEG,
|
|
2453
|
+
-d * unit,
|
|
2454
|
+
l.x,
|
|
2455
|
+
l.y ?? 0
|
|
2456
|
+
) - l.x),
|
|
2305
2457
|
0
|
|
2306
2458
|
) / landmarks.length;
|
|
2307
2459
|
const atCap = achieved(cap);
|
|
2308
2460
|
const atRest = achieved(0);
|
|
2309
2461
|
const attainable = [atCap, atRest];
|
|
2310
|
-
if (target > atRest + TURN_DEPTH_EPS)
|
|
2311
|
-
return {
|
|
2312
|
-
|
|
2313
|
-
|
|
2462
|
+
if (target > atRest + TURN_DEPTH_EPS) {
|
|
2463
|
+
return {
|
|
2464
|
+
reached: false,
|
|
2465
|
+
depth: 0,
|
|
2466
|
+
achieved: atRest,
|
|
2467
|
+
attainable,
|
|
2468
|
+
limit: "floor"
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
if (target < atCap - TURN_DEPTH_EPS) {
|
|
2472
|
+
return {
|
|
2473
|
+
reached: false,
|
|
2474
|
+
depth: cap,
|
|
2475
|
+
achieved: atCap,
|
|
2476
|
+
attainable,
|
|
2477
|
+
limit: "cap"
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2314
2480
|
const clampedTarget = Math.min(Math.max(target, atCap), atRest);
|
|
2315
2481
|
let lo = 0;
|
|
2316
2482
|
let hi = cap;
|
|
@@ -2340,7 +2506,7 @@ function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, hol
|
|
|
2340
2506
|
}
|
|
2341
2507
|
return x + side * (holdEdge - holdBase);
|
|
2342
2508
|
}
|
|
2343
|
-
function hairFrontLandingAt(x, y, hairFront, holdTargetAt,
|
|
2509
|
+
function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadShift) {
|
|
2344
2510
|
const { cols, rows } = meshCellsFor(hairFront.cropW, hairFront.cropH);
|
|
2345
2511
|
const top = hairFront.centerY + hairFront.cropH / 2;
|
|
2346
2512
|
return renderedLandingX(
|
|
@@ -2355,7 +2521,7 @@ function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadTipShift) {
|
|
|
2355
2521
|
scaleY: 1,
|
|
2356
2522
|
rotation: 0
|
|
2357
2523
|
},
|
|
2358
|
-
(vx, vy) => holdTargetAt(vx, vy)
|
|
2524
|
+
(vx, vy) => holdTargetAt(vx, vy) + leadShift * Math.pow((top - vy) / hairFront.cropH, HAIR_SWAY_CURL),
|
|
2359
2525
|
x,
|
|
2360
2526
|
y
|
|
2361
2527
|
);
|
|
@@ -2388,6 +2554,19 @@ function aitken(x0, x1, x2, cap) {
|
|
|
2388
2554
|
const x = x2 - d2 * d2 / denominator;
|
|
2389
2555
|
return Number.isFinite(x) ? Math.min(cap, Math.max(0, x)) : x2;
|
|
2390
2556
|
}
|
|
2557
|
+
function strandDepthCap(margins, plateCap) {
|
|
2558
|
+
const holds = (depth) => margins.every((m) => m(depth) >= 0);
|
|
2559
|
+
if (holds(plateCap)) return plateCap;
|
|
2560
|
+
if (!holds(0)) return void 0;
|
|
2561
|
+
let lo = 0;
|
|
2562
|
+
let hi = plateCap;
|
|
2563
|
+
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2564
|
+
const mid = (lo + hi) / 2;
|
|
2565
|
+
if (holds(mid)) lo = mid;
|
|
2566
|
+
else hi = mid;
|
|
2567
|
+
}
|
|
2568
|
+
return lo;
|
|
2569
|
+
}
|
|
2391
2570
|
function evaluateTurnCandidate(ctx, radius) {
|
|
2392
2571
|
const surfaceAt = (travel) => turnSurface({
|
|
2393
2572
|
faceCenterX: ctx.faceCenterX,
|
|
@@ -2411,7 +2590,104 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2411
2590
|
return edge === void 0 ? ctx.headHalfWidth : side * (edge - ctx.faceCenterX);
|
|
2412
2591
|
};
|
|
2413
2592
|
const unit = headTurnParallaxUnit(radius);
|
|
2593
|
+
const plateCapOf = (marks) => familyDepthCap(marks, ctx.plateEdgeX, unit);
|
|
2594
|
+
const eyePlateCap = plateCapOf(ctx.landmarks.eye);
|
|
2414
2595
|
const leadTipShift = ctx.hairFrontSilhouette === void 0 ? 0 : HAIR_FRONT_DEPTH * unit;
|
|
2596
|
+
const bangsLandingAt = (hold, plateLandingAt, deg, leadShift, x, y) => hairFrontLandingAt(
|
|
2597
|
+
x,
|
|
2598
|
+
y,
|
|
2599
|
+
ctx.hairFrontSilhouette,
|
|
2600
|
+
(vx, vy) => hairFrontHoldTarget(
|
|
2601
|
+
vx,
|
|
2602
|
+
vy,
|
|
2603
|
+
deg,
|
|
2604
|
+
ctx.faceCenterX,
|
|
2605
|
+
plateLandingAt,
|
|
2606
|
+
ctx.edgeAt,
|
|
2607
|
+
hold.holdBase,
|
|
2608
|
+
hold.holdEdgeAt(deg)
|
|
2609
|
+
),
|
|
2610
|
+
leadShift
|
|
2611
|
+
);
|
|
2612
|
+
const strandStopsOn = (hold) => {
|
|
2613
|
+
const rowMaps = rowMapsOf(hold.surface);
|
|
2614
|
+
const plateLandingAt = plateLandingOn(ctx.plate, rowMaps);
|
|
2615
|
+
return (ctx.strands ?? []).map((strand) => {
|
|
2616
|
+
const iris = {
|
|
2617
|
+
x: strand.carrier.part.x,
|
|
2618
|
+
w: strand.carrier.part.cropW,
|
|
2619
|
+
carrier: strand.carrier
|
|
2620
|
+
};
|
|
2621
|
+
const stops = HEAD_TURN_STOPS.filter(
|
|
2622
|
+
(deg) => Math.sign(deg) === strand.side
|
|
2623
|
+
).map((deg) => {
|
|
2624
|
+
const runAt = (x) => bangsLandingAt(
|
|
2625
|
+
hold,
|
|
2626
|
+
plateLandingAt,
|
|
2627
|
+
deg,
|
|
2628
|
+
leadTipShift * deg / HEAD_TURN_MAX_DEG,
|
|
2629
|
+
x,
|
|
2630
|
+
strand.y
|
|
2631
|
+
);
|
|
2632
|
+
return {
|
|
2633
|
+
deg,
|
|
2634
|
+
runOuter: runAt(strand.runOuter),
|
|
2635
|
+
runFace: strand.runFace === null ? null : runAt(strand.runFace),
|
|
2636
|
+
irisAt: (depth, x) => landmarkLandingX(
|
|
2637
|
+
iris,
|
|
2638
|
+
(y) => rowMaps(deg, y),
|
|
2639
|
+
deg,
|
|
2640
|
+
depth * unit * deg / HEAD_TURN_MAX_DEG,
|
|
2641
|
+
x,
|
|
2642
|
+
strand.y
|
|
2643
|
+
)
|
|
2644
|
+
};
|
|
2645
|
+
});
|
|
2646
|
+
const { runFace } = strand;
|
|
2647
|
+
if (runFace === null) return { strand, stops, margins: void 0 };
|
|
2648
|
+
const allowance = Math.min(0, strand.side * (runFace - strand.irisOuter));
|
|
2649
|
+
const margins = stops.map(
|
|
2650
|
+
(stop) => (depth) => strand.side * (stop.runFace - stop.irisAt(depth, strand.irisOuter)) - allowance
|
|
2651
|
+
);
|
|
2652
|
+
return { strand, stops, margins };
|
|
2653
|
+
});
|
|
2654
|
+
};
|
|
2655
|
+
const strandOverlapOf = (sides, depth) => {
|
|
2656
|
+
const overlap = {};
|
|
2657
|
+
for (const { strand, stops, margins } of sides) {
|
|
2658
|
+
const s = strand.side;
|
|
2659
|
+
const covered = (irisOuter, irisInner, runOuter, runFace) => Math.max(
|
|
2660
|
+
0,
|
|
2661
|
+
Math.min(s * irisOuter, s * runOuter) - Math.max(s * irisInner, runFace === null ? -Infinity : s * runFace)
|
|
2662
|
+
);
|
|
2663
|
+
let worst;
|
|
2664
|
+
for (const stop of stops) {
|
|
2665
|
+
const px = covered(
|
|
2666
|
+
stop.irisAt(depth, strand.irisOuter),
|
|
2667
|
+
stop.irisAt(depth, strand.irisInner),
|
|
2668
|
+
stop.runOuter,
|
|
2669
|
+
stop.runFace
|
|
2670
|
+
);
|
|
2671
|
+
if (px > 0 && (worst === void 0 || px > worst.px)) {
|
|
2672
|
+
worst = { deg: stop.deg, px };
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
if (worst === void 0) continue;
|
|
2676
|
+
overlap[s < 0 ? "left" : "right"] = {
|
|
2677
|
+
deg: worst.deg,
|
|
2678
|
+
px: worst.px,
|
|
2679
|
+
hh: worst.px / ctx.hh,
|
|
2680
|
+
restPx: covered(
|
|
2681
|
+
strand.irisOuter,
|
|
2682
|
+
strand.irisInner,
|
|
2683
|
+
strand.runOuter,
|
|
2684
|
+
strand.runFace
|
|
2685
|
+
),
|
|
2686
|
+
held: margins !== void 0 && margins.every((m) => m(depth) >= 0)
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
return overlap.left === void 0 && overlap.right === void 0 ? void 0 : overlap;
|
|
2690
|
+
};
|
|
2415
2691
|
const evaluateHold = (ratio, depths2) => {
|
|
2416
2692
|
const holdEdgeFrom = (base) => (deg) => base * (1 + (ratio - 1) * Math.abs(deg) / HEAD_TURN_MAX_DEG);
|
|
2417
2693
|
const travel = ctx.headHalfWidth === void 0 ? ctx.travel : Math.min(
|
|
@@ -2461,21 +2737,13 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2461
2737
|
if (edge !== void 0) return edge;
|
|
2462
2738
|
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side * holdBase : ctx.hairFrontSilhouette.x + side * ctx.hairFrontSilhouette.cropW / 2;
|
|
2463
2739
|
};
|
|
2464
|
-
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x :
|
|
2740
|
+
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x : bangsLandingAt(
|
|
2741
|
+
{ holdBase, holdEdgeAt },
|
|
2742
|
+
plateLandingAt,
|
|
2743
|
+
-HEAD_TURN_MAX_DEG,
|
|
2744
|
+
-leadTipShift,
|
|
2465
2745
|
x,
|
|
2466
|
-
ctx.eyeRowY
|
|
2467
|
-
ctx.hairFrontSilhouette,
|
|
2468
|
-
(vx, vy) => hairFrontHoldTarget(
|
|
2469
|
-
vx,
|
|
2470
|
-
vy,
|
|
2471
|
-
-HEAD_TURN_MAX_DEG,
|
|
2472
|
-
ctx.faceCenterX,
|
|
2473
|
-
plateLandingAt,
|
|
2474
|
-
ctx.edgeAt,
|
|
2475
|
-
holdBase,
|
|
2476
|
-
holdEdgeAt(-HEAD_TURN_MAX_DEG)
|
|
2477
|
-
),
|
|
2478
|
-
leadTipShift
|
|
2746
|
+
ctx.eyeRowY
|
|
2479
2747
|
);
|
|
2480
2748
|
const landingOfRole = (role, x) => {
|
|
2481
2749
|
if (role === "hair_front") return hairFrontAt(x);
|
|
@@ -2495,7 +2763,14 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2495
2763
|
const shift = role === "face" ? 0 : -depths2[turnFamily(role)] * unit;
|
|
2496
2764
|
return carrierLandingX(
|
|
2497
2765
|
carrier,
|
|
2498
|
-
(nodeX, nodeY) =>
|
|
2766
|
+
(nodeX, nodeY) => groupNodeLanding(
|
|
2767
|
+
carrier.anchor,
|
|
2768
|
+
mapAt,
|
|
2769
|
+
-HEAD_TURN_MAX_DEG,
|
|
2770
|
+
shift,
|
|
2771
|
+
nodeX,
|
|
2772
|
+
nodeY
|
|
2773
|
+
).x,
|
|
2499
2774
|
x,
|
|
2500
2775
|
ctx.eyeRowY
|
|
2501
2776
|
);
|
|
@@ -2636,12 +2911,25 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2636
2911
|
"auto-rig: evaluateTurnCandidate: the hold it settled on folds"
|
|
2637
2912
|
);
|
|
2638
2913
|
}
|
|
2914
|
+
const strandSides = ctx.strands === void 0 ? void 0 : strandStopsOn(hold);
|
|
2915
|
+
let strandFeasible = true;
|
|
2916
|
+
let maxDepth;
|
|
2917
|
+
if (strandSides !== void 0) {
|
|
2918
|
+
const margins = strandSides.map((s) => s.margins);
|
|
2919
|
+
const cap = margins.every((m) => m !== void 0) ? strandDepthCap(
|
|
2920
|
+
margins.flatMap((m) => m),
|
|
2921
|
+
eyePlateCap
|
|
2922
|
+
) : void 0;
|
|
2923
|
+
strandFeasible = cap !== void 0;
|
|
2924
|
+
maxDepth = cap ?? 0;
|
|
2925
|
+
}
|
|
2639
2926
|
const eye = solveTurnDepthSigned(
|
|
2640
2927
|
hold.silhouetteCenterShift - ctx.eyeShift * ctx.hh,
|
|
2641
2928
|
ctx.landmarks.eye,
|
|
2642
2929
|
unit,
|
|
2643
2930
|
hold.mapAt,
|
|
2644
|
-
ctx.plateEdgeX
|
|
2931
|
+
ctx.plateEdgeX,
|
|
2932
|
+
maxDepth
|
|
2645
2933
|
);
|
|
2646
2934
|
const features = solveFeatureDepths(
|
|
2647
2935
|
ctx,
|
|
@@ -2662,25 +2950,19 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2662
2950
|
seed = depths;
|
|
2663
2951
|
depths = solved;
|
|
2664
2952
|
} else {
|
|
2665
|
-
const capOf = (marks) => familyReachPx(marks, ctx.plateEdgeX) / unit;
|
|
2666
2953
|
depths = {
|
|
2667
|
-
eye: aitken(
|
|
2668
|
-
seed.eye,
|
|
2669
|
-
depths.eye,
|
|
2670
|
-
solved.eye,
|
|
2671
|
-
capOf(ctx.landmarks.eye)
|
|
2672
|
-
),
|
|
2954
|
+
eye: aitken(seed.eye, depths.eye, solved.eye, eyePlateCap),
|
|
2673
2955
|
nose: aitken(
|
|
2674
2956
|
seed.nose,
|
|
2675
2957
|
depths.nose,
|
|
2676
2958
|
solved.nose,
|
|
2677
|
-
|
|
2959
|
+
plateCapOf(ctx.landmarks.nose)
|
|
2678
2960
|
),
|
|
2679
2961
|
mouth: aitken(
|
|
2680
2962
|
seed.mouth,
|
|
2681
2963
|
depths.mouth,
|
|
2682
2964
|
solved.mouth,
|
|
2683
|
-
|
|
2965
|
+
plateCapOf(ctx.landmarks.mouth)
|
|
2684
2966
|
)
|
|
2685
2967
|
};
|
|
2686
2968
|
seed = void 0;
|
|
@@ -2699,7 +2981,7 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2699
2981
|
if (silhouetteClamped && !ctx.clampSilhouetteRatio) {
|
|
2700
2982
|
return silhouetteMiss(span);
|
|
2701
2983
|
}
|
|
2702
|
-
if (!eye.reached && !ctx.clampEyeShift) {
|
|
2984
|
+
if (!eye.reached && eye.limit === "floor" && !ctx.clampEyeShift) {
|
|
2703
2985
|
return {
|
|
2704
2986
|
blocked: "eyeShift",
|
|
2705
2987
|
// eyeShift is a MAGNITUDE (the sign is ignored — see TurnTargets), so
|
|
@@ -2712,9 +2994,24 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2712
2994
|
};
|
|
2713
2995
|
}
|
|
2714
2996
|
const eyeShiftPx = -eye.depth * unit;
|
|
2715
|
-
const scaleOf = (l) => (landmarkLandingX(
|
|
2997
|
+
const scaleOf = (l) => (landmarkLandingX(
|
|
2998
|
+
l,
|
|
2999
|
+
mapAt,
|
|
3000
|
+
-HEAD_TURN_MAX_DEG,
|
|
3001
|
+
eyeShiftPx,
|
|
3002
|
+
l.x + l.w / 2,
|
|
3003
|
+
l.y ?? 0
|
|
3004
|
+
) - landmarkLandingX(
|
|
3005
|
+
l,
|
|
3006
|
+
mapAt,
|
|
3007
|
+
-HEAD_TURN_MAX_DEG,
|
|
3008
|
+
eyeShiftPx,
|
|
3009
|
+
l.x - l.w / 2,
|
|
3010
|
+
l.y ?? 0
|
|
3011
|
+
)) / l.w;
|
|
2716
3012
|
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
2717
3013
|
const near = ctx.landmarks.eye.reduce((a, b) => b.x > a.x ? b : a);
|
|
3014
|
+
const strandOverlap = strandSides === void 0 ? void 0 : strandOverlapOf(strandSides, eye.depth);
|
|
2718
3015
|
return {
|
|
2719
3016
|
radius,
|
|
2720
3017
|
ratio: scaleOf(far) / scaleOf(near),
|
|
@@ -2729,7 +3026,9 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2729
3026
|
mouth: features.mouth,
|
|
2730
3027
|
silhouetteClamped,
|
|
2731
3028
|
silhouetteCenterShift,
|
|
2732
|
-
renderedSilhouetteRatio
|
|
3029
|
+
renderedSilhouetteRatio,
|
|
3030
|
+
strandFeasible,
|
|
3031
|
+
...strandOverlap === void 0 ? {} : { strandOverlap }
|
|
2733
3032
|
};
|
|
2734
3033
|
}
|
|
2735
3034
|
}
|
|
@@ -2766,6 +3065,9 @@ function sweepTurnRadii(ctx) {
|
|
|
2766
3065
|
unsettled
|
|
2767
3066
|
};
|
|
2768
3067
|
}
|
|
3068
|
+
function runHolds(run, ratio) {
|
|
3069
|
+
return ratio >= run.ratios[0] - TURN_DEPTH_EPS && ratio <= run.ratios[1] + TURN_DEPTH_EPS;
|
|
3070
|
+
}
|
|
2769
3071
|
function ratioClusters(candidates) {
|
|
2770
3072
|
const clusters = [];
|
|
2771
3073
|
let run = [];
|
|
@@ -2788,7 +3090,7 @@ function ratioClusters(candidates) {
|
|
|
2788
3090
|
close();
|
|
2789
3091
|
return clusters;
|
|
2790
3092
|
}
|
|
2791
|
-
function fitTurnRadius(ctx, candidates, target) {
|
|
3093
|
+
function fitTurnRadius(ctx, candidates, target, accept) {
|
|
2792
3094
|
let lo = candidates[0];
|
|
2793
3095
|
let hi = candidates[candidates.length - 1];
|
|
2794
3096
|
for (let i = candidates.length - 2; i >= 0; i--) {
|
|
@@ -2798,13 +3100,42 @@ function fitTurnRadius(ctx, candidates, target) {
|
|
|
2798
3100
|
break;
|
|
2799
3101
|
}
|
|
2800
3102
|
}
|
|
3103
|
+
return bisectTurnRadius(
|
|
3104
|
+
lo,
|
|
3105
|
+
hi,
|
|
3106
|
+
target,
|
|
3107
|
+
(radius) => {
|
|
3108
|
+
const mid = evaluateTurnCandidate(ctx, radius);
|
|
3109
|
+
return "blocked" in mid ? void 0 : mid;
|
|
3110
|
+
},
|
|
3111
|
+
accept
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
function bisectTurnRadius(lo, hi, target, evaluate, accept) {
|
|
3115
|
+
if (!accept(lo) || !accept(hi)) {
|
|
3116
|
+
throw new Error(
|
|
3117
|
+
"auto-rig: bisectTurnRadius: a bracket end is not accepted"
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
const miss = (c) => Math.abs(c.ratio - target);
|
|
3121
|
+
let nearest = miss(lo) <= miss(hi) ? lo : hi;
|
|
2801
3122
|
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2802
|
-
const mid =
|
|
2803
|
-
if (
|
|
3123
|
+
const mid = evaluate(Math.sqrt(lo.radius * hi.radius));
|
|
3124
|
+
if (mid === void 0) break;
|
|
3125
|
+
if (accept(mid) && miss(mid) < miss(nearest)) nearest = mid;
|
|
2804
3126
|
if ((mid.ratio - target) * (lo.ratio - target) <= 0) hi = mid;
|
|
2805
3127
|
else lo = mid;
|
|
2806
3128
|
}
|
|
2807
|
-
|
|
3129
|
+
if (accept(lo) && accept(hi)) return miss(lo) <= miss(hi) ? lo : hi;
|
|
3130
|
+
return nearest;
|
|
3131
|
+
}
|
|
3132
|
+
function strandPreferredCandidates(candidates, farEyeRatio) {
|
|
3133
|
+
const feasible = candidates.filter((c) => c.strandFeasible);
|
|
3134
|
+
if (feasible.length === candidates.length) return candidates;
|
|
3135
|
+
const holds = ratioClusters(feasible).some(
|
|
3136
|
+
(run) => runHolds(run, farEyeRatio)
|
|
3137
|
+
);
|
|
3138
|
+
return holds ? feasible : candidates;
|
|
2808
3139
|
}
|
|
2809
3140
|
function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
2810
3141
|
const solve = (measured, marks, share) => solveTurnDepthSigned(
|
|
@@ -2819,13 +3150,38 @@ function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
|
2819
3150
|
mouth: solve(ctx.mouthShift, ctx.landmarks.mouth, MOUTH_SHIFT_SHARE)
|
|
2820
3151
|
};
|
|
2821
3152
|
}
|
|
2822
|
-
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges) {
|
|
3153
|
+
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges, strandEdges) {
|
|
2823
3154
|
if (targets.headHalfWidth !== void 0 && targets.headHalfWidth <= faceHalfWidth) {
|
|
2824
3155
|
throw new TurnTargetError(
|
|
2825
3156
|
`auto-rig: turnTargets.headHalfWidth (${targets.headHalfWidth}) must be wider than the face plate's own half-width (${faceHalfWidth})`
|
|
2826
3157
|
);
|
|
2827
3158
|
}
|
|
2828
3159
|
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
3160
|
+
let strands;
|
|
3161
|
+
const strandSides = ["left", "right"].filter(
|
|
3162
|
+
(key) => strandEdges?.[key] !== void 0
|
|
3163
|
+
);
|
|
3164
|
+
if (strandSides.length > 0) {
|
|
3165
|
+
if (hairFront === void 0) {
|
|
3166
|
+
throw new Error(
|
|
3167
|
+
"auto-rig: solveTurnModel: strandEdges was given, but there is no hair_front to land its runs on"
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
const irises = ["iris_L", "iris_R"].map((role) => carriers.get(role)).filter((c) => c !== void 0).sort((a, b) => a.part.x - b.part.x);
|
|
3171
|
+
strands = strandSides.map((key) => {
|
|
3172
|
+
const carrier = irises.length === 2 ? irises[key === "left" ? 0 : 1] : void 0;
|
|
3173
|
+
if (carrier === void 0) {
|
|
3174
|
+
throw new Error(
|
|
3175
|
+
`auto-rig: solveTurnModel: strandEdges.${key} has no iris carrier to land`
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
return {
|
|
3179
|
+
...strandEdges[key],
|
|
3180
|
+
side: key === "left" ? -1 : 1,
|
|
3181
|
+
carrier
|
|
3182
|
+
};
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
2829
3185
|
const ctx = {
|
|
2830
3186
|
landmarks,
|
|
2831
3187
|
lattice,
|
|
@@ -2857,9 +3213,9 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
|
|
|
2857
3213
|
clampSilhouetteRatio: targets.defaulted.has("silhouetteRatio"),
|
|
2858
3214
|
hairFrontSilhouette: hairFront,
|
|
2859
3215
|
// A companion to a MEASURED headHalfWidth only — see its own doc.
|
|
2860
|
-
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges
|
|
3216
|
+
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges,
|
|
3217
|
+
strands
|
|
2861
3218
|
};
|
|
2862
|
-
const clamped = [];
|
|
2863
3219
|
const clampInto = (value, [lo, hi]) => Math.min(Math.max(value, lo), hi);
|
|
2864
3220
|
const pass = sweepTurnRadii(ctx);
|
|
2865
3221
|
if (pass.candidates.length === 0) {
|
|
@@ -2890,92 +3246,107 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
|
|
|
2890
3246
|
attainable: pass.heldRatioLower === Infinity || pass.heldRatioUpper === -Infinity ? void 0 : [pass.heldRatioLower, pass.heldRatioUpper]
|
|
2891
3247
|
};
|
|
2892
3248
|
}
|
|
2893
|
-
const
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
let cluster = clusters.find(
|
|
2901
|
-
(c) => farEyeRatio >= c.ratios[0] - TURN_DEPTH_EPS && farEyeRatio <= c.ratios[1] + TURN_DEPTH_EPS
|
|
2902
|
-
);
|
|
2903
|
-
if (cluster === void 0) {
|
|
2904
|
-
cluster = clusters.reduce(
|
|
2905
|
-
(a, b) => distanceTo(b) < distanceTo(a) ? b : a
|
|
3249
|
+
const fitFrom = (pool, accept) => {
|
|
3250
|
+
const clamped = [];
|
|
3251
|
+
const clusters = ratioClusters(pool);
|
|
3252
|
+
const distanceTo = (c) => Math.max(
|
|
3253
|
+
c.ratios[0] - targets.farEyeRatio,
|
|
3254
|
+
targets.farEyeRatio - c.ratios[1],
|
|
3255
|
+
0
|
|
2906
3256
|
);
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3257
|
+
let farEyeRatio = targets.farEyeRatio;
|
|
3258
|
+
let cluster = clusters.find((c) => runHolds(c, farEyeRatio));
|
|
3259
|
+
if (cluster === void 0) {
|
|
3260
|
+
cluster = clusters.reduce(
|
|
3261
|
+
(a, b) => distanceTo(b) < distanceTo(a) ? b : a
|
|
3262
|
+
);
|
|
3263
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3264
|
+
return {
|
|
3265
|
+
unreachable: true,
|
|
3266
|
+
field: "farEyeRatio",
|
|
3267
|
+
value: targets.farEyeRatio,
|
|
3268
|
+
attainable: cluster.ratios
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
farEyeRatio = clampInto(farEyeRatio, cluster.ratios);
|
|
3272
|
+
clamped.push("farEyeRatio");
|
|
3273
|
+
}
|
|
3274
|
+
const best = fitTurnRadius(ctx, cluster.candidates, farEyeRatio, accept);
|
|
3275
|
+
if (Math.abs(best.ratio - farEyeRatio) > TURN_DEPTH_EPS) {
|
|
3276
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3277
|
+
return {
|
|
3278
|
+
unreachable: true,
|
|
3279
|
+
field: "farEyeRatio",
|
|
3280
|
+
value: targets.farEyeRatio,
|
|
3281
|
+
attainable: cluster.ratios
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
if (!clamped.includes("farEyeRatio")) clamped.push("farEyeRatio");
|
|
3285
|
+
}
|
|
3286
|
+
if (!best.eye.reached) clamped.push("eyeShift");
|
|
3287
|
+
if (best.silhouetteClamped) clamped.push("silhouetteRatio");
|
|
3288
|
+
const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;
|
|
3289
|
+
for (const [field, solution] of [
|
|
3290
|
+
["noseShift", best.nose],
|
|
3291
|
+
["mouthShift", best.mouth]
|
|
3292
|
+
]) {
|
|
3293
|
+
if (solution.reached) continue;
|
|
3294
|
+
if (!targets.defaulted.has(field)) {
|
|
3295
|
+
return {
|
|
3296
|
+
unreachable: true,
|
|
3297
|
+
field,
|
|
3298
|
+
value: targets[field],
|
|
3299
|
+
// noseShift/mouthShift are MAGNITUDES too (see
|
|
3300
|
+
// solveFeatureDepths), so a negative lower bound here would
|
|
3301
|
+
// advertise a value the field's own contract already rules out; 0
|
|
3302
|
+
// is the true floor.
|
|
3303
|
+
attainable: [
|
|
3304
|
+
Math.max(
|
|
3305
|
+
0,
|
|
3306
|
+
(-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh
|
|
3307
|
+
),
|
|
3308
|
+
(-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh
|
|
3309
|
+
]
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
clamped.push(field);
|
|
2927
3313
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3314
|
+
return {
|
|
3315
|
+
unreachable: false,
|
|
3316
|
+
radius: best.radius,
|
|
3317
|
+
holdBase: best.holdBase,
|
|
3318
|
+
holdEdgeAt: best.holdEdgeAt,
|
|
3319
|
+
travel: best.travel,
|
|
3320
|
+
surface: best.surface,
|
|
3321
|
+
depths: {
|
|
3322
|
+
eye: best.eye.depth,
|
|
3323
|
+
nose: best.nose.depth,
|
|
3324
|
+
mouth: best.mouth.depth
|
|
3325
|
+
},
|
|
3326
|
+
achieved: {
|
|
3327
|
+
eyeShift,
|
|
3328
|
+
farEyeRatio: best.ratio,
|
|
3329
|
+
silhouetteRatio: best.renderedSilhouetteRatio
|
|
3330
|
+
},
|
|
3331
|
+
clamped,
|
|
3332
|
+
...best.strandOverlap === void 0 ? {} : { strandOverlap: best.strandOverlap }
|
|
3333
|
+
};
|
|
3334
|
+
};
|
|
3335
|
+
const preferred = strandPreferredCandidates(
|
|
3336
|
+
pass.candidates,
|
|
3337
|
+
targets.farEyeRatio
|
|
3338
|
+
);
|
|
3339
|
+
if (preferred !== pass.candidates) {
|
|
3340
|
+
const fitted = fitFrom(preferred, (c) => c.strandFeasible);
|
|
3341
|
+
if (!fitted.unreachable && !fitted.clamped.includes("farEyeRatio")) {
|
|
3342
|
+
return fitted;
|
|
2954
3343
|
}
|
|
2955
|
-
clamped.push(field);
|
|
2956
3344
|
}
|
|
2957
|
-
return
|
|
2958
|
-
unreachable: false,
|
|
2959
|
-
radius: best.radius,
|
|
2960
|
-
holdBase: best.holdBase,
|
|
2961
|
-
holdEdgeAt: best.holdEdgeAt,
|
|
2962
|
-
travel: best.travel,
|
|
2963
|
-
surface: best.surface,
|
|
2964
|
-
depths: {
|
|
2965
|
-
eye: best.eye.depth,
|
|
2966
|
-
nose: best.nose.depth,
|
|
2967
|
-
mouth: best.mouth.depth
|
|
2968
|
-
},
|
|
2969
|
-
achieved: {
|
|
2970
|
-
eyeShift,
|
|
2971
|
-
farEyeRatio: best.ratio,
|
|
2972
|
-
silhouetteRatio: best.renderedSilhouetteRatio
|
|
2973
|
-
},
|
|
2974
|
-
clamped
|
|
2975
|
-
};
|
|
3345
|
+
return fitFrom(pass.candidates, () => true);
|
|
2976
3346
|
}
|
|
2977
3347
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
2978
3348
|
var HAIR_FRONT_DEPTH = 0.1;
|
|
3349
|
+
var MOUTH_TURN_TILT_DEG = 5;
|
|
2979
3350
|
var NOD_TRAVEL = 30;
|
|
2980
3351
|
var NOD_BEND = 0.5;
|
|
2981
3352
|
var HAIR_BACK_NOD_DEPTH = -0.135;
|
|
@@ -3582,10 +3953,16 @@ function turnSetup(layers) {
|
|
|
3582
3953
|
)
|
|
3583
3954
|
);
|
|
3584
3955
|
}
|
|
3956
|
+
const mouthAt = members.find((m) => m.role === "mouth").transform;
|
|
3957
|
+
const mouthAnchor = {
|
|
3958
|
+
x: mouthAt.x,
|
|
3959
|
+
y: mouthAt.y,
|
|
3960
|
+
tiltDeg: MOUTH_TURN_TILT_DEG
|
|
3961
|
+
};
|
|
3585
3962
|
const carriers = new Map(
|
|
3586
3963
|
members.map((m) => [
|
|
3587
3964
|
m.role,
|
|
3588
|
-
{ grid: groupGrids.get(m.group), part: m.part }
|
|
3965
|
+
m.group === "mouthWarp" ? { grid: groupGrids.get(m.group), part: m.part, anchor: mouthAnchor } : { grid: groupGrids.get(m.group), part: m.part }
|
|
3589
3966
|
])
|
|
3590
3967
|
);
|
|
3591
3968
|
const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
|
|
@@ -3656,6 +4033,7 @@ function solveInputsOf(s) {
|
|
|
3656
4033
|
}
|
|
3657
4034
|
function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
3658
4035
|
validateLayerInputs(layers, canvas);
|
|
4036
|
+
validateStrandEdges(layers, options.strandEdges);
|
|
3659
4037
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
3660
4038
|
const bodyLayer = layers.find((l) => l.role === "body");
|
|
3661
4039
|
const parameters = [...STANDARD_PARAMETERS];
|
|
@@ -3691,6 +4069,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3691
4069
|
plateGuardRows,
|
|
3692
4070
|
members,
|
|
3693
4071
|
groupGrids,
|
|
4072
|
+
carriers,
|
|
3694
4073
|
parallaxUnitY,
|
|
3695
4074
|
hasNose,
|
|
3696
4075
|
hasMouthOpen
|
|
@@ -3699,7 +4078,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3699
4078
|
const turn = hasNose ? solveTurnModel(
|
|
3700
4079
|
resolveTurnTargets(options.turnTargets),
|
|
3701
4080
|
...solveInputsOf(setup),
|
|
3702
|
-
options.headEdges
|
|
4081
|
+
options.headEdges,
|
|
4082
|
+
options.strandEdges
|
|
3703
4083
|
) : void 0;
|
|
3704
4084
|
if (turn?.unreachable) {
|
|
3705
4085
|
throw new TurnTargetError(
|
|
@@ -3717,7 +4097,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3717
4097
|
holdBase: turn.holdBase,
|
|
3718
4098
|
depths: turn.depths,
|
|
3719
4099
|
achieved: turn.achieved,
|
|
3720
|
-
clamped: turn.clamped
|
|
4100
|
+
clamped: turn.clamped,
|
|
4101
|
+
...turn.strandOverlap === void 0 ? {} : { strandOverlap: turn.strandOverlap }
|
|
3721
4102
|
});
|
|
3722
4103
|
}
|
|
3723
4104
|
const latticeReach = Math.max(
|
|
@@ -3790,7 +4171,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3790
4171
|
}
|
|
3791
4172
|
];
|
|
3792
4173
|
for (const [group, grid] of groupGrids) {
|
|
3793
|
-
const
|
|
4174
|
+
const { role } = members.find((m) => m.group === group);
|
|
4175
|
+
const family = turnFamily(role);
|
|
3794
4176
|
const depth = group === "faceWarp" ? 0 : turn?.depths[family] ?? 0;
|
|
3795
4177
|
deformers.push({
|
|
3796
4178
|
kind: "warp",
|
|
@@ -3802,7 +4184,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3802
4184
|
import_format3.StandardParameter.AngleX,
|
|
3803
4185
|
import_format3.StandardParameter.AngleY,
|
|
3804
4186
|
surface,
|
|
3805
|
-
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG
|
|
4187
|
+
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG,
|
|
4188
|
+
carriers.get(role).anchor
|
|
3806
4189
|
)
|
|
3807
4190
|
});
|
|
3808
4191
|
}
|