@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.mjs
CHANGED
|
@@ -1776,6 +1776,164 @@ function validateLayerInputs(layers, canvas) {
|
|
|
1776
1776
|
}
|
|
1777
1777
|
}
|
|
1778
1778
|
}
|
|
1779
|
+
if (layer.denseCore !== void 0) {
|
|
1780
|
+
const where = `auto-rig: validateLayerInputs: role "${role}" denseCore`;
|
|
1781
|
+
const core = layer.denseCore;
|
|
1782
|
+
if (typeof core !== "object" || core === null || Array.isArray(core)) {
|
|
1783
|
+
throw new Error(`${where} must be a plain object { x, y, w, h }`);
|
|
1784
|
+
}
|
|
1785
|
+
for (const field of ["x", "y", "w", "h"]) {
|
|
1786
|
+
const v = core[field];
|
|
1787
|
+
const size = field === "w" || field === "h";
|
|
1788
|
+
if (typeof v !== "number" || !Number.isFinite(v) || size && v <= 0) {
|
|
1789
|
+
throw new Error(
|
|
1790
|
+
`${where}.${field} is ${String(v)}, not a finite number${size ? " > 0" : ""}`
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
const { x, y, w, h } = layer.denseCore;
|
|
1795
|
+
const right = bbox.x + bbox.w;
|
|
1796
|
+
const bottom = bbox.y + bbox.h;
|
|
1797
|
+
if (x < bbox.x) {
|
|
1798
|
+
throw new Error(
|
|
1799
|
+
`${where}.x (${x}) lies left of the bbox's left edge (${bbox.x})`
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
if (y < bbox.y) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
`${where}.y (${y}) lies above the bbox's top edge (${bbox.y})`
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
if (x + w > right) {
|
|
1808
|
+
throw new Error(
|
|
1809
|
+
`${where}.w (${w}) puts its right edge at ${x + w}, past the bbox's right edge (${right})`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
if (y + h > bottom) {
|
|
1813
|
+
throw new Error(
|
|
1814
|
+
`${where}.h (${h}) puts its bottom edge at ${y + h}, past the bbox's bottom edge (${bottom})`
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function validateStrandEdges(layers, strandEdges) {
|
|
1821
|
+
if (strandEdges === void 0) return;
|
|
1822
|
+
const where = "auto-rig: validateStrandEdges: strandEdges";
|
|
1823
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1824
|
+
if (!isPlainObject(strandEdges)) {
|
|
1825
|
+
throw new Error(
|
|
1826
|
+
`${where} must be a plain object with optional left / right sides`
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
for (const key of ["left", "right"]) {
|
|
1830
|
+
const raw = strandEdges[key];
|
|
1831
|
+
if (raw === void 0) continue;
|
|
1832
|
+
const path = `${where}.${key}`;
|
|
1833
|
+
if (!isPlainObject(raw)) {
|
|
1834
|
+
throw new Error(`${path} must be a plain object`);
|
|
1835
|
+
}
|
|
1836
|
+
for (const field of ["y", "irisOuter", "irisInner", "runOuter"]) {
|
|
1837
|
+
const v = raw[field];
|
|
1838
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1839
|
+
throw new Error(
|
|
1840
|
+
`${path}.${field} is ${String(v)}, not a finite number`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
const rawFace = raw.runFace;
|
|
1845
|
+
if (rawFace !== null && (typeof rawFace !== "number" || !Number.isFinite(rawFace))) {
|
|
1846
|
+
throw new Error(
|
|
1847
|
+
`${path}.runFace is ${String(rawFace)}, not a finite number or null`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
const { y, irisOuter, irisInner, runOuter, runFace } = raw;
|
|
1851
|
+
const cropOf = (role) => {
|
|
1852
|
+
const layer = layers.find((l) => l.role === role);
|
|
1853
|
+
if (layer === void 0) {
|
|
1854
|
+
throw new Error(
|
|
1855
|
+
`${path} needs a "${role}" layer to be measured on, and this layer set has none`
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
1859
|
+
return {
|
|
1860
|
+
role,
|
|
1861
|
+
x: t.x,
|
|
1862
|
+
left: t.x - layer.cropW / 2,
|
|
1863
|
+
right: t.x + layer.cropW / 2,
|
|
1864
|
+
bottom: t.y - layer.cropH / 2,
|
|
1865
|
+
top: t.y + layer.cropH / 2
|
|
1866
|
+
};
|
|
1867
|
+
};
|
|
1868
|
+
const hair = cropOf("hair_front");
|
|
1869
|
+
const [lowIris, highIris] = [cropOf("iris_L"), cropOf("iris_R")].sort(
|
|
1870
|
+
(a, b) => a.x - b.x
|
|
1871
|
+
);
|
|
1872
|
+
const sign = key === "left" ? -1 : 1;
|
|
1873
|
+
const iris = key === "left" ? lowIris : highIris;
|
|
1874
|
+
const other = key === "left" ? highIris : lowIris;
|
|
1875
|
+
const fail = (field, value, why) => new Error(`${path}.${field} (${value}) ${why}`);
|
|
1876
|
+
for (const crop of [iris, hair]) {
|
|
1877
|
+
if (y < crop.bottom || y > crop.top) {
|
|
1878
|
+
throw fail(
|
|
1879
|
+
"y",
|
|
1880
|
+
y,
|
|
1881
|
+
`lies outside the "${crop.role}" crop's rows (${crop.bottom}\u2026${crop.top})`
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
for (const [field, x] of [
|
|
1886
|
+
["irisOuter", irisOuter],
|
|
1887
|
+
["irisInner", irisInner]
|
|
1888
|
+
]) {
|
|
1889
|
+
if (x < iris.left || x > iris.right) {
|
|
1890
|
+
throw fail(
|
|
1891
|
+
field,
|
|
1892
|
+
x,
|
|
1893
|
+
`lies outside the "${iris.role}" crop's columns (${iris.left}\u2026${iris.right})`
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (sign * (irisOuter - irisInner) <= 0) {
|
|
1898
|
+
throw fail(
|
|
1899
|
+
"irisOuter",
|
|
1900
|
+
irisOuter,
|
|
1901
|
+
`must lie strictly outward of strandEdges.${key}.irisInner (${irisInner})`
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
for (const [field, x] of [
|
|
1905
|
+
["runOuter", runOuter],
|
|
1906
|
+
["runFace", runFace]
|
|
1907
|
+
]) {
|
|
1908
|
+
if (x !== null && (x < hair.left || x > hair.right)) {
|
|
1909
|
+
throw fail(
|
|
1910
|
+
field,
|
|
1911
|
+
x,
|
|
1912
|
+
`lies outside the "hair_front" crop's columns (${hair.left}\u2026${hair.right})`
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (sign * (runOuter - iris.x) <= 0) {
|
|
1917
|
+
throw fail(
|
|
1918
|
+
"runOuter",
|
|
1919
|
+
runOuter,
|
|
1920
|
+
`must lie strictly outward of the "${iris.role}" crop's centre (${iris.x})`
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
if (runFace !== null && sign * (runOuter - runFace) <= 0) {
|
|
1924
|
+
throw fail(
|
|
1925
|
+
"runOuter",
|
|
1926
|
+
runOuter,
|
|
1927
|
+
`must lie strictly outward of strandEdges.${key}.runFace (${runFace})`
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
if (runFace !== null && sign * (runFace - other.x) <= 0) {
|
|
1931
|
+
throw fail(
|
|
1932
|
+
"runFace",
|
|
1933
|
+
runFace,
|
|
1934
|
+
`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`
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1779
1937
|
}
|
|
1780
1938
|
}
|
|
1781
1939
|
function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
@@ -1934,21 +2092,26 @@ function turnSurface(spec) {
|
|
|
1934
2092
|
)
|
|
1935
2093
|
};
|
|
1936
2094
|
}
|
|
1937
|
-
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt) {
|
|
2095
|
+
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt, anchor) {
|
|
1938
2096
|
const STOPS = [...HEAD_TURN_STOPS];
|
|
1939
2097
|
const pointCount = grid.points.length / 2;
|
|
1940
2098
|
const keyforms2d = [];
|
|
1941
2099
|
for (const angleY of STOPS) {
|
|
1942
2100
|
for (const angleX of STOPS) {
|
|
1943
2101
|
const shift = shiftAt(angleX);
|
|
2102
|
+
const mapAt = (nodeY) => surface.mapAt(angleX, nodeY);
|
|
1944
2103
|
const offsets = [];
|
|
1945
2104
|
for (let i = 0; i < pointCount; i++) {
|
|
1946
2105
|
const x = grid.points[i * 2];
|
|
1947
2106
|
const y = grid.points[i * 2 + 1];
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2107
|
+
const nodDy = angleY === 0 ? 0 : surface.nodBendAt(y, angleY);
|
|
2108
|
+
if (angleX === 0) {
|
|
2109
|
+
offsets.push(0, nodDy);
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
const landed = groupNodeLanding(anchor, mapAt, angleX, shift, x, y);
|
|
2113
|
+
const turnDy = landed.y - y;
|
|
2114
|
+
offsets.push(landed.x - x, angleY === 0 ? turnDy : turnDy + nodDy);
|
|
1952
2115
|
}
|
|
1953
2116
|
keyforms2d.push({ offsets });
|
|
1954
2117
|
}
|
|
@@ -2068,6 +2231,22 @@ function carrierLandingX(carrier, nodeLanding, px, py) {
|
|
|
2068
2231
|
py
|
|
2069
2232
|
);
|
|
2070
2233
|
}
|
|
2234
|
+
function groupNodeLanding(anchor, mapAt, deg, shift, nodeX, nodeY) {
|
|
2235
|
+
if (anchor === void 0) {
|
|
2236
|
+
return { x: mapAt(nodeY).mapX(nodeX + shift), y: nodeY };
|
|
2237
|
+
}
|
|
2238
|
+
const anchorRow = mapAt(anchor.y);
|
|
2239
|
+
const pivotX = anchorRow.mapX(anchor.x + shift);
|
|
2240
|
+
const dx = mapAt(nodeY).mapX(nodeX) - anchorRow.mapX(anchor.x);
|
|
2241
|
+
const dy = nodeY - anchor.y;
|
|
2242
|
+
const phi = anchor.tiltDeg * deg / HEAD_TURN_MAX_DEG * (Math.PI / 180);
|
|
2243
|
+
const cos = Math.cos(phi);
|
|
2244
|
+
const sin = Math.sin(phi);
|
|
2245
|
+
return {
|
|
2246
|
+
x: pivotX + cos * dx - sin * dy,
|
|
2247
|
+
y: anchor.y + sin * dx + cos * dy
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2071
2250
|
function rowMapsOf(surface) {
|
|
2072
2251
|
const maps = /* @__PURE__ */ new Map();
|
|
2073
2252
|
return (deg, nodeY) => {
|
|
@@ -2190,12 +2369,17 @@ function resolveTurnTargets(targets = {}) {
|
|
|
2190
2369
|
defaulted
|
|
2191
2370
|
};
|
|
2192
2371
|
}
|
|
2372
|
+
function noseDrawnBox(layer) {
|
|
2373
|
+
return layer.denseCore ?? layer.bbox;
|
|
2374
|
+
}
|
|
2193
2375
|
function turnLandmarks(layers, carriers) {
|
|
2194
2376
|
const markOf = (role) => {
|
|
2195
2377
|
const layer = layers.find((l) => l.role === role);
|
|
2196
2378
|
if (!layer) return void 0;
|
|
2197
|
-
const
|
|
2198
|
-
const
|
|
2379
|
+
const drawn = role === "nose" ? noseDrawnBox(layer) : layer.bbox;
|
|
2380
|
+
const t = bboxToTransform(drawn, layer.canvasW, layer.canvasH, role);
|
|
2381
|
+
const w = drawn === layer.bbox ? layer.cropW : drawn.w;
|
|
2382
|
+
const mark = { x: t.x, y: t.y, w };
|
|
2199
2383
|
const carrier = carriers?.get(role);
|
|
2200
2384
|
if (carrier !== void 0) mark.carrier = carrier;
|
|
2201
2385
|
return mark;
|
|
@@ -2221,11 +2405,12 @@ function eyeRowOf(landmarks, fallback) {
|
|
|
2221
2405
|
}
|
|
2222
2406
|
var TURN_BISECT_STEPS = 40;
|
|
2223
2407
|
var TURN_DEPTH_EPS = 1e-6;
|
|
2224
|
-
function landmarkLandingX(landmark, mapAt, shift, px, py) {
|
|
2225
|
-
|
|
2408
|
+
function landmarkLandingX(landmark, mapAt, deg, shift, px, py) {
|
|
2409
|
+
const { carrier } = landmark;
|
|
2410
|
+
if (carrier === void 0) return mapAt(py).mapX(px + shift);
|
|
2226
2411
|
return carrierLandingX(
|
|
2227
|
-
|
|
2228
|
-
(nodeX, nodeY) =>
|
|
2412
|
+
carrier,
|
|
2413
|
+
(nodeX, nodeY) => groupNodeLanding(carrier.anchor, mapAt, deg, shift, nodeX, nodeY).x,
|
|
2229
2414
|
px,
|
|
2230
2415
|
py
|
|
2231
2416
|
);
|
|
@@ -2234,22 +2419,47 @@ function familyReachPx(landmarks, plateEdgeX) {
|
|
|
2234
2419
|
const farEdge = Math.min(...landmarks.map((l) => l.x - l.w / 2));
|
|
2235
2420
|
return Math.max(0, farEdge - plateEdgeX);
|
|
2236
2421
|
}
|
|
2237
|
-
function
|
|
2422
|
+
function familyDepthCap(landmarks, plateEdgeX, unit) {
|
|
2423
|
+
return familyReachPx(landmarks, plateEdgeX) / unit;
|
|
2424
|
+
}
|
|
2425
|
+
function solveTurnDepthSigned(target, landmarks, unit, mapAt, plateEdgeX, maxDepth) {
|
|
2238
2426
|
if (landmarks.length === 0) {
|
|
2239
2427
|
throw new Error("auto-rig: solveTurnDepth: no landmark to slide");
|
|
2240
2428
|
}
|
|
2241
|
-
const
|
|
2429
|
+
const plateCap = familyDepthCap(landmarks, plateEdgeX, unit);
|
|
2430
|
+
const cap = maxDepth === void 0 ? plateCap : Math.min(plateCap, maxDepth);
|
|
2242
2431
|
const achieved = (d) => landmarks.reduce(
|
|
2243
|
-
(sum, l) => sum + (landmarkLandingX(
|
|
2432
|
+
(sum, l) => sum + (landmarkLandingX(
|
|
2433
|
+
l,
|
|
2434
|
+
mapAt,
|
|
2435
|
+
-HEAD_TURN_MAX_DEG,
|
|
2436
|
+
-d * unit,
|
|
2437
|
+
l.x,
|
|
2438
|
+
l.y ?? 0
|
|
2439
|
+
) - l.x),
|
|
2244
2440
|
0
|
|
2245
2441
|
) / landmarks.length;
|
|
2246
2442
|
const atCap = achieved(cap);
|
|
2247
2443
|
const atRest = achieved(0);
|
|
2248
2444
|
const attainable = [atCap, atRest];
|
|
2249
|
-
if (target > atRest + TURN_DEPTH_EPS)
|
|
2250
|
-
return {
|
|
2251
|
-
|
|
2252
|
-
|
|
2445
|
+
if (target > atRest + TURN_DEPTH_EPS) {
|
|
2446
|
+
return {
|
|
2447
|
+
reached: false,
|
|
2448
|
+
depth: 0,
|
|
2449
|
+
achieved: atRest,
|
|
2450
|
+
attainable,
|
|
2451
|
+
limit: "floor"
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
if (target < atCap - TURN_DEPTH_EPS) {
|
|
2455
|
+
return {
|
|
2456
|
+
reached: false,
|
|
2457
|
+
depth: cap,
|
|
2458
|
+
achieved: atCap,
|
|
2459
|
+
attainable,
|
|
2460
|
+
limit: "cap"
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2253
2463
|
const clampedTarget = Math.min(Math.max(target, atCap), atRest);
|
|
2254
2464
|
let lo = 0;
|
|
2255
2465
|
let hi = cap;
|
|
@@ -2279,7 +2489,7 @@ function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, hol
|
|
|
2279
2489
|
}
|
|
2280
2490
|
return x + side * (holdEdge - holdBase);
|
|
2281
2491
|
}
|
|
2282
|
-
function hairFrontLandingAt(x, y, hairFront, holdTargetAt,
|
|
2492
|
+
function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadShift) {
|
|
2283
2493
|
const { cols, rows } = meshCellsFor(hairFront.cropW, hairFront.cropH);
|
|
2284
2494
|
const top = hairFront.centerY + hairFront.cropH / 2;
|
|
2285
2495
|
return renderedLandingX(
|
|
@@ -2294,7 +2504,7 @@ function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadTipShift) {
|
|
|
2294
2504
|
scaleY: 1,
|
|
2295
2505
|
rotation: 0
|
|
2296
2506
|
},
|
|
2297
|
-
(vx, vy) => holdTargetAt(vx, vy)
|
|
2507
|
+
(vx, vy) => holdTargetAt(vx, vy) + leadShift * Math.pow((top - vy) / hairFront.cropH, HAIR_SWAY_CURL),
|
|
2298
2508
|
x,
|
|
2299
2509
|
y
|
|
2300
2510
|
);
|
|
@@ -2327,6 +2537,19 @@ function aitken(x0, x1, x2, cap) {
|
|
|
2327
2537
|
const x = x2 - d2 * d2 / denominator;
|
|
2328
2538
|
return Number.isFinite(x) ? Math.min(cap, Math.max(0, x)) : x2;
|
|
2329
2539
|
}
|
|
2540
|
+
function strandDepthCap(margins, plateCap) {
|
|
2541
|
+
const holds = (depth) => margins.every((m) => m(depth) >= 0);
|
|
2542
|
+
if (holds(plateCap)) return plateCap;
|
|
2543
|
+
if (!holds(0)) return void 0;
|
|
2544
|
+
let lo = 0;
|
|
2545
|
+
let hi = plateCap;
|
|
2546
|
+
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2547
|
+
const mid = (lo + hi) / 2;
|
|
2548
|
+
if (holds(mid)) lo = mid;
|
|
2549
|
+
else hi = mid;
|
|
2550
|
+
}
|
|
2551
|
+
return lo;
|
|
2552
|
+
}
|
|
2330
2553
|
function evaluateTurnCandidate(ctx, radius) {
|
|
2331
2554
|
const surfaceAt = (travel) => turnSurface({
|
|
2332
2555
|
faceCenterX: ctx.faceCenterX,
|
|
@@ -2350,7 +2573,104 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2350
2573
|
return edge === void 0 ? ctx.headHalfWidth : side * (edge - ctx.faceCenterX);
|
|
2351
2574
|
};
|
|
2352
2575
|
const unit = headTurnParallaxUnit(radius);
|
|
2576
|
+
const plateCapOf = (marks) => familyDepthCap(marks, ctx.plateEdgeX, unit);
|
|
2577
|
+
const eyePlateCap = plateCapOf(ctx.landmarks.eye);
|
|
2353
2578
|
const leadTipShift = ctx.hairFrontSilhouette === void 0 ? 0 : HAIR_FRONT_DEPTH * unit;
|
|
2579
|
+
const bangsLandingAt = (hold, plateLandingAt, deg, leadShift, x, y) => hairFrontLandingAt(
|
|
2580
|
+
x,
|
|
2581
|
+
y,
|
|
2582
|
+
ctx.hairFrontSilhouette,
|
|
2583
|
+
(vx, vy) => hairFrontHoldTarget(
|
|
2584
|
+
vx,
|
|
2585
|
+
vy,
|
|
2586
|
+
deg,
|
|
2587
|
+
ctx.faceCenterX,
|
|
2588
|
+
plateLandingAt,
|
|
2589
|
+
ctx.edgeAt,
|
|
2590
|
+
hold.holdBase,
|
|
2591
|
+
hold.holdEdgeAt(deg)
|
|
2592
|
+
),
|
|
2593
|
+
leadShift
|
|
2594
|
+
);
|
|
2595
|
+
const strandStopsOn = (hold) => {
|
|
2596
|
+
const rowMaps = rowMapsOf(hold.surface);
|
|
2597
|
+
const plateLandingAt = plateLandingOn(ctx.plate, rowMaps);
|
|
2598
|
+
return (ctx.strands ?? []).map((strand) => {
|
|
2599
|
+
const iris = {
|
|
2600
|
+
x: strand.carrier.part.x,
|
|
2601
|
+
w: strand.carrier.part.cropW,
|
|
2602
|
+
carrier: strand.carrier
|
|
2603
|
+
};
|
|
2604
|
+
const stops = HEAD_TURN_STOPS.filter(
|
|
2605
|
+
(deg) => Math.sign(deg) === strand.side
|
|
2606
|
+
).map((deg) => {
|
|
2607
|
+
const runAt = (x) => bangsLandingAt(
|
|
2608
|
+
hold,
|
|
2609
|
+
plateLandingAt,
|
|
2610
|
+
deg,
|
|
2611
|
+
leadTipShift * deg / HEAD_TURN_MAX_DEG,
|
|
2612
|
+
x,
|
|
2613
|
+
strand.y
|
|
2614
|
+
);
|
|
2615
|
+
return {
|
|
2616
|
+
deg,
|
|
2617
|
+
runOuter: runAt(strand.runOuter),
|
|
2618
|
+
runFace: strand.runFace === null ? null : runAt(strand.runFace),
|
|
2619
|
+
irisAt: (depth, x) => landmarkLandingX(
|
|
2620
|
+
iris,
|
|
2621
|
+
(y) => rowMaps(deg, y),
|
|
2622
|
+
deg,
|
|
2623
|
+
depth * unit * deg / HEAD_TURN_MAX_DEG,
|
|
2624
|
+
x,
|
|
2625
|
+
strand.y
|
|
2626
|
+
)
|
|
2627
|
+
};
|
|
2628
|
+
});
|
|
2629
|
+
const { runFace } = strand;
|
|
2630
|
+
if (runFace === null) return { strand, stops, margins: void 0 };
|
|
2631
|
+
const allowance = Math.min(0, strand.side * (runFace - strand.irisOuter));
|
|
2632
|
+
const margins = stops.map(
|
|
2633
|
+
(stop) => (depth) => strand.side * (stop.runFace - stop.irisAt(depth, strand.irisOuter)) - allowance
|
|
2634
|
+
);
|
|
2635
|
+
return { strand, stops, margins };
|
|
2636
|
+
});
|
|
2637
|
+
};
|
|
2638
|
+
const strandOverlapOf = (sides, depth) => {
|
|
2639
|
+
const overlap = {};
|
|
2640
|
+
for (const { strand, stops, margins } of sides) {
|
|
2641
|
+
const s = strand.side;
|
|
2642
|
+
const covered = (irisOuter, irisInner, runOuter, runFace) => Math.max(
|
|
2643
|
+
0,
|
|
2644
|
+
Math.min(s * irisOuter, s * runOuter) - Math.max(s * irisInner, runFace === null ? -Infinity : s * runFace)
|
|
2645
|
+
);
|
|
2646
|
+
let worst;
|
|
2647
|
+
for (const stop of stops) {
|
|
2648
|
+
const px = covered(
|
|
2649
|
+
stop.irisAt(depth, strand.irisOuter),
|
|
2650
|
+
stop.irisAt(depth, strand.irisInner),
|
|
2651
|
+
stop.runOuter,
|
|
2652
|
+
stop.runFace
|
|
2653
|
+
);
|
|
2654
|
+
if (px > 0 && (worst === void 0 || px > worst.px)) {
|
|
2655
|
+
worst = { deg: stop.deg, px };
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
if (worst === void 0) continue;
|
|
2659
|
+
overlap[s < 0 ? "left" : "right"] = {
|
|
2660
|
+
deg: worst.deg,
|
|
2661
|
+
px: worst.px,
|
|
2662
|
+
hh: worst.px / ctx.hh,
|
|
2663
|
+
restPx: covered(
|
|
2664
|
+
strand.irisOuter,
|
|
2665
|
+
strand.irisInner,
|
|
2666
|
+
strand.runOuter,
|
|
2667
|
+
strand.runFace
|
|
2668
|
+
),
|
|
2669
|
+
held: margins !== void 0 && margins.every((m) => m(depth) >= 0)
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
return overlap.left === void 0 && overlap.right === void 0 ? void 0 : overlap;
|
|
2673
|
+
};
|
|
2354
2674
|
const evaluateHold = (ratio, depths2) => {
|
|
2355
2675
|
const holdEdgeFrom = (base) => (deg) => base * (1 + (ratio - 1) * Math.abs(deg) / HEAD_TURN_MAX_DEG);
|
|
2356
2676
|
const travel = ctx.headHalfWidth === void 0 ? ctx.travel : Math.min(
|
|
@@ -2400,21 +2720,13 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2400
2720
|
if (edge !== void 0) return edge;
|
|
2401
2721
|
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side * holdBase : ctx.hairFrontSilhouette.x + side * ctx.hairFrontSilhouette.cropW / 2;
|
|
2402
2722
|
};
|
|
2403
|
-
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x :
|
|
2723
|
+
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x : bangsLandingAt(
|
|
2724
|
+
{ holdBase, holdEdgeAt },
|
|
2725
|
+
plateLandingAt,
|
|
2726
|
+
-HEAD_TURN_MAX_DEG,
|
|
2727
|
+
-leadTipShift,
|
|
2404
2728
|
x,
|
|
2405
|
-
ctx.eyeRowY
|
|
2406
|
-
ctx.hairFrontSilhouette,
|
|
2407
|
-
(vx, vy) => hairFrontHoldTarget(
|
|
2408
|
-
vx,
|
|
2409
|
-
vy,
|
|
2410
|
-
-HEAD_TURN_MAX_DEG,
|
|
2411
|
-
ctx.faceCenterX,
|
|
2412
|
-
plateLandingAt,
|
|
2413
|
-
ctx.edgeAt,
|
|
2414
|
-
holdBase,
|
|
2415
|
-
holdEdgeAt(-HEAD_TURN_MAX_DEG)
|
|
2416
|
-
),
|
|
2417
|
-
leadTipShift
|
|
2729
|
+
ctx.eyeRowY
|
|
2418
2730
|
);
|
|
2419
2731
|
const landingOfRole = (role, x) => {
|
|
2420
2732
|
if (role === "hair_front") return hairFrontAt(x);
|
|
@@ -2434,7 +2746,14 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2434
2746
|
const shift = role === "face" ? 0 : -depths2[turnFamily(role)] * unit;
|
|
2435
2747
|
return carrierLandingX(
|
|
2436
2748
|
carrier,
|
|
2437
|
-
(nodeX, nodeY) =>
|
|
2749
|
+
(nodeX, nodeY) => groupNodeLanding(
|
|
2750
|
+
carrier.anchor,
|
|
2751
|
+
mapAt,
|
|
2752
|
+
-HEAD_TURN_MAX_DEG,
|
|
2753
|
+
shift,
|
|
2754
|
+
nodeX,
|
|
2755
|
+
nodeY
|
|
2756
|
+
).x,
|
|
2438
2757
|
x,
|
|
2439
2758
|
ctx.eyeRowY
|
|
2440
2759
|
);
|
|
@@ -2575,12 +2894,25 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2575
2894
|
"auto-rig: evaluateTurnCandidate: the hold it settled on folds"
|
|
2576
2895
|
);
|
|
2577
2896
|
}
|
|
2897
|
+
const strandSides = ctx.strands === void 0 ? void 0 : strandStopsOn(hold);
|
|
2898
|
+
let strandFeasible = true;
|
|
2899
|
+
let maxDepth;
|
|
2900
|
+
if (strandSides !== void 0) {
|
|
2901
|
+
const margins = strandSides.map((s) => s.margins);
|
|
2902
|
+
const cap = margins.every((m) => m !== void 0) ? strandDepthCap(
|
|
2903
|
+
margins.flatMap((m) => m),
|
|
2904
|
+
eyePlateCap
|
|
2905
|
+
) : void 0;
|
|
2906
|
+
strandFeasible = cap !== void 0;
|
|
2907
|
+
maxDepth = cap ?? 0;
|
|
2908
|
+
}
|
|
2578
2909
|
const eye = solveTurnDepthSigned(
|
|
2579
2910
|
hold.silhouetteCenterShift - ctx.eyeShift * ctx.hh,
|
|
2580
2911
|
ctx.landmarks.eye,
|
|
2581
2912
|
unit,
|
|
2582
2913
|
hold.mapAt,
|
|
2583
|
-
ctx.plateEdgeX
|
|
2914
|
+
ctx.plateEdgeX,
|
|
2915
|
+
maxDepth
|
|
2584
2916
|
);
|
|
2585
2917
|
const features = solveFeatureDepths(
|
|
2586
2918
|
ctx,
|
|
@@ -2601,25 +2933,19 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2601
2933
|
seed = depths;
|
|
2602
2934
|
depths = solved;
|
|
2603
2935
|
} else {
|
|
2604
|
-
const capOf = (marks) => familyReachPx(marks, ctx.plateEdgeX) / unit;
|
|
2605
2936
|
depths = {
|
|
2606
|
-
eye: aitken(
|
|
2607
|
-
seed.eye,
|
|
2608
|
-
depths.eye,
|
|
2609
|
-
solved.eye,
|
|
2610
|
-
capOf(ctx.landmarks.eye)
|
|
2611
|
-
),
|
|
2937
|
+
eye: aitken(seed.eye, depths.eye, solved.eye, eyePlateCap),
|
|
2612
2938
|
nose: aitken(
|
|
2613
2939
|
seed.nose,
|
|
2614
2940
|
depths.nose,
|
|
2615
2941
|
solved.nose,
|
|
2616
|
-
|
|
2942
|
+
plateCapOf(ctx.landmarks.nose)
|
|
2617
2943
|
),
|
|
2618
2944
|
mouth: aitken(
|
|
2619
2945
|
seed.mouth,
|
|
2620
2946
|
depths.mouth,
|
|
2621
2947
|
solved.mouth,
|
|
2622
|
-
|
|
2948
|
+
plateCapOf(ctx.landmarks.mouth)
|
|
2623
2949
|
)
|
|
2624
2950
|
};
|
|
2625
2951
|
seed = void 0;
|
|
@@ -2638,7 +2964,7 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2638
2964
|
if (silhouetteClamped && !ctx.clampSilhouetteRatio) {
|
|
2639
2965
|
return silhouetteMiss(span);
|
|
2640
2966
|
}
|
|
2641
|
-
if (!eye.reached && !ctx.clampEyeShift) {
|
|
2967
|
+
if (!eye.reached && eye.limit === "floor" && !ctx.clampEyeShift) {
|
|
2642
2968
|
return {
|
|
2643
2969
|
blocked: "eyeShift",
|
|
2644
2970
|
// eyeShift is a MAGNITUDE (the sign is ignored — see TurnTargets), so
|
|
@@ -2651,9 +2977,24 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2651
2977
|
};
|
|
2652
2978
|
}
|
|
2653
2979
|
const eyeShiftPx = -eye.depth * unit;
|
|
2654
|
-
const scaleOf = (l) => (landmarkLandingX(
|
|
2980
|
+
const scaleOf = (l) => (landmarkLandingX(
|
|
2981
|
+
l,
|
|
2982
|
+
mapAt,
|
|
2983
|
+
-HEAD_TURN_MAX_DEG,
|
|
2984
|
+
eyeShiftPx,
|
|
2985
|
+
l.x + l.w / 2,
|
|
2986
|
+
l.y ?? 0
|
|
2987
|
+
) - landmarkLandingX(
|
|
2988
|
+
l,
|
|
2989
|
+
mapAt,
|
|
2990
|
+
-HEAD_TURN_MAX_DEG,
|
|
2991
|
+
eyeShiftPx,
|
|
2992
|
+
l.x - l.w / 2,
|
|
2993
|
+
l.y ?? 0
|
|
2994
|
+
)) / l.w;
|
|
2655
2995
|
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
2656
2996
|
const near = ctx.landmarks.eye.reduce((a, b) => b.x > a.x ? b : a);
|
|
2997
|
+
const strandOverlap = strandSides === void 0 ? void 0 : strandOverlapOf(strandSides, eye.depth);
|
|
2657
2998
|
return {
|
|
2658
2999
|
radius,
|
|
2659
3000
|
ratio: scaleOf(far) / scaleOf(near),
|
|
@@ -2668,7 +3009,9 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2668
3009
|
mouth: features.mouth,
|
|
2669
3010
|
silhouetteClamped,
|
|
2670
3011
|
silhouetteCenterShift,
|
|
2671
|
-
renderedSilhouetteRatio
|
|
3012
|
+
renderedSilhouetteRatio,
|
|
3013
|
+
strandFeasible,
|
|
3014
|
+
...strandOverlap === void 0 ? {} : { strandOverlap }
|
|
2672
3015
|
};
|
|
2673
3016
|
}
|
|
2674
3017
|
}
|
|
@@ -2705,6 +3048,9 @@ function sweepTurnRadii(ctx) {
|
|
|
2705
3048
|
unsettled
|
|
2706
3049
|
};
|
|
2707
3050
|
}
|
|
3051
|
+
function runHolds(run, ratio) {
|
|
3052
|
+
return ratio >= run.ratios[0] - TURN_DEPTH_EPS && ratio <= run.ratios[1] + TURN_DEPTH_EPS;
|
|
3053
|
+
}
|
|
2708
3054
|
function ratioClusters(candidates) {
|
|
2709
3055
|
const clusters = [];
|
|
2710
3056
|
let run = [];
|
|
@@ -2727,7 +3073,7 @@ function ratioClusters(candidates) {
|
|
|
2727
3073
|
close();
|
|
2728
3074
|
return clusters;
|
|
2729
3075
|
}
|
|
2730
|
-
function fitTurnRadius(ctx, candidates, target) {
|
|
3076
|
+
function fitTurnRadius(ctx, candidates, target, accept) {
|
|
2731
3077
|
let lo = candidates[0];
|
|
2732
3078
|
let hi = candidates[candidates.length - 1];
|
|
2733
3079
|
for (let i = candidates.length - 2; i >= 0; i--) {
|
|
@@ -2737,13 +3083,42 @@ function fitTurnRadius(ctx, candidates, target) {
|
|
|
2737
3083
|
break;
|
|
2738
3084
|
}
|
|
2739
3085
|
}
|
|
3086
|
+
return bisectTurnRadius(
|
|
3087
|
+
lo,
|
|
3088
|
+
hi,
|
|
3089
|
+
target,
|
|
3090
|
+
(radius) => {
|
|
3091
|
+
const mid = evaluateTurnCandidate(ctx, radius);
|
|
3092
|
+
return "blocked" in mid ? void 0 : mid;
|
|
3093
|
+
},
|
|
3094
|
+
accept
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
function bisectTurnRadius(lo, hi, target, evaluate, accept) {
|
|
3098
|
+
if (!accept(lo) || !accept(hi)) {
|
|
3099
|
+
throw new Error(
|
|
3100
|
+
"auto-rig: bisectTurnRadius: a bracket end is not accepted"
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
const miss = (c) => Math.abs(c.ratio - target);
|
|
3104
|
+
let nearest = miss(lo) <= miss(hi) ? lo : hi;
|
|
2740
3105
|
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2741
|
-
const mid =
|
|
2742
|
-
if (
|
|
3106
|
+
const mid = evaluate(Math.sqrt(lo.radius * hi.radius));
|
|
3107
|
+
if (mid === void 0) break;
|
|
3108
|
+
if (accept(mid) && miss(mid) < miss(nearest)) nearest = mid;
|
|
2743
3109
|
if ((mid.ratio - target) * (lo.ratio - target) <= 0) hi = mid;
|
|
2744
3110
|
else lo = mid;
|
|
2745
3111
|
}
|
|
2746
|
-
|
|
3112
|
+
if (accept(lo) && accept(hi)) return miss(lo) <= miss(hi) ? lo : hi;
|
|
3113
|
+
return nearest;
|
|
3114
|
+
}
|
|
3115
|
+
function strandPreferredCandidates(candidates, farEyeRatio) {
|
|
3116
|
+
const feasible = candidates.filter((c) => c.strandFeasible);
|
|
3117
|
+
if (feasible.length === candidates.length) return candidates;
|
|
3118
|
+
const holds = ratioClusters(feasible).some(
|
|
3119
|
+
(run) => runHolds(run, farEyeRatio)
|
|
3120
|
+
);
|
|
3121
|
+
return holds ? feasible : candidates;
|
|
2747
3122
|
}
|
|
2748
3123
|
function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
2749
3124
|
const solve = (measured, marks, share) => solveTurnDepthSigned(
|
|
@@ -2758,13 +3133,38 @@ function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
|
2758
3133
|
mouth: solve(ctx.mouthShift, ctx.landmarks.mouth, MOUTH_SHIFT_SHARE)
|
|
2759
3134
|
};
|
|
2760
3135
|
}
|
|
2761
|
-
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges) {
|
|
3136
|
+
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges, strandEdges) {
|
|
2762
3137
|
if (targets.headHalfWidth !== void 0 && targets.headHalfWidth <= faceHalfWidth) {
|
|
2763
3138
|
throw new TurnTargetError(
|
|
2764
3139
|
`auto-rig: turnTargets.headHalfWidth (${targets.headHalfWidth}) must be wider than the face plate's own half-width (${faceHalfWidth})`
|
|
2765
3140
|
);
|
|
2766
3141
|
}
|
|
2767
3142
|
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
3143
|
+
let strands;
|
|
3144
|
+
const strandSides = ["left", "right"].filter(
|
|
3145
|
+
(key) => strandEdges?.[key] !== void 0
|
|
3146
|
+
);
|
|
3147
|
+
if (strandSides.length > 0) {
|
|
3148
|
+
if (hairFront === void 0) {
|
|
3149
|
+
throw new Error(
|
|
3150
|
+
"auto-rig: solveTurnModel: strandEdges was given, but there is no hair_front to land its runs on"
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
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);
|
|
3154
|
+
strands = strandSides.map((key) => {
|
|
3155
|
+
const carrier = irises.length === 2 ? irises[key === "left" ? 0 : 1] : void 0;
|
|
3156
|
+
if (carrier === void 0) {
|
|
3157
|
+
throw new Error(
|
|
3158
|
+
`auto-rig: solveTurnModel: strandEdges.${key} has no iris carrier to land`
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
3161
|
+
return {
|
|
3162
|
+
...strandEdges[key],
|
|
3163
|
+
side: key === "left" ? -1 : 1,
|
|
3164
|
+
carrier
|
|
3165
|
+
};
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
2768
3168
|
const ctx = {
|
|
2769
3169
|
landmarks,
|
|
2770
3170
|
lattice,
|
|
@@ -2796,9 +3196,9 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
|
|
|
2796
3196
|
clampSilhouetteRatio: targets.defaulted.has("silhouetteRatio"),
|
|
2797
3197
|
hairFrontSilhouette: hairFront,
|
|
2798
3198
|
// A companion to a MEASURED headHalfWidth only — see its own doc.
|
|
2799
|
-
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges
|
|
3199
|
+
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges,
|
|
3200
|
+
strands
|
|
2800
3201
|
};
|
|
2801
|
-
const clamped = [];
|
|
2802
3202
|
const clampInto = (value, [lo, hi]) => Math.min(Math.max(value, lo), hi);
|
|
2803
3203
|
const pass = sweepTurnRadii(ctx);
|
|
2804
3204
|
if (pass.candidates.length === 0) {
|
|
@@ -2829,92 +3229,108 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
|
|
|
2829
3229
|
attainable: pass.heldRatioLower === Infinity || pass.heldRatioUpper === -Infinity ? void 0 : [pass.heldRatioLower, pass.heldRatioUpper]
|
|
2830
3230
|
};
|
|
2831
3231
|
}
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
let cluster = clusters.find(
|
|
2840
|
-
(c) => farEyeRatio >= c.ratios[0] - TURN_DEPTH_EPS && farEyeRatio <= c.ratios[1] + TURN_DEPTH_EPS
|
|
2841
|
-
);
|
|
2842
|
-
if (cluster === void 0) {
|
|
2843
|
-
cluster = clusters.reduce(
|
|
2844
|
-
(a, b) => distanceTo(b) < distanceTo(a) ? b : a
|
|
3232
|
+
const fitFrom = (pool, accept) => {
|
|
3233
|
+
const clamped = [];
|
|
3234
|
+
const clusters = ratioClusters(pool);
|
|
3235
|
+
const distanceTo = (c) => Math.max(
|
|
3236
|
+
c.ratios[0] - targets.farEyeRatio,
|
|
3237
|
+
targets.farEyeRatio - c.ratios[1],
|
|
3238
|
+
0
|
|
2845
3239
|
);
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
3240
|
+
let farEyeRatio = targets.farEyeRatio;
|
|
3241
|
+
let cluster = clusters.find((c) => runHolds(c, farEyeRatio));
|
|
3242
|
+
if (cluster === void 0) {
|
|
3243
|
+
cluster = clusters.reduce(
|
|
3244
|
+
(a, b) => distanceTo(b) < distanceTo(a) ? b : a
|
|
3245
|
+
);
|
|
3246
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3247
|
+
return {
|
|
3248
|
+
unreachable: true,
|
|
3249
|
+
field: "farEyeRatio",
|
|
3250
|
+
value: targets.farEyeRatio,
|
|
3251
|
+
attainable: cluster.ratios
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
farEyeRatio = clampInto(farEyeRatio, cluster.ratios);
|
|
3255
|
+
clamped.push("farEyeRatio");
|
|
3256
|
+
}
|
|
3257
|
+
const best = fitTurnRadius(ctx, cluster.candidates, farEyeRatio, accept);
|
|
3258
|
+
if (Math.abs(best.ratio - farEyeRatio) > TURN_DEPTH_EPS) {
|
|
3259
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3260
|
+
return {
|
|
3261
|
+
unreachable: true,
|
|
3262
|
+
field: "farEyeRatio",
|
|
3263
|
+
value: targets.farEyeRatio,
|
|
3264
|
+
attainable: cluster.ratios
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
if (!clamped.includes("farEyeRatio")) clamped.push("farEyeRatio");
|
|
3268
|
+
}
|
|
3269
|
+
if (!best.eye.reached) clamped.push("eyeShift");
|
|
3270
|
+
if (best.silhouetteClamped) clamped.push("silhouetteRatio");
|
|
3271
|
+
const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;
|
|
3272
|
+
for (const [field, solution] of [
|
|
3273
|
+
["noseShift", best.nose],
|
|
3274
|
+
["mouthShift", best.mouth]
|
|
3275
|
+
]) {
|
|
3276
|
+
if (solution.reached) continue;
|
|
3277
|
+
if (!targets.defaulted.has(field)) {
|
|
3278
|
+
return {
|
|
3279
|
+
unreachable: true,
|
|
3280
|
+
field,
|
|
3281
|
+
value: targets[field],
|
|
3282
|
+
// noseShift/mouthShift are MAGNITUDES too (see
|
|
3283
|
+
// solveFeatureDepths), so a negative lower bound here would
|
|
3284
|
+
// advertise a value the field's own contract already rules out; 0
|
|
3285
|
+
// is the true floor.
|
|
3286
|
+
attainable: [
|
|
3287
|
+
Math.max(
|
|
3288
|
+
0,
|
|
3289
|
+
(-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh
|
|
3290
|
+
),
|
|
3291
|
+
(-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh
|
|
3292
|
+
]
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
clamped.push(field);
|
|
2866
3296
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
3297
|
+
return {
|
|
3298
|
+
unreachable: false,
|
|
3299
|
+
radius: best.radius,
|
|
3300
|
+
holdBase: best.holdBase,
|
|
3301
|
+
holdEdgeAt: best.holdEdgeAt,
|
|
3302
|
+
travel: best.travel,
|
|
3303
|
+
surface: best.surface,
|
|
3304
|
+
depths: {
|
|
3305
|
+
eye: best.eye.depth,
|
|
3306
|
+
nose: best.nose.depth,
|
|
3307
|
+
mouth: best.mouth.depth
|
|
3308
|
+
},
|
|
3309
|
+
achieved: {
|
|
3310
|
+
eyeShift,
|
|
3311
|
+
farEyeRatio: best.ratio,
|
|
3312
|
+
silhouetteRatio: best.renderedSilhouetteRatio
|
|
3313
|
+
},
|
|
3314
|
+
clamped,
|
|
3315
|
+
...best.strandOverlap === void 0 ? {} : { strandOverlap: best.strandOverlap }
|
|
3316
|
+
};
|
|
3317
|
+
};
|
|
3318
|
+
const preferred = strandPreferredCandidates(
|
|
3319
|
+
pass.candidates,
|
|
3320
|
+
targets.farEyeRatio
|
|
3321
|
+
);
|
|
3322
|
+
if (preferred !== pass.candidates) {
|
|
3323
|
+
const fitted = fitFrom(preferred, (c) => c.strandFeasible);
|
|
3324
|
+
if (!fitted.unreachable && !fitted.clamped.includes("farEyeRatio")) {
|
|
3325
|
+
return fitted;
|
|
2893
3326
|
}
|
|
2894
|
-
clamped.push(field);
|
|
2895
3327
|
}
|
|
2896
|
-
return
|
|
2897
|
-
unreachable: false,
|
|
2898
|
-
radius: best.radius,
|
|
2899
|
-
holdBase: best.holdBase,
|
|
2900
|
-
holdEdgeAt: best.holdEdgeAt,
|
|
2901
|
-
travel: best.travel,
|
|
2902
|
-
surface: best.surface,
|
|
2903
|
-
depths: {
|
|
2904
|
-
eye: best.eye.depth,
|
|
2905
|
-
nose: best.nose.depth,
|
|
2906
|
-
mouth: best.mouth.depth
|
|
2907
|
-
},
|
|
2908
|
-
achieved: {
|
|
2909
|
-
eyeShift,
|
|
2910
|
-
farEyeRatio: best.ratio,
|
|
2911
|
-
silhouetteRatio: best.renderedSilhouetteRatio
|
|
2912
|
-
},
|
|
2913
|
-
clamped
|
|
2914
|
-
};
|
|
3328
|
+
return fitFrom(pass.candidates, () => true);
|
|
2915
3329
|
}
|
|
2916
3330
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
2917
3331
|
var HAIR_FRONT_DEPTH = 0.1;
|
|
3332
|
+
var MOUTH_TURN_TILT_DEG = 5;
|
|
3333
|
+
var NOSE_TURN_TILT_DEG = 6;
|
|
2918
3334
|
var NOD_TRAVEL = 30;
|
|
2919
3335
|
var NOD_BEND = 0.5;
|
|
2920
3336
|
var HAIR_BACK_NOD_DEPTH = -0.135;
|
|
@@ -3521,11 +3937,37 @@ function turnSetup(layers) {
|
|
|
3521
3937
|
)
|
|
3522
3938
|
);
|
|
3523
3939
|
}
|
|
3940
|
+
const mouthAt = members.find((m) => m.role === "mouth").transform;
|
|
3941
|
+
const mouthAnchor = {
|
|
3942
|
+
x: mouthAt.x,
|
|
3943
|
+
y: mouthAt.y,
|
|
3944
|
+
tiltDeg: MOUTH_TURN_TILT_DEG
|
|
3945
|
+
};
|
|
3946
|
+
const noseLayer = layers.find((l) => l.role === "nose");
|
|
3947
|
+
let noseAnchor;
|
|
3948
|
+
if (noseLayer) {
|
|
3949
|
+
const drawn = noseDrawnBox(noseLayer);
|
|
3950
|
+
const at = bboxToTransform(
|
|
3951
|
+
drawn,
|
|
3952
|
+
noseLayer.canvasW,
|
|
3953
|
+
noseLayer.canvasH,
|
|
3954
|
+
"nose"
|
|
3955
|
+
);
|
|
3956
|
+
noseAnchor = {
|
|
3957
|
+
x: at.x,
|
|
3958
|
+
y: at.y + drawn.h / 2,
|
|
3959
|
+
tiltDeg: NOSE_TURN_TILT_DEG
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3524
3962
|
const carriers = new Map(
|
|
3525
|
-
members.map((m) =>
|
|
3526
|
-
m.
|
|
3527
|
-
|
|
3528
|
-
|
|
3963
|
+
members.map((m) => {
|
|
3964
|
+
const grid = groupGrids.get(m.group);
|
|
3965
|
+
const anchor = m.group === "mouthWarp" ? mouthAnchor : m.group === "noseWarp" ? noseAnchor : void 0;
|
|
3966
|
+
return [
|
|
3967
|
+
m.role,
|
|
3968
|
+
anchor === void 0 ? { grid, part: m.part } : { grid, part: m.part, anchor }
|
|
3969
|
+
];
|
|
3970
|
+
})
|
|
3529
3971
|
);
|
|
3530
3972
|
const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
|
|
3531
3973
|
let reach = 0;
|
|
@@ -3595,6 +4037,7 @@ function solveInputsOf(s) {
|
|
|
3595
4037
|
}
|
|
3596
4038
|
function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
3597
4039
|
validateLayerInputs(layers, canvas);
|
|
4040
|
+
validateStrandEdges(layers, options.strandEdges);
|
|
3598
4041
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
3599
4042
|
const bodyLayer = layers.find((l) => l.role === "body");
|
|
3600
4043
|
const parameters = [...STANDARD_PARAMETERS];
|
|
@@ -3630,6 +4073,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3630
4073
|
plateGuardRows,
|
|
3631
4074
|
members,
|
|
3632
4075
|
groupGrids,
|
|
4076
|
+
carriers,
|
|
3633
4077
|
parallaxUnitY,
|
|
3634
4078
|
hasNose,
|
|
3635
4079
|
hasMouthOpen
|
|
@@ -3638,7 +4082,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3638
4082
|
const turn = hasNose ? solveTurnModel(
|
|
3639
4083
|
resolveTurnTargets(options.turnTargets),
|
|
3640
4084
|
...solveInputsOf(setup),
|
|
3641
|
-
options.headEdges
|
|
4085
|
+
options.headEdges,
|
|
4086
|
+
options.strandEdges
|
|
3642
4087
|
) : void 0;
|
|
3643
4088
|
if (turn?.unreachable) {
|
|
3644
4089
|
throw new TurnTargetError(
|
|
@@ -3656,7 +4101,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3656
4101
|
holdBase: turn.holdBase,
|
|
3657
4102
|
depths: turn.depths,
|
|
3658
4103
|
achieved: turn.achieved,
|
|
3659
|
-
clamped: turn.clamped
|
|
4104
|
+
clamped: turn.clamped,
|
|
4105
|
+
...turn.strandOverlap === void 0 ? {} : { strandOverlap: turn.strandOverlap }
|
|
3660
4106
|
});
|
|
3661
4107
|
}
|
|
3662
4108
|
const latticeReach = Math.max(
|
|
@@ -3729,7 +4175,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3729
4175
|
}
|
|
3730
4176
|
];
|
|
3731
4177
|
for (const [group, grid] of groupGrids) {
|
|
3732
|
-
const
|
|
4178
|
+
const { role } = members.find((m) => m.group === group);
|
|
4179
|
+
const family = turnFamily(role);
|
|
3733
4180
|
const depth = group === "faceWarp" ? 0 : turn?.depths[family] ?? 0;
|
|
3734
4181
|
deformers.push({
|
|
3735
4182
|
kind: "warp",
|
|
@@ -3741,7 +4188,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3741
4188
|
StandardParameter.AngleX,
|
|
3742
4189
|
StandardParameter.AngleY,
|
|
3743
4190
|
surface,
|
|
3744
|
-
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG
|
|
4191
|
+
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG,
|
|
4192
|
+
carriers.get(role).anchor
|
|
3745
4193
|
)
|
|
3746
4194
|
});
|
|
3747
4195
|
}
|