@ikijs/editor 0.6.0 → 0.8.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 +40 -7
- package/dist/index.d.mts +131 -6
- package/dist/index.d.ts +131 -6
- package/dist/index.js +590 -142
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +590 -142
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1837,6 +1837,164 @@ function validateLayerInputs(layers, canvas) {
|
|
|
1837
1837
|
}
|
|
1838
1838
|
}
|
|
1839
1839
|
}
|
|
1840
|
+
if (layer.denseCore !== void 0) {
|
|
1841
|
+
const where = `auto-rig: validateLayerInputs: role "${role}" denseCore`;
|
|
1842
|
+
const core = layer.denseCore;
|
|
1843
|
+
if (typeof core !== "object" || core === null || Array.isArray(core)) {
|
|
1844
|
+
throw new Error(`${where} must be a plain object { x, y, w, h }`);
|
|
1845
|
+
}
|
|
1846
|
+
for (const field of ["x", "y", "w", "h"]) {
|
|
1847
|
+
const v = core[field];
|
|
1848
|
+
const size = field === "w" || field === "h";
|
|
1849
|
+
if (typeof v !== "number" || !Number.isFinite(v) || size && v <= 0) {
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
`${where}.${field} is ${String(v)}, not a finite number${size ? " > 0" : ""}`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
const { x, y, w, h } = layer.denseCore;
|
|
1856
|
+
const right = bbox.x + bbox.w;
|
|
1857
|
+
const bottom = bbox.y + bbox.h;
|
|
1858
|
+
if (x < bbox.x) {
|
|
1859
|
+
throw new Error(
|
|
1860
|
+
`${where}.x (${x}) lies left of the bbox's left edge (${bbox.x})`
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
if (y < bbox.y) {
|
|
1864
|
+
throw new Error(
|
|
1865
|
+
`${where}.y (${y}) lies above the bbox's top edge (${bbox.y})`
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
if (x + w > right) {
|
|
1869
|
+
throw new Error(
|
|
1870
|
+
`${where}.w (${w}) puts its right edge at ${x + w}, past the bbox's right edge (${right})`
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
if (y + h > bottom) {
|
|
1874
|
+
throw new Error(
|
|
1875
|
+
`${where}.h (${h}) puts its bottom edge at ${y + h}, past the bbox's bottom edge (${bottom})`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
function validateStrandEdges(layers, strandEdges) {
|
|
1882
|
+
if (strandEdges === void 0) return;
|
|
1883
|
+
const where = "auto-rig: validateStrandEdges: strandEdges";
|
|
1884
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1885
|
+
if (!isPlainObject(strandEdges)) {
|
|
1886
|
+
throw new Error(
|
|
1887
|
+
`${where} must be a plain object with optional left / right sides`
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
for (const key of ["left", "right"]) {
|
|
1891
|
+
const raw = strandEdges[key];
|
|
1892
|
+
if (raw === void 0) continue;
|
|
1893
|
+
const path = `${where}.${key}`;
|
|
1894
|
+
if (!isPlainObject(raw)) {
|
|
1895
|
+
throw new Error(`${path} must be a plain object`);
|
|
1896
|
+
}
|
|
1897
|
+
for (const field of ["y", "irisOuter", "irisInner", "runOuter"]) {
|
|
1898
|
+
const v = raw[field];
|
|
1899
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1900
|
+
throw new Error(
|
|
1901
|
+
`${path}.${field} is ${String(v)}, not a finite number`
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
const rawFace = raw.runFace;
|
|
1906
|
+
if (rawFace !== null && (typeof rawFace !== "number" || !Number.isFinite(rawFace))) {
|
|
1907
|
+
throw new Error(
|
|
1908
|
+
`${path}.runFace is ${String(rawFace)}, not a finite number or null`
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
const { y, irisOuter, irisInner, runOuter, runFace } = raw;
|
|
1912
|
+
const cropOf = (role) => {
|
|
1913
|
+
const layer = layers.find((l) => l.role === role);
|
|
1914
|
+
if (layer === void 0) {
|
|
1915
|
+
throw new Error(
|
|
1916
|
+
`${path} needs a "${role}" layer to be measured on, and this layer set has none`
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
1920
|
+
return {
|
|
1921
|
+
role,
|
|
1922
|
+
x: t.x,
|
|
1923
|
+
left: t.x - layer.cropW / 2,
|
|
1924
|
+
right: t.x + layer.cropW / 2,
|
|
1925
|
+
bottom: t.y - layer.cropH / 2,
|
|
1926
|
+
top: t.y + layer.cropH / 2
|
|
1927
|
+
};
|
|
1928
|
+
};
|
|
1929
|
+
const hair = cropOf("hair_front");
|
|
1930
|
+
const [lowIris, highIris] = [cropOf("iris_L"), cropOf("iris_R")].sort(
|
|
1931
|
+
(a, b) => a.x - b.x
|
|
1932
|
+
);
|
|
1933
|
+
const sign = key === "left" ? -1 : 1;
|
|
1934
|
+
const iris = key === "left" ? lowIris : highIris;
|
|
1935
|
+
const other = key === "left" ? highIris : lowIris;
|
|
1936
|
+
const fail = (field, value, why) => new Error(`${path}.${field} (${value}) ${why}`);
|
|
1937
|
+
for (const crop of [iris, hair]) {
|
|
1938
|
+
if (y < crop.bottom || y > crop.top) {
|
|
1939
|
+
throw fail(
|
|
1940
|
+
"y",
|
|
1941
|
+
y,
|
|
1942
|
+
`lies outside the "${crop.role}" crop's rows (${crop.bottom}\u2026${crop.top})`
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
for (const [field, x] of [
|
|
1947
|
+
["irisOuter", irisOuter],
|
|
1948
|
+
["irisInner", irisInner]
|
|
1949
|
+
]) {
|
|
1950
|
+
if (x < iris.left || x > iris.right) {
|
|
1951
|
+
throw fail(
|
|
1952
|
+
field,
|
|
1953
|
+
x,
|
|
1954
|
+
`lies outside the "${iris.role}" crop's columns (${iris.left}\u2026${iris.right})`
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (sign * (irisOuter - irisInner) <= 0) {
|
|
1959
|
+
throw fail(
|
|
1960
|
+
"irisOuter",
|
|
1961
|
+
irisOuter,
|
|
1962
|
+
`must lie strictly outward of strandEdges.${key}.irisInner (${irisInner})`
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
for (const [field, x] of [
|
|
1966
|
+
["runOuter", runOuter],
|
|
1967
|
+
["runFace", runFace]
|
|
1968
|
+
]) {
|
|
1969
|
+
if (x !== null && (x < hair.left || x > hair.right)) {
|
|
1970
|
+
throw fail(
|
|
1971
|
+
field,
|
|
1972
|
+
x,
|
|
1973
|
+
`lies outside the "hair_front" crop's columns (${hair.left}\u2026${hair.right})`
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (sign * (runOuter - iris.x) <= 0) {
|
|
1978
|
+
throw fail(
|
|
1979
|
+
"runOuter",
|
|
1980
|
+
runOuter,
|
|
1981
|
+
`must lie strictly outward of the "${iris.role}" crop's centre (${iris.x})`
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
if (runFace !== null && sign * (runOuter - runFace) <= 0) {
|
|
1985
|
+
throw fail(
|
|
1986
|
+
"runOuter",
|
|
1987
|
+
runOuter,
|
|
1988
|
+
`must lie strictly outward of strandEdges.${key}.runFace (${runFace})`
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
if (runFace !== null && sign * (runFace - other.x) <= 0) {
|
|
1992
|
+
throw fail(
|
|
1993
|
+
"runFace",
|
|
1994
|
+
runFace,
|
|
1995
|
+
`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`
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1840
1998
|
}
|
|
1841
1999
|
}
|
|
1842
2000
|
function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
@@ -1995,21 +2153,26 @@ function turnSurface(spec) {
|
|
|
1995
2153
|
)
|
|
1996
2154
|
};
|
|
1997
2155
|
}
|
|
1998
|
-
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt) {
|
|
2156
|
+
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt, anchor) {
|
|
1999
2157
|
const STOPS = [...HEAD_TURN_STOPS];
|
|
2000
2158
|
const pointCount = grid.points.length / 2;
|
|
2001
2159
|
const keyforms2d = [];
|
|
2002
2160
|
for (const angleY of STOPS) {
|
|
2003
2161
|
for (const angleX of STOPS) {
|
|
2004
2162
|
const shift = shiftAt(angleX);
|
|
2163
|
+
const mapAt = (nodeY) => surface.mapAt(angleX, nodeY);
|
|
2005
2164
|
const offsets = [];
|
|
2006
2165
|
for (let i = 0; i < pointCount; i++) {
|
|
2007
2166
|
const x = grid.points[i * 2];
|
|
2008
2167
|
const y = grid.points[i * 2 + 1];
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2168
|
+
const nodDy = angleY === 0 ? 0 : surface.nodBendAt(y, angleY);
|
|
2169
|
+
if (angleX === 0) {
|
|
2170
|
+
offsets.push(0, nodDy);
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
const landed = groupNodeLanding(anchor, mapAt, angleX, shift, x, y);
|
|
2174
|
+
const turnDy = landed.y - y;
|
|
2175
|
+
offsets.push(landed.x - x, angleY === 0 ? turnDy : turnDy + nodDy);
|
|
2013
2176
|
}
|
|
2014
2177
|
keyforms2d.push({ offsets });
|
|
2015
2178
|
}
|
|
@@ -2129,6 +2292,22 @@ function carrierLandingX(carrier, nodeLanding, px, py) {
|
|
|
2129
2292
|
py
|
|
2130
2293
|
);
|
|
2131
2294
|
}
|
|
2295
|
+
function groupNodeLanding(anchor, mapAt, deg, shift, nodeX, nodeY) {
|
|
2296
|
+
if (anchor === void 0) {
|
|
2297
|
+
return { x: mapAt(nodeY).mapX(nodeX + shift), y: nodeY };
|
|
2298
|
+
}
|
|
2299
|
+
const anchorRow = mapAt(anchor.y);
|
|
2300
|
+
const pivotX = anchorRow.mapX(anchor.x + shift);
|
|
2301
|
+
const dx = mapAt(nodeY).mapX(nodeX) - anchorRow.mapX(anchor.x);
|
|
2302
|
+
const dy = nodeY - anchor.y;
|
|
2303
|
+
const phi = anchor.tiltDeg * deg / HEAD_TURN_MAX_DEG * (Math.PI / 180);
|
|
2304
|
+
const cos = Math.cos(phi);
|
|
2305
|
+
const sin = Math.sin(phi);
|
|
2306
|
+
return {
|
|
2307
|
+
x: pivotX + cos * dx - sin * dy,
|
|
2308
|
+
y: anchor.y + sin * dx + cos * dy
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2132
2311
|
function rowMapsOf(surface) {
|
|
2133
2312
|
const maps = /* @__PURE__ */ new Map();
|
|
2134
2313
|
return (deg, nodeY) => {
|
|
@@ -2251,12 +2430,17 @@ function resolveTurnTargets(targets = {}) {
|
|
|
2251
2430
|
defaulted
|
|
2252
2431
|
};
|
|
2253
2432
|
}
|
|
2433
|
+
function noseDrawnBox(layer) {
|
|
2434
|
+
return layer.denseCore ?? layer.bbox;
|
|
2435
|
+
}
|
|
2254
2436
|
function turnLandmarks(layers, carriers) {
|
|
2255
2437
|
const markOf = (role) => {
|
|
2256
2438
|
const layer = layers.find((l) => l.role === role);
|
|
2257
2439
|
if (!layer) return void 0;
|
|
2258
|
-
const
|
|
2259
|
-
const
|
|
2440
|
+
const drawn = role === "nose" ? noseDrawnBox(layer) : layer.bbox;
|
|
2441
|
+
const t = bboxToTransform(drawn, layer.canvasW, layer.canvasH, role);
|
|
2442
|
+
const w = drawn === layer.bbox ? layer.cropW : drawn.w;
|
|
2443
|
+
const mark = { x: t.x, y: t.y, w };
|
|
2260
2444
|
const carrier = carriers?.get(role);
|
|
2261
2445
|
if (carrier !== void 0) mark.carrier = carrier;
|
|
2262
2446
|
return mark;
|
|
@@ -2282,11 +2466,12 @@ function eyeRowOf(landmarks, fallback) {
|
|
|
2282
2466
|
}
|
|
2283
2467
|
var TURN_BISECT_STEPS = 40;
|
|
2284
2468
|
var TURN_DEPTH_EPS = 1e-6;
|
|
2285
|
-
function landmarkLandingX(landmark, mapAt, shift, px, py) {
|
|
2286
|
-
|
|
2469
|
+
function landmarkLandingX(landmark, mapAt, deg, shift, px, py) {
|
|
2470
|
+
const { carrier } = landmark;
|
|
2471
|
+
if (carrier === void 0) return mapAt(py).mapX(px + shift);
|
|
2287
2472
|
return carrierLandingX(
|
|
2288
|
-
|
|
2289
|
-
(nodeX, nodeY) =>
|
|
2473
|
+
carrier,
|
|
2474
|
+
(nodeX, nodeY) => groupNodeLanding(carrier.anchor, mapAt, deg, shift, nodeX, nodeY).x,
|
|
2290
2475
|
px,
|
|
2291
2476
|
py
|
|
2292
2477
|
);
|
|
@@ -2295,22 +2480,47 @@ function familyReachPx(landmarks, plateEdgeX) {
|
|
|
2295
2480
|
const farEdge = Math.min(...landmarks.map((l) => l.x - l.w / 2));
|
|
2296
2481
|
return Math.max(0, farEdge - plateEdgeX);
|
|
2297
2482
|
}
|
|
2298
|
-
function
|
|
2483
|
+
function familyDepthCap(landmarks, plateEdgeX, unit) {
|
|
2484
|
+
return familyReachPx(landmarks, plateEdgeX) / unit;
|
|
2485
|
+
}
|
|
2486
|
+
function solveTurnDepthSigned(target, landmarks, unit, mapAt, plateEdgeX, maxDepth) {
|
|
2299
2487
|
if (landmarks.length === 0) {
|
|
2300
2488
|
throw new Error("auto-rig: solveTurnDepth: no landmark to slide");
|
|
2301
2489
|
}
|
|
2302
|
-
const
|
|
2490
|
+
const plateCap = familyDepthCap(landmarks, plateEdgeX, unit);
|
|
2491
|
+
const cap = maxDepth === void 0 ? plateCap : Math.min(plateCap, maxDepth);
|
|
2303
2492
|
const achieved = (d) => landmarks.reduce(
|
|
2304
|
-
(sum, l) => sum + (landmarkLandingX(
|
|
2493
|
+
(sum, l) => sum + (landmarkLandingX(
|
|
2494
|
+
l,
|
|
2495
|
+
mapAt,
|
|
2496
|
+
-HEAD_TURN_MAX_DEG,
|
|
2497
|
+
-d * unit,
|
|
2498
|
+
l.x,
|
|
2499
|
+
l.y ?? 0
|
|
2500
|
+
) - l.x),
|
|
2305
2501
|
0
|
|
2306
2502
|
) / landmarks.length;
|
|
2307
2503
|
const atCap = achieved(cap);
|
|
2308
2504
|
const atRest = achieved(0);
|
|
2309
2505
|
const attainable = [atCap, atRest];
|
|
2310
|
-
if (target > atRest + TURN_DEPTH_EPS)
|
|
2311
|
-
return {
|
|
2312
|
-
|
|
2313
|
-
|
|
2506
|
+
if (target > atRest + TURN_DEPTH_EPS) {
|
|
2507
|
+
return {
|
|
2508
|
+
reached: false,
|
|
2509
|
+
depth: 0,
|
|
2510
|
+
achieved: atRest,
|
|
2511
|
+
attainable,
|
|
2512
|
+
limit: "floor"
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
if (target < atCap - TURN_DEPTH_EPS) {
|
|
2516
|
+
return {
|
|
2517
|
+
reached: false,
|
|
2518
|
+
depth: cap,
|
|
2519
|
+
achieved: atCap,
|
|
2520
|
+
attainable,
|
|
2521
|
+
limit: "cap"
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2314
2524
|
const clampedTarget = Math.min(Math.max(target, atCap), atRest);
|
|
2315
2525
|
let lo = 0;
|
|
2316
2526
|
let hi = cap;
|
|
@@ -2340,7 +2550,7 @@ function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, hol
|
|
|
2340
2550
|
}
|
|
2341
2551
|
return x + side * (holdEdge - holdBase);
|
|
2342
2552
|
}
|
|
2343
|
-
function hairFrontLandingAt(x, y, hairFront, holdTargetAt,
|
|
2553
|
+
function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadShift) {
|
|
2344
2554
|
const { cols, rows } = meshCellsFor(hairFront.cropW, hairFront.cropH);
|
|
2345
2555
|
const top = hairFront.centerY + hairFront.cropH / 2;
|
|
2346
2556
|
return renderedLandingX(
|
|
@@ -2355,7 +2565,7 @@ function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadTipShift) {
|
|
|
2355
2565
|
scaleY: 1,
|
|
2356
2566
|
rotation: 0
|
|
2357
2567
|
},
|
|
2358
|
-
(vx, vy) => holdTargetAt(vx, vy)
|
|
2568
|
+
(vx, vy) => holdTargetAt(vx, vy) + leadShift * Math.pow((top - vy) / hairFront.cropH, HAIR_SWAY_CURL),
|
|
2359
2569
|
x,
|
|
2360
2570
|
y
|
|
2361
2571
|
);
|
|
@@ -2388,6 +2598,19 @@ function aitken(x0, x1, x2, cap) {
|
|
|
2388
2598
|
const x = x2 - d2 * d2 / denominator;
|
|
2389
2599
|
return Number.isFinite(x) ? Math.min(cap, Math.max(0, x)) : x2;
|
|
2390
2600
|
}
|
|
2601
|
+
function strandDepthCap(margins, plateCap) {
|
|
2602
|
+
const holds = (depth) => margins.every((m) => m(depth) >= 0);
|
|
2603
|
+
if (holds(plateCap)) return plateCap;
|
|
2604
|
+
if (!holds(0)) return void 0;
|
|
2605
|
+
let lo = 0;
|
|
2606
|
+
let hi = plateCap;
|
|
2607
|
+
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2608
|
+
const mid = (lo + hi) / 2;
|
|
2609
|
+
if (holds(mid)) lo = mid;
|
|
2610
|
+
else hi = mid;
|
|
2611
|
+
}
|
|
2612
|
+
return lo;
|
|
2613
|
+
}
|
|
2391
2614
|
function evaluateTurnCandidate(ctx, radius) {
|
|
2392
2615
|
const surfaceAt = (travel) => turnSurface({
|
|
2393
2616
|
faceCenterX: ctx.faceCenterX,
|
|
@@ -2411,7 +2634,104 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2411
2634
|
return edge === void 0 ? ctx.headHalfWidth : side * (edge - ctx.faceCenterX);
|
|
2412
2635
|
};
|
|
2413
2636
|
const unit = headTurnParallaxUnit(radius);
|
|
2637
|
+
const plateCapOf = (marks) => familyDepthCap(marks, ctx.plateEdgeX, unit);
|
|
2638
|
+
const eyePlateCap = plateCapOf(ctx.landmarks.eye);
|
|
2414
2639
|
const leadTipShift = ctx.hairFrontSilhouette === void 0 ? 0 : HAIR_FRONT_DEPTH * unit;
|
|
2640
|
+
const bangsLandingAt = (hold, plateLandingAt, deg, leadShift, x, y) => hairFrontLandingAt(
|
|
2641
|
+
x,
|
|
2642
|
+
y,
|
|
2643
|
+
ctx.hairFrontSilhouette,
|
|
2644
|
+
(vx, vy) => hairFrontHoldTarget(
|
|
2645
|
+
vx,
|
|
2646
|
+
vy,
|
|
2647
|
+
deg,
|
|
2648
|
+
ctx.faceCenterX,
|
|
2649
|
+
plateLandingAt,
|
|
2650
|
+
ctx.edgeAt,
|
|
2651
|
+
hold.holdBase,
|
|
2652
|
+
hold.holdEdgeAt(deg)
|
|
2653
|
+
),
|
|
2654
|
+
leadShift
|
|
2655
|
+
);
|
|
2656
|
+
const strandStopsOn = (hold) => {
|
|
2657
|
+
const rowMaps = rowMapsOf(hold.surface);
|
|
2658
|
+
const plateLandingAt = plateLandingOn(ctx.plate, rowMaps);
|
|
2659
|
+
return (ctx.strands ?? []).map((strand) => {
|
|
2660
|
+
const iris = {
|
|
2661
|
+
x: strand.carrier.part.x,
|
|
2662
|
+
w: strand.carrier.part.cropW,
|
|
2663
|
+
carrier: strand.carrier
|
|
2664
|
+
};
|
|
2665
|
+
const stops = HEAD_TURN_STOPS.filter(
|
|
2666
|
+
(deg) => Math.sign(deg) === strand.side
|
|
2667
|
+
).map((deg) => {
|
|
2668
|
+
const runAt = (x) => bangsLandingAt(
|
|
2669
|
+
hold,
|
|
2670
|
+
plateLandingAt,
|
|
2671
|
+
deg,
|
|
2672
|
+
leadTipShift * deg / HEAD_TURN_MAX_DEG,
|
|
2673
|
+
x,
|
|
2674
|
+
strand.y
|
|
2675
|
+
);
|
|
2676
|
+
return {
|
|
2677
|
+
deg,
|
|
2678
|
+
runOuter: runAt(strand.runOuter),
|
|
2679
|
+
runFace: strand.runFace === null ? null : runAt(strand.runFace),
|
|
2680
|
+
irisAt: (depth, x) => landmarkLandingX(
|
|
2681
|
+
iris,
|
|
2682
|
+
(y) => rowMaps(deg, y),
|
|
2683
|
+
deg,
|
|
2684
|
+
depth * unit * deg / HEAD_TURN_MAX_DEG,
|
|
2685
|
+
x,
|
|
2686
|
+
strand.y
|
|
2687
|
+
)
|
|
2688
|
+
};
|
|
2689
|
+
});
|
|
2690
|
+
const { runFace } = strand;
|
|
2691
|
+
if (runFace === null) return { strand, stops, margins: void 0 };
|
|
2692
|
+
const allowance = Math.min(0, strand.side * (runFace - strand.irisOuter));
|
|
2693
|
+
const margins = stops.map(
|
|
2694
|
+
(stop) => (depth) => strand.side * (stop.runFace - stop.irisAt(depth, strand.irisOuter)) - allowance
|
|
2695
|
+
);
|
|
2696
|
+
return { strand, stops, margins };
|
|
2697
|
+
});
|
|
2698
|
+
};
|
|
2699
|
+
const strandOverlapOf = (sides, depth) => {
|
|
2700
|
+
const overlap = {};
|
|
2701
|
+
for (const { strand, stops, margins } of sides) {
|
|
2702
|
+
const s = strand.side;
|
|
2703
|
+
const covered = (irisOuter, irisInner, runOuter, runFace) => Math.max(
|
|
2704
|
+
0,
|
|
2705
|
+
Math.min(s * irisOuter, s * runOuter) - Math.max(s * irisInner, runFace === null ? -Infinity : s * runFace)
|
|
2706
|
+
);
|
|
2707
|
+
let worst;
|
|
2708
|
+
for (const stop of stops) {
|
|
2709
|
+
const px = covered(
|
|
2710
|
+
stop.irisAt(depth, strand.irisOuter),
|
|
2711
|
+
stop.irisAt(depth, strand.irisInner),
|
|
2712
|
+
stop.runOuter,
|
|
2713
|
+
stop.runFace
|
|
2714
|
+
);
|
|
2715
|
+
if (px > 0 && (worst === void 0 || px > worst.px)) {
|
|
2716
|
+
worst = { deg: stop.deg, px };
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
if (worst === void 0) continue;
|
|
2720
|
+
overlap[s < 0 ? "left" : "right"] = {
|
|
2721
|
+
deg: worst.deg,
|
|
2722
|
+
px: worst.px,
|
|
2723
|
+
hh: worst.px / ctx.hh,
|
|
2724
|
+
restPx: covered(
|
|
2725
|
+
strand.irisOuter,
|
|
2726
|
+
strand.irisInner,
|
|
2727
|
+
strand.runOuter,
|
|
2728
|
+
strand.runFace
|
|
2729
|
+
),
|
|
2730
|
+
held: margins !== void 0 && margins.every((m) => m(depth) >= 0)
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
return overlap.left === void 0 && overlap.right === void 0 ? void 0 : overlap;
|
|
2734
|
+
};
|
|
2415
2735
|
const evaluateHold = (ratio, depths2) => {
|
|
2416
2736
|
const holdEdgeFrom = (base) => (deg) => base * (1 + (ratio - 1) * Math.abs(deg) / HEAD_TURN_MAX_DEG);
|
|
2417
2737
|
const travel = ctx.headHalfWidth === void 0 ? ctx.travel : Math.min(
|
|
@@ -2461,21 +2781,13 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2461
2781
|
if (edge !== void 0) return edge;
|
|
2462
2782
|
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side * holdBase : ctx.hairFrontSilhouette.x + side * ctx.hairFrontSilhouette.cropW / 2;
|
|
2463
2783
|
};
|
|
2464
|
-
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x :
|
|
2784
|
+
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x : bangsLandingAt(
|
|
2785
|
+
{ holdBase, holdEdgeAt },
|
|
2786
|
+
plateLandingAt,
|
|
2787
|
+
-HEAD_TURN_MAX_DEG,
|
|
2788
|
+
-leadTipShift,
|
|
2465
2789
|
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
|
|
2790
|
+
ctx.eyeRowY
|
|
2479
2791
|
);
|
|
2480
2792
|
const landingOfRole = (role, x) => {
|
|
2481
2793
|
if (role === "hair_front") return hairFrontAt(x);
|
|
@@ -2495,7 +2807,14 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2495
2807
|
const shift = role === "face" ? 0 : -depths2[turnFamily(role)] * unit;
|
|
2496
2808
|
return carrierLandingX(
|
|
2497
2809
|
carrier,
|
|
2498
|
-
(nodeX, nodeY) =>
|
|
2810
|
+
(nodeX, nodeY) => groupNodeLanding(
|
|
2811
|
+
carrier.anchor,
|
|
2812
|
+
mapAt,
|
|
2813
|
+
-HEAD_TURN_MAX_DEG,
|
|
2814
|
+
shift,
|
|
2815
|
+
nodeX,
|
|
2816
|
+
nodeY
|
|
2817
|
+
).x,
|
|
2499
2818
|
x,
|
|
2500
2819
|
ctx.eyeRowY
|
|
2501
2820
|
);
|
|
@@ -2636,12 +2955,25 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2636
2955
|
"auto-rig: evaluateTurnCandidate: the hold it settled on folds"
|
|
2637
2956
|
);
|
|
2638
2957
|
}
|
|
2958
|
+
const strandSides = ctx.strands === void 0 ? void 0 : strandStopsOn(hold);
|
|
2959
|
+
let strandFeasible = true;
|
|
2960
|
+
let maxDepth;
|
|
2961
|
+
if (strandSides !== void 0) {
|
|
2962
|
+
const margins = strandSides.map((s) => s.margins);
|
|
2963
|
+
const cap = margins.every((m) => m !== void 0) ? strandDepthCap(
|
|
2964
|
+
margins.flatMap((m) => m),
|
|
2965
|
+
eyePlateCap
|
|
2966
|
+
) : void 0;
|
|
2967
|
+
strandFeasible = cap !== void 0;
|
|
2968
|
+
maxDepth = cap ?? 0;
|
|
2969
|
+
}
|
|
2639
2970
|
const eye = solveTurnDepthSigned(
|
|
2640
2971
|
hold.silhouetteCenterShift - ctx.eyeShift * ctx.hh,
|
|
2641
2972
|
ctx.landmarks.eye,
|
|
2642
2973
|
unit,
|
|
2643
2974
|
hold.mapAt,
|
|
2644
|
-
ctx.plateEdgeX
|
|
2975
|
+
ctx.plateEdgeX,
|
|
2976
|
+
maxDepth
|
|
2645
2977
|
);
|
|
2646
2978
|
const features = solveFeatureDepths(
|
|
2647
2979
|
ctx,
|
|
@@ -2662,25 +2994,19 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2662
2994
|
seed = depths;
|
|
2663
2995
|
depths = solved;
|
|
2664
2996
|
} else {
|
|
2665
|
-
const capOf = (marks) => familyReachPx(marks, ctx.plateEdgeX) / unit;
|
|
2666
2997
|
depths = {
|
|
2667
|
-
eye: aitken(
|
|
2668
|
-
seed.eye,
|
|
2669
|
-
depths.eye,
|
|
2670
|
-
solved.eye,
|
|
2671
|
-
capOf(ctx.landmarks.eye)
|
|
2672
|
-
),
|
|
2998
|
+
eye: aitken(seed.eye, depths.eye, solved.eye, eyePlateCap),
|
|
2673
2999
|
nose: aitken(
|
|
2674
3000
|
seed.nose,
|
|
2675
3001
|
depths.nose,
|
|
2676
3002
|
solved.nose,
|
|
2677
|
-
|
|
3003
|
+
plateCapOf(ctx.landmarks.nose)
|
|
2678
3004
|
),
|
|
2679
3005
|
mouth: aitken(
|
|
2680
3006
|
seed.mouth,
|
|
2681
3007
|
depths.mouth,
|
|
2682
3008
|
solved.mouth,
|
|
2683
|
-
|
|
3009
|
+
plateCapOf(ctx.landmarks.mouth)
|
|
2684
3010
|
)
|
|
2685
3011
|
};
|
|
2686
3012
|
seed = void 0;
|
|
@@ -2699,7 +3025,7 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2699
3025
|
if (silhouetteClamped && !ctx.clampSilhouetteRatio) {
|
|
2700
3026
|
return silhouetteMiss(span);
|
|
2701
3027
|
}
|
|
2702
|
-
if (!eye.reached && !ctx.clampEyeShift) {
|
|
3028
|
+
if (!eye.reached && eye.limit === "floor" && !ctx.clampEyeShift) {
|
|
2703
3029
|
return {
|
|
2704
3030
|
blocked: "eyeShift",
|
|
2705
3031
|
// eyeShift is a MAGNITUDE (the sign is ignored — see TurnTargets), so
|
|
@@ -2712,9 +3038,24 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2712
3038
|
};
|
|
2713
3039
|
}
|
|
2714
3040
|
const eyeShiftPx = -eye.depth * unit;
|
|
2715
|
-
const scaleOf = (l) => (landmarkLandingX(
|
|
3041
|
+
const scaleOf = (l) => (landmarkLandingX(
|
|
3042
|
+
l,
|
|
3043
|
+
mapAt,
|
|
3044
|
+
-HEAD_TURN_MAX_DEG,
|
|
3045
|
+
eyeShiftPx,
|
|
3046
|
+
l.x + l.w / 2,
|
|
3047
|
+
l.y ?? 0
|
|
3048
|
+
) - landmarkLandingX(
|
|
3049
|
+
l,
|
|
3050
|
+
mapAt,
|
|
3051
|
+
-HEAD_TURN_MAX_DEG,
|
|
3052
|
+
eyeShiftPx,
|
|
3053
|
+
l.x - l.w / 2,
|
|
3054
|
+
l.y ?? 0
|
|
3055
|
+
)) / l.w;
|
|
2716
3056
|
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
2717
3057
|
const near = ctx.landmarks.eye.reduce((a, b) => b.x > a.x ? b : a);
|
|
3058
|
+
const strandOverlap = strandSides === void 0 ? void 0 : strandOverlapOf(strandSides, eye.depth);
|
|
2718
3059
|
return {
|
|
2719
3060
|
radius,
|
|
2720
3061
|
ratio: scaleOf(far) / scaleOf(near),
|
|
@@ -2729,7 +3070,9 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2729
3070
|
mouth: features.mouth,
|
|
2730
3071
|
silhouetteClamped,
|
|
2731
3072
|
silhouetteCenterShift,
|
|
2732
|
-
renderedSilhouetteRatio
|
|
3073
|
+
renderedSilhouetteRatio,
|
|
3074
|
+
strandFeasible,
|
|
3075
|
+
...strandOverlap === void 0 ? {} : { strandOverlap }
|
|
2733
3076
|
};
|
|
2734
3077
|
}
|
|
2735
3078
|
}
|
|
@@ -2766,6 +3109,9 @@ function sweepTurnRadii(ctx) {
|
|
|
2766
3109
|
unsettled
|
|
2767
3110
|
};
|
|
2768
3111
|
}
|
|
3112
|
+
function runHolds(run, ratio) {
|
|
3113
|
+
return ratio >= run.ratios[0] - TURN_DEPTH_EPS && ratio <= run.ratios[1] + TURN_DEPTH_EPS;
|
|
3114
|
+
}
|
|
2769
3115
|
function ratioClusters(candidates) {
|
|
2770
3116
|
const clusters = [];
|
|
2771
3117
|
let run = [];
|
|
@@ -2788,7 +3134,7 @@ function ratioClusters(candidates) {
|
|
|
2788
3134
|
close();
|
|
2789
3135
|
return clusters;
|
|
2790
3136
|
}
|
|
2791
|
-
function fitTurnRadius(ctx, candidates, target) {
|
|
3137
|
+
function fitTurnRadius(ctx, candidates, target, accept) {
|
|
2792
3138
|
let lo = candidates[0];
|
|
2793
3139
|
let hi = candidates[candidates.length - 1];
|
|
2794
3140
|
for (let i = candidates.length - 2; i >= 0; i--) {
|
|
@@ -2798,13 +3144,42 @@ function fitTurnRadius(ctx, candidates, target) {
|
|
|
2798
3144
|
break;
|
|
2799
3145
|
}
|
|
2800
3146
|
}
|
|
3147
|
+
return bisectTurnRadius(
|
|
3148
|
+
lo,
|
|
3149
|
+
hi,
|
|
3150
|
+
target,
|
|
3151
|
+
(radius) => {
|
|
3152
|
+
const mid = evaluateTurnCandidate(ctx, radius);
|
|
3153
|
+
return "blocked" in mid ? void 0 : mid;
|
|
3154
|
+
},
|
|
3155
|
+
accept
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
function bisectTurnRadius(lo, hi, target, evaluate, accept) {
|
|
3159
|
+
if (!accept(lo) || !accept(hi)) {
|
|
3160
|
+
throw new Error(
|
|
3161
|
+
"auto-rig: bisectTurnRadius: a bracket end is not accepted"
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
const miss = (c) => Math.abs(c.ratio - target);
|
|
3165
|
+
let nearest = miss(lo) <= miss(hi) ? lo : hi;
|
|
2801
3166
|
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2802
|
-
const mid =
|
|
2803
|
-
if (
|
|
3167
|
+
const mid = evaluate(Math.sqrt(lo.radius * hi.radius));
|
|
3168
|
+
if (mid === void 0) break;
|
|
3169
|
+
if (accept(mid) && miss(mid) < miss(nearest)) nearest = mid;
|
|
2804
3170
|
if ((mid.ratio - target) * (lo.ratio - target) <= 0) hi = mid;
|
|
2805
3171
|
else lo = mid;
|
|
2806
3172
|
}
|
|
2807
|
-
|
|
3173
|
+
if (accept(lo) && accept(hi)) return miss(lo) <= miss(hi) ? lo : hi;
|
|
3174
|
+
return nearest;
|
|
3175
|
+
}
|
|
3176
|
+
function strandPreferredCandidates(candidates, farEyeRatio) {
|
|
3177
|
+
const feasible = candidates.filter((c) => c.strandFeasible);
|
|
3178
|
+
if (feasible.length === candidates.length) return candidates;
|
|
3179
|
+
const holds = ratioClusters(feasible).some(
|
|
3180
|
+
(run) => runHolds(run, farEyeRatio)
|
|
3181
|
+
);
|
|
3182
|
+
return holds ? feasible : candidates;
|
|
2808
3183
|
}
|
|
2809
3184
|
function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
2810
3185
|
const solve = (measured, marks, share) => solveTurnDepthSigned(
|
|
@@ -2819,13 +3194,38 @@ function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
|
2819
3194
|
mouth: solve(ctx.mouthShift, ctx.landmarks.mouth, MOUTH_SHIFT_SHARE)
|
|
2820
3195
|
};
|
|
2821
3196
|
}
|
|
2822
|
-
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges) {
|
|
3197
|
+
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges, strandEdges) {
|
|
2823
3198
|
if (targets.headHalfWidth !== void 0 && targets.headHalfWidth <= faceHalfWidth) {
|
|
2824
3199
|
throw new TurnTargetError(
|
|
2825
3200
|
`auto-rig: turnTargets.headHalfWidth (${targets.headHalfWidth}) must be wider than the face plate's own half-width (${faceHalfWidth})`
|
|
2826
3201
|
);
|
|
2827
3202
|
}
|
|
2828
3203
|
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
3204
|
+
let strands;
|
|
3205
|
+
const strandSides = ["left", "right"].filter(
|
|
3206
|
+
(key) => strandEdges?.[key] !== void 0
|
|
3207
|
+
);
|
|
3208
|
+
if (strandSides.length > 0) {
|
|
3209
|
+
if (hairFront === void 0) {
|
|
3210
|
+
throw new Error(
|
|
3211
|
+
"auto-rig: solveTurnModel: strandEdges was given, but there is no hair_front to land its runs on"
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
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);
|
|
3215
|
+
strands = strandSides.map((key) => {
|
|
3216
|
+
const carrier = irises.length === 2 ? irises[key === "left" ? 0 : 1] : void 0;
|
|
3217
|
+
if (carrier === void 0) {
|
|
3218
|
+
throw new Error(
|
|
3219
|
+
`auto-rig: solveTurnModel: strandEdges.${key} has no iris carrier to land`
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
return {
|
|
3223
|
+
...strandEdges[key],
|
|
3224
|
+
side: key === "left" ? -1 : 1,
|
|
3225
|
+
carrier
|
|
3226
|
+
};
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
2829
3229
|
const ctx = {
|
|
2830
3230
|
landmarks,
|
|
2831
3231
|
lattice,
|
|
@@ -2857,9 +3257,9 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
|
|
|
2857
3257
|
clampSilhouetteRatio: targets.defaulted.has("silhouetteRatio"),
|
|
2858
3258
|
hairFrontSilhouette: hairFront,
|
|
2859
3259
|
// A companion to a MEASURED headHalfWidth only — see its own doc.
|
|
2860
|
-
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges
|
|
3260
|
+
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges,
|
|
3261
|
+
strands
|
|
2861
3262
|
};
|
|
2862
|
-
const clamped = [];
|
|
2863
3263
|
const clampInto = (value, [lo, hi]) => Math.min(Math.max(value, lo), hi);
|
|
2864
3264
|
const pass = sweepTurnRadii(ctx);
|
|
2865
3265
|
if (pass.candidates.length === 0) {
|
|
@@ -2890,92 +3290,108 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
|
|
|
2890
3290
|
attainable: pass.heldRatioLower === Infinity || pass.heldRatioUpper === -Infinity ? void 0 : [pass.heldRatioLower, pass.heldRatioUpper]
|
|
2891
3291
|
};
|
|
2892
3292
|
}
|
|
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
|
|
3293
|
+
const fitFrom = (pool, accept) => {
|
|
3294
|
+
const clamped = [];
|
|
3295
|
+
const clusters = ratioClusters(pool);
|
|
3296
|
+
const distanceTo = (c) => Math.max(
|
|
3297
|
+
c.ratios[0] - targets.farEyeRatio,
|
|
3298
|
+
targets.farEyeRatio - c.ratios[1],
|
|
3299
|
+
0
|
|
2906
3300
|
);
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3301
|
+
let farEyeRatio = targets.farEyeRatio;
|
|
3302
|
+
let cluster = clusters.find((c) => runHolds(c, farEyeRatio));
|
|
3303
|
+
if (cluster === void 0) {
|
|
3304
|
+
cluster = clusters.reduce(
|
|
3305
|
+
(a, b) => distanceTo(b) < distanceTo(a) ? b : a
|
|
3306
|
+
);
|
|
3307
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3308
|
+
return {
|
|
3309
|
+
unreachable: true,
|
|
3310
|
+
field: "farEyeRatio",
|
|
3311
|
+
value: targets.farEyeRatio,
|
|
3312
|
+
attainable: cluster.ratios
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
farEyeRatio = clampInto(farEyeRatio, cluster.ratios);
|
|
3316
|
+
clamped.push("farEyeRatio");
|
|
3317
|
+
}
|
|
3318
|
+
const best = fitTurnRadius(ctx, cluster.candidates, farEyeRatio, accept);
|
|
3319
|
+
if (Math.abs(best.ratio - farEyeRatio) > TURN_DEPTH_EPS) {
|
|
3320
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3321
|
+
return {
|
|
3322
|
+
unreachable: true,
|
|
3323
|
+
field: "farEyeRatio",
|
|
3324
|
+
value: targets.farEyeRatio,
|
|
3325
|
+
attainable: cluster.ratios
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
if (!clamped.includes("farEyeRatio")) clamped.push("farEyeRatio");
|
|
3329
|
+
}
|
|
3330
|
+
if (!best.eye.reached) clamped.push("eyeShift");
|
|
3331
|
+
if (best.silhouetteClamped) clamped.push("silhouetteRatio");
|
|
3332
|
+
const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;
|
|
3333
|
+
for (const [field, solution] of [
|
|
3334
|
+
["noseShift", best.nose],
|
|
3335
|
+
["mouthShift", best.mouth]
|
|
3336
|
+
]) {
|
|
3337
|
+
if (solution.reached) continue;
|
|
3338
|
+
if (!targets.defaulted.has(field)) {
|
|
3339
|
+
return {
|
|
3340
|
+
unreachable: true,
|
|
3341
|
+
field,
|
|
3342
|
+
value: targets[field],
|
|
3343
|
+
// noseShift/mouthShift are MAGNITUDES too (see
|
|
3344
|
+
// solveFeatureDepths), so a negative lower bound here would
|
|
3345
|
+
// advertise a value the field's own contract already rules out; 0
|
|
3346
|
+
// is the true floor.
|
|
3347
|
+
attainable: [
|
|
3348
|
+
Math.max(
|
|
3349
|
+
0,
|
|
3350
|
+
(-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh
|
|
3351
|
+
),
|
|
3352
|
+
(-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh
|
|
3353
|
+
]
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
clamped.push(field);
|
|
2927
3357
|
}
|
|
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
|
-
|
|
3358
|
+
return {
|
|
3359
|
+
unreachable: false,
|
|
3360
|
+
radius: best.radius,
|
|
3361
|
+
holdBase: best.holdBase,
|
|
3362
|
+
holdEdgeAt: best.holdEdgeAt,
|
|
3363
|
+
travel: best.travel,
|
|
3364
|
+
surface: best.surface,
|
|
3365
|
+
depths: {
|
|
3366
|
+
eye: best.eye.depth,
|
|
3367
|
+
nose: best.nose.depth,
|
|
3368
|
+
mouth: best.mouth.depth
|
|
3369
|
+
},
|
|
3370
|
+
achieved: {
|
|
3371
|
+
eyeShift,
|
|
3372
|
+
farEyeRatio: best.ratio,
|
|
3373
|
+
silhouetteRatio: best.renderedSilhouetteRatio
|
|
3374
|
+
},
|
|
3375
|
+
clamped,
|
|
3376
|
+
...best.strandOverlap === void 0 ? {} : { strandOverlap: best.strandOverlap }
|
|
3377
|
+
};
|
|
3378
|
+
};
|
|
3379
|
+
const preferred = strandPreferredCandidates(
|
|
3380
|
+
pass.candidates,
|
|
3381
|
+
targets.farEyeRatio
|
|
3382
|
+
);
|
|
3383
|
+
if (preferred !== pass.candidates) {
|
|
3384
|
+
const fitted = fitFrom(preferred, (c) => c.strandFeasible);
|
|
3385
|
+
if (!fitted.unreachable && !fitted.clamped.includes("farEyeRatio")) {
|
|
3386
|
+
return fitted;
|
|
2954
3387
|
}
|
|
2955
|
-
clamped.push(field);
|
|
2956
3388
|
}
|
|
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
|
-
};
|
|
3389
|
+
return fitFrom(pass.candidates, () => true);
|
|
2976
3390
|
}
|
|
2977
3391
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
2978
3392
|
var HAIR_FRONT_DEPTH = 0.1;
|
|
3393
|
+
var MOUTH_TURN_TILT_DEG = 5;
|
|
3394
|
+
var NOSE_TURN_TILT_DEG = 6;
|
|
2979
3395
|
var NOD_TRAVEL = 30;
|
|
2980
3396
|
var NOD_BEND = 0.5;
|
|
2981
3397
|
var HAIR_BACK_NOD_DEPTH = -0.135;
|
|
@@ -3582,11 +3998,37 @@ function turnSetup(layers) {
|
|
|
3582
3998
|
)
|
|
3583
3999
|
);
|
|
3584
4000
|
}
|
|
4001
|
+
const mouthAt = members.find((m) => m.role === "mouth").transform;
|
|
4002
|
+
const mouthAnchor = {
|
|
4003
|
+
x: mouthAt.x,
|
|
4004
|
+
y: mouthAt.y,
|
|
4005
|
+
tiltDeg: MOUTH_TURN_TILT_DEG
|
|
4006
|
+
};
|
|
4007
|
+
const noseLayer = layers.find((l) => l.role === "nose");
|
|
4008
|
+
let noseAnchor;
|
|
4009
|
+
if (noseLayer) {
|
|
4010
|
+
const drawn = noseDrawnBox(noseLayer);
|
|
4011
|
+
const at = bboxToTransform(
|
|
4012
|
+
drawn,
|
|
4013
|
+
noseLayer.canvasW,
|
|
4014
|
+
noseLayer.canvasH,
|
|
4015
|
+
"nose"
|
|
4016
|
+
);
|
|
4017
|
+
noseAnchor = {
|
|
4018
|
+
x: at.x,
|
|
4019
|
+
y: at.y + drawn.h / 2,
|
|
4020
|
+
tiltDeg: NOSE_TURN_TILT_DEG
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
3585
4023
|
const carriers = new Map(
|
|
3586
|
-
members.map((m) =>
|
|
3587
|
-
m.
|
|
3588
|
-
|
|
3589
|
-
|
|
4024
|
+
members.map((m) => {
|
|
4025
|
+
const grid = groupGrids.get(m.group);
|
|
4026
|
+
const anchor = m.group === "mouthWarp" ? mouthAnchor : m.group === "noseWarp" ? noseAnchor : void 0;
|
|
4027
|
+
return [
|
|
4028
|
+
m.role,
|
|
4029
|
+
anchor === void 0 ? { grid, part: m.part } : { grid, part: m.part, anchor }
|
|
4030
|
+
];
|
|
4031
|
+
})
|
|
3590
4032
|
);
|
|
3591
4033
|
const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
|
|
3592
4034
|
let reach = 0;
|
|
@@ -3656,6 +4098,7 @@ function solveInputsOf(s) {
|
|
|
3656
4098
|
}
|
|
3657
4099
|
function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
3658
4100
|
validateLayerInputs(layers, canvas);
|
|
4101
|
+
validateStrandEdges(layers, options.strandEdges);
|
|
3659
4102
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
3660
4103
|
const bodyLayer = layers.find((l) => l.role === "body");
|
|
3661
4104
|
const parameters = [...STANDARD_PARAMETERS];
|
|
@@ -3691,6 +4134,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3691
4134
|
plateGuardRows,
|
|
3692
4135
|
members,
|
|
3693
4136
|
groupGrids,
|
|
4137
|
+
carriers,
|
|
3694
4138
|
parallaxUnitY,
|
|
3695
4139
|
hasNose,
|
|
3696
4140
|
hasMouthOpen
|
|
@@ -3699,7 +4143,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3699
4143
|
const turn = hasNose ? solveTurnModel(
|
|
3700
4144
|
resolveTurnTargets(options.turnTargets),
|
|
3701
4145
|
...solveInputsOf(setup),
|
|
3702
|
-
options.headEdges
|
|
4146
|
+
options.headEdges,
|
|
4147
|
+
options.strandEdges
|
|
3703
4148
|
) : void 0;
|
|
3704
4149
|
if (turn?.unreachable) {
|
|
3705
4150
|
throw new TurnTargetError(
|
|
@@ -3717,7 +4162,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3717
4162
|
holdBase: turn.holdBase,
|
|
3718
4163
|
depths: turn.depths,
|
|
3719
4164
|
achieved: turn.achieved,
|
|
3720
|
-
clamped: turn.clamped
|
|
4165
|
+
clamped: turn.clamped,
|
|
4166
|
+
...turn.strandOverlap === void 0 ? {} : { strandOverlap: turn.strandOverlap }
|
|
3721
4167
|
});
|
|
3722
4168
|
}
|
|
3723
4169
|
const latticeReach = Math.max(
|
|
@@ -3790,7 +4236,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3790
4236
|
}
|
|
3791
4237
|
];
|
|
3792
4238
|
for (const [group, grid] of groupGrids) {
|
|
3793
|
-
const
|
|
4239
|
+
const { role } = members.find((m) => m.group === group);
|
|
4240
|
+
const family = turnFamily(role);
|
|
3794
4241
|
const depth = group === "faceWarp" ? 0 : turn?.depths[family] ?? 0;
|
|
3795
4242
|
deformers.push({
|
|
3796
4243
|
kind: "warp",
|
|
@@ -3802,7 +4249,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3802
4249
|
import_format3.StandardParameter.AngleX,
|
|
3803
4250
|
import_format3.StandardParameter.AngleY,
|
|
3804
4251
|
surface,
|
|
3805
|
-
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG
|
|
4252
|
+
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG,
|
|
4253
|
+
carriers.get(role).anchor
|
|
3806
4254
|
)
|
|
3807
4255
|
});
|
|
3808
4256
|
}
|