@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.mjs
CHANGED
|
@@ -1778,6 +1778,125 @@ function validateLayerInputs(layers, canvas) {
|
|
|
1778
1778
|
}
|
|
1779
1779
|
}
|
|
1780
1780
|
}
|
|
1781
|
+
function validateStrandEdges(layers, strandEdges) {
|
|
1782
|
+
if (strandEdges === void 0) return;
|
|
1783
|
+
const where = "auto-rig: validateStrandEdges: strandEdges";
|
|
1784
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1785
|
+
if (!isPlainObject(strandEdges)) {
|
|
1786
|
+
throw new Error(
|
|
1787
|
+
`${where} must be a plain object with optional left / right sides`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
for (const key of ["left", "right"]) {
|
|
1791
|
+
const raw = strandEdges[key];
|
|
1792
|
+
if (raw === void 0) continue;
|
|
1793
|
+
const path = `${where}.${key}`;
|
|
1794
|
+
if (!isPlainObject(raw)) {
|
|
1795
|
+
throw new Error(`${path} must be a plain object`);
|
|
1796
|
+
}
|
|
1797
|
+
for (const field of ["y", "irisOuter", "irisInner", "runOuter"]) {
|
|
1798
|
+
const v = raw[field];
|
|
1799
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1800
|
+
throw new Error(
|
|
1801
|
+
`${path}.${field} is ${String(v)}, not a finite number`
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
const rawFace = raw.runFace;
|
|
1806
|
+
if (rawFace !== null && (typeof rawFace !== "number" || !Number.isFinite(rawFace))) {
|
|
1807
|
+
throw new Error(
|
|
1808
|
+
`${path}.runFace is ${String(rawFace)}, not a finite number or null`
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
const { y, irisOuter, irisInner, runOuter, runFace } = raw;
|
|
1812
|
+
const cropOf = (role) => {
|
|
1813
|
+
const layer = layers.find((l) => l.role === role);
|
|
1814
|
+
if (layer === void 0) {
|
|
1815
|
+
throw new Error(
|
|
1816
|
+
`${path} needs a "${role}" layer to be measured on, and this layer set has none`
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
1820
|
+
return {
|
|
1821
|
+
role,
|
|
1822
|
+
x: t.x,
|
|
1823
|
+
left: t.x - layer.cropW / 2,
|
|
1824
|
+
right: t.x + layer.cropW / 2,
|
|
1825
|
+
bottom: t.y - layer.cropH / 2,
|
|
1826
|
+
top: t.y + layer.cropH / 2
|
|
1827
|
+
};
|
|
1828
|
+
};
|
|
1829
|
+
const hair = cropOf("hair_front");
|
|
1830
|
+
const [lowIris, highIris] = [cropOf("iris_L"), cropOf("iris_R")].sort(
|
|
1831
|
+
(a, b) => a.x - b.x
|
|
1832
|
+
);
|
|
1833
|
+
const sign = key === "left" ? -1 : 1;
|
|
1834
|
+
const iris = key === "left" ? lowIris : highIris;
|
|
1835
|
+
const other = key === "left" ? highIris : lowIris;
|
|
1836
|
+
const fail = (field, value, why) => new Error(`${path}.${field} (${value}) ${why}`);
|
|
1837
|
+
for (const crop of [iris, hair]) {
|
|
1838
|
+
if (y < crop.bottom || y > crop.top) {
|
|
1839
|
+
throw fail(
|
|
1840
|
+
"y",
|
|
1841
|
+
y,
|
|
1842
|
+
`lies outside the "${crop.role}" crop's rows (${crop.bottom}\u2026${crop.top})`
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
for (const [field, x] of [
|
|
1847
|
+
["irisOuter", irisOuter],
|
|
1848
|
+
["irisInner", irisInner]
|
|
1849
|
+
]) {
|
|
1850
|
+
if (x < iris.left || x > iris.right) {
|
|
1851
|
+
throw fail(
|
|
1852
|
+
field,
|
|
1853
|
+
x,
|
|
1854
|
+
`lies outside the "${iris.role}" crop's columns (${iris.left}\u2026${iris.right})`
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
if (sign * (irisOuter - irisInner) <= 0) {
|
|
1859
|
+
throw fail(
|
|
1860
|
+
"irisOuter",
|
|
1861
|
+
irisOuter,
|
|
1862
|
+
`must lie strictly outward of strandEdges.${key}.irisInner (${irisInner})`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
for (const [field, x] of [
|
|
1866
|
+
["runOuter", runOuter],
|
|
1867
|
+
["runFace", runFace]
|
|
1868
|
+
]) {
|
|
1869
|
+
if (x !== null && (x < hair.left || x > hair.right)) {
|
|
1870
|
+
throw fail(
|
|
1871
|
+
field,
|
|
1872
|
+
x,
|
|
1873
|
+
`lies outside the "hair_front" crop's columns (${hair.left}\u2026${hair.right})`
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (sign * (runOuter - iris.x) <= 0) {
|
|
1878
|
+
throw fail(
|
|
1879
|
+
"runOuter",
|
|
1880
|
+
runOuter,
|
|
1881
|
+
`must lie strictly outward of the "${iris.role}" crop's centre (${iris.x})`
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
if (runFace !== null && sign * (runOuter - runFace) <= 0) {
|
|
1885
|
+
throw fail(
|
|
1886
|
+
"runOuter",
|
|
1887
|
+
runOuter,
|
|
1888
|
+
`must lie strictly outward of strandEdges.${key}.runFace (${runFace})`
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
if (runFace !== null && sign * (runFace - other.x) <= 0) {
|
|
1892
|
+
throw fail(
|
|
1893
|
+
"runFace",
|
|
1894
|
+
runFace,
|
|
1895
|
+
`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`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1781
1900
|
function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
1782
1901
|
const pts = [];
|
|
1783
1902
|
for (let row = 0; row <= rows; row++) {
|
|
@@ -1934,21 +2053,26 @@ function turnSurface(spec) {
|
|
|
1934
2053
|
)
|
|
1935
2054
|
};
|
|
1936
2055
|
}
|
|
1937
|
-
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt) {
|
|
2056
|
+
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt, anchor) {
|
|
1938
2057
|
const STOPS = [...HEAD_TURN_STOPS];
|
|
1939
2058
|
const pointCount = grid.points.length / 2;
|
|
1940
2059
|
const keyforms2d = [];
|
|
1941
2060
|
for (const angleY of STOPS) {
|
|
1942
2061
|
for (const angleX of STOPS) {
|
|
1943
2062
|
const shift = shiftAt(angleX);
|
|
2063
|
+
const mapAt = (nodeY) => surface.mapAt(angleX, nodeY);
|
|
1944
2064
|
const offsets = [];
|
|
1945
2065
|
for (let i = 0; i < pointCount; i++) {
|
|
1946
2066
|
const x = grid.points[i * 2];
|
|
1947
2067
|
const y = grid.points[i * 2 + 1];
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2068
|
+
const nodDy = angleY === 0 ? 0 : surface.nodBendAt(y, angleY);
|
|
2069
|
+
if (angleX === 0) {
|
|
2070
|
+
offsets.push(0, nodDy);
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
const landed = groupNodeLanding(anchor, mapAt, angleX, shift, x, y);
|
|
2074
|
+
const turnDy = landed.y - y;
|
|
2075
|
+
offsets.push(landed.x - x, angleY === 0 ? turnDy : turnDy + nodDy);
|
|
1952
2076
|
}
|
|
1953
2077
|
keyforms2d.push({ offsets });
|
|
1954
2078
|
}
|
|
@@ -2068,6 +2192,22 @@ function carrierLandingX(carrier, nodeLanding, px, py) {
|
|
|
2068
2192
|
py
|
|
2069
2193
|
);
|
|
2070
2194
|
}
|
|
2195
|
+
function groupNodeLanding(anchor, mapAt, deg, shift, nodeX, nodeY) {
|
|
2196
|
+
if (anchor === void 0) {
|
|
2197
|
+
return { x: mapAt(nodeY).mapX(nodeX + shift), y: nodeY };
|
|
2198
|
+
}
|
|
2199
|
+
const anchorRow = mapAt(anchor.y);
|
|
2200
|
+
const pivotX = anchorRow.mapX(anchor.x + shift);
|
|
2201
|
+
const dx = mapAt(nodeY).mapX(nodeX) - anchorRow.mapX(anchor.x);
|
|
2202
|
+
const dy = nodeY - anchor.y;
|
|
2203
|
+
const phi = anchor.tiltDeg * deg / HEAD_TURN_MAX_DEG * (Math.PI / 180);
|
|
2204
|
+
const cos = Math.cos(phi);
|
|
2205
|
+
const sin = Math.sin(phi);
|
|
2206
|
+
return {
|
|
2207
|
+
x: pivotX + cos * dx - sin * dy,
|
|
2208
|
+
y: anchor.y + sin * dx + cos * dy
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2071
2211
|
function rowMapsOf(surface) {
|
|
2072
2212
|
const maps = /* @__PURE__ */ new Map();
|
|
2073
2213
|
return (deg, nodeY) => {
|
|
@@ -2221,11 +2361,12 @@ function eyeRowOf(landmarks, fallback) {
|
|
|
2221
2361
|
}
|
|
2222
2362
|
var TURN_BISECT_STEPS = 40;
|
|
2223
2363
|
var TURN_DEPTH_EPS = 1e-6;
|
|
2224
|
-
function landmarkLandingX(landmark, mapAt, shift, px, py) {
|
|
2225
|
-
|
|
2364
|
+
function landmarkLandingX(landmark, mapAt, deg, shift, px, py) {
|
|
2365
|
+
const { carrier } = landmark;
|
|
2366
|
+
if (carrier === void 0) return mapAt(py).mapX(px + shift);
|
|
2226
2367
|
return carrierLandingX(
|
|
2227
|
-
|
|
2228
|
-
(nodeX, nodeY) =>
|
|
2368
|
+
carrier,
|
|
2369
|
+
(nodeX, nodeY) => groupNodeLanding(carrier.anchor, mapAt, deg, shift, nodeX, nodeY).x,
|
|
2229
2370
|
px,
|
|
2230
2371
|
py
|
|
2231
2372
|
);
|
|
@@ -2234,22 +2375,47 @@ function familyReachPx(landmarks, plateEdgeX) {
|
|
|
2234
2375
|
const farEdge = Math.min(...landmarks.map((l) => l.x - l.w / 2));
|
|
2235
2376
|
return Math.max(0, farEdge - plateEdgeX);
|
|
2236
2377
|
}
|
|
2237
|
-
function
|
|
2378
|
+
function familyDepthCap(landmarks, plateEdgeX, unit) {
|
|
2379
|
+
return familyReachPx(landmarks, plateEdgeX) / unit;
|
|
2380
|
+
}
|
|
2381
|
+
function solveTurnDepthSigned(target, landmarks, unit, mapAt, plateEdgeX, maxDepth) {
|
|
2238
2382
|
if (landmarks.length === 0) {
|
|
2239
2383
|
throw new Error("auto-rig: solveTurnDepth: no landmark to slide");
|
|
2240
2384
|
}
|
|
2241
|
-
const
|
|
2385
|
+
const plateCap = familyDepthCap(landmarks, plateEdgeX, unit);
|
|
2386
|
+
const cap = maxDepth === void 0 ? plateCap : Math.min(plateCap, maxDepth);
|
|
2242
2387
|
const achieved = (d) => landmarks.reduce(
|
|
2243
|
-
(sum, l) => sum + (landmarkLandingX(
|
|
2388
|
+
(sum, l) => sum + (landmarkLandingX(
|
|
2389
|
+
l,
|
|
2390
|
+
mapAt,
|
|
2391
|
+
-HEAD_TURN_MAX_DEG,
|
|
2392
|
+
-d * unit,
|
|
2393
|
+
l.x,
|
|
2394
|
+
l.y ?? 0
|
|
2395
|
+
) - l.x),
|
|
2244
2396
|
0
|
|
2245
2397
|
) / landmarks.length;
|
|
2246
2398
|
const atCap = achieved(cap);
|
|
2247
2399
|
const atRest = achieved(0);
|
|
2248
2400
|
const attainable = [atCap, atRest];
|
|
2249
|
-
if (target > atRest + TURN_DEPTH_EPS)
|
|
2250
|
-
return {
|
|
2251
|
-
|
|
2252
|
-
|
|
2401
|
+
if (target > atRest + TURN_DEPTH_EPS) {
|
|
2402
|
+
return {
|
|
2403
|
+
reached: false,
|
|
2404
|
+
depth: 0,
|
|
2405
|
+
achieved: atRest,
|
|
2406
|
+
attainable,
|
|
2407
|
+
limit: "floor"
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
if (target < atCap - TURN_DEPTH_EPS) {
|
|
2411
|
+
return {
|
|
2412
|
+
reached: false,
|
|
2413
|
+
depth: cap,
|
|
2414
|
+
achieved: atCap,
|
|
2415
|
+
attainable,
|
|
2416
|
+
limit: "cap"
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2253
2419
|
const clampedTarget = Math.min(Math.max(target, atCap), atRest);
|
|
2254
2420
|
let lo = 0;
|
|
2255
2421
|
let hi = cap;
|
|
@@ -2279,7 +2445,7 @@ function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, hol
|
|
|
2279
2445
|
}
|
|
2280
2446
|
return x + side * (holdEdge - holdBase);
|
|
2281
2447
|
}
|
|
2282
|
-
function hairFrontLandingAt(x, y, hairFront, holdTargetAt,
|
|
2448
|
+
function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadShift) {
|
|
2283
2449
|
const { cols, rows } = meshCellsFor(hairFront.cropW, hairFront.cropH);
|
|
2284
2450
|
const top = hairFront.centerY + hairFront.cropH / 2;
|
|
2285
2451
|
return renderedLandingX(
|
|
@@ -2294,7 +2460,7 @@ function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadTipShift) {
|
|
|
2294
2460
|
scaleY: 1,
|
|
2295
2461
|
rotation: 0
|
|
2296
2462
|
},
|
|
2297
|
-
(vx, vy) => holdTargetAt(vx, vy)
|
|
2463
|
+
(vx, vy) => holdTargetAt(vx, vy) + leadShift * Math.pow((top - vy) / hairFront.cropH, HAIR_SWAY_CURL),
|
|
2298
2464
|
x,
|
|
2299
2465
|
y
|
|
2300
2466
|
);
|
|
@@ -2327,6 +2493,19 @@ function aitken(x0, x1, x2, cap) {
|
|
|
2327
2493
|
const x = x2 - d2 * d2 / denominator;
|
|
2328
2494
|
return Number.isFinite(x) ? Math.min(cap, Math.max(0, x)) : x2;
|
|
2329
2495
|
}
|
|
2496
|
+
function strandDepthCap(margins, plateCap) {
|
|
2497
|
+
const holds = (depth) => margins.every((m) => m(depth) >= 0);
|
|
2498
|
+
if (holds(plateCap)) return plateCap;
|
|
2499
|
+
if (!holds(0)) return void 0;
|
|
2500
|
+
let lo = 0;
|
|
2501
|
+
let hi = plateCap;
|
|
2502
|
+
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2503
|
+
const mid = (lo + hi) / 2;
|
|
2504
|
+
if (holds(mid)) lo = mid;
|
|
2505
|
+
else hi = mid;
|
|
2506
|
+
}
|
|
2507
|
+
return lo;
|
|
2508
|
+
}
|
|
2330
2509
|
function evaluateTurnCandidate(ctx, radius) {
|
|
2331
2510
|
const surfaceAt = (travel) => turnSurface({
|
|
2332
2511
|
faceCenterX: ctx.faceCenterX,
|
|
@@ -2350,7 +2529,104 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2350
2529
|
return edge === void 0 ? ctx.headHalfWidth : side * (edge - ctx.faceCenterX);
|
|
2351
2530
|
};
|
|
2352
2531
|
const unit = headTurnParallaxUnit(radius);
|
|
2532
|
+
const plateCapOf = (marks) => familyDepthCap(marks, ctx.plateEdgeX, unit);
|
|
2533
|
+
const eyePlateCap = plateCapOf(ctx.landmarks.eye);
|
|
2353
2534
|
const leadTipShift = ctx.hairFrontSilhouette === void 0 ? 0 : HAIR_FRONT_DEPTH * unit;
|
|
2535
|
+
const bangsLandingAt = (hold, plateLandingAt, deg, leadShift, x, y) => hairFrontLandingAt(
|
|
2536
|
+
x,
|
|
2537
|
+
y,
|
|
2538
|
+
ctx.hairFrontSilhouette,
|
|
2539
|
+
(vx, vy) => hairFrontHoldTarget(
|
|
2540
|
+
vx,
|
|
2541
|
+
vy,
|
|
2542
|
+
deg,
|
|
2543
|
+
ctx.faceCenterX,
|
|
2544
|
+
plateLandingAt,
|
|
2545
|
+
ctx.edgeAt,
|
|
2546
|
+
hold.holdBase,
|
|
2547
|
+
hold.holdEdgeAt(deg)
|
|
2548
|
+
),
|
|
2549
|
+
leadShift
|
|
2550
|
+
);
|
|
2551
|
+
const strandStopsOn = (hold) => {
|
|
2552
|
+
const rowMaps = rowMapsOf(hold.surface);
|
|
2553
|
+
const plateLandingAt = plateLandingOn(ctx.plate, rowMaps);
|
|
2554
|
+
return (ctx.strands ?? []).map((strand) => {
|
|
2555
|
+
const iris = {
|
|
2556
|
+
x: strand.carrier.part.x,
|
|
2557
|
+
w: strand.carrier.part.cropW,
|
|
2558
|
+
carrier: strand.carrier
|
|
2559
|
+
};
|
|
2560
|
+
const stops = HEAD_TURN_STOPS.filter(
|
|
2561
|
+
(deg) => Math.sign(deg) === strand.side
|
|
2562
|
+
).map((deg) => {
|
|
2563
|
+
const runAt = (x) => bangsLandingAt(
|
|
2564
|
+
hold,
|
|
2565
|
+
plateLandingAt,
|
|
2566
|
+
deg,
|
|
2567
|
+
leadTipShift * deg / HEAD_TURN_MAX_DEG,
|
|
2568
|
+
x,
|
|
2569
|
+
strand.y
|
|
2570
|
+
);
|
|
2571
|
+
return {
|
|
2572
|
+
deg,
|
|
2573
|
+
runOuter: runAt(strand.runOuter),
|
|
2574
|
+
runFace: strand.runFace === null ? null : runAt(strand.runFace),
|
|
2575
|
+
irisAt: (depth, x) => landmarkLandingX(
|
|
2576
|
+
iris,
|
|
2577
|
+
(y) => rowMaps(deg, y),
|
|
2578
|
+
deg,
|
|
2579
|
+
depth * unit * deg / HEAD_TURN_MAX_DEG,
|
|
2580
|
+
x,
|
|
2581
|
+
strand.y
|
|
2582
|
+
)
|
|
2583
|
+
};
|
|
2584
|
+
});
|
|
2585
|
+
const { runFace } = strand;
|
|
2586
|
+
if (runFace === null) return { strand, stops, margins: void 0 };
|
|
2587
|
+
const allowance = Math.min(0, strand.side * (runFace - strand.irisOuter));
|
|
2588
|
+
const margins = stops.map(
|
|
2589
|
+
(stop) => (depth) => strand.side * (stop.runFace - stop.irisAt(depth, strand.irisOuter)) - allowance
|
|
2590
|
+
);
|
|
2591
|
+
return { strand, stops, margins };
|
|
2592
|
+
});
|
|
2593
|
+
};
|
|
2594
|
+
const strandOverlapOf = (sides, depth) => {
|
|
2595
|
+
const overlap = {};
|
|
2596
|
+
for (const { strand, stops, margins } of sides) {
|
|
2597
|
+
const s = strand.side;
|
|
2598
|
+
const covered = (irisOuter, irisInner, runOuter, runFace) => Math.max(
|
|
2599
|
+
0,
|
|
2600
|
+
Math.min(s * irisOuter, s * runOuter) - Math.max(s * irisInner, runFace === null ? -Infinity : s * runFace)
|
|
2601
|
+
);
|
|
2602
|
+
let worst;
|
|
2603
|
+
for (const stop of stops) {
|
|
2604
|
+
const px = covered(
|
|
2605
|
+
stop.irisAt(depth, strand.irisOuter),
|
|
2606
|
+
stop.irisAt(depth, strand.irisInner),
|
|
2607
|
+
stop.runOuter,
|
|
2608
|
+
stop.runFace
|
|
2609
|
+
);
|
|
2610
|
+
if (px > 0 && (worst === void 0 || px > worst.px)) {
|
|
2611
|
+
worst = { deg: stop.deg, px };
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
if (worst === void 0) continue;
|
|
2615
|
+
overlap[s < 0 ? "left" : "right"] = {
|
|
2616
|
+
deg: worst.deg,
|
|
2617
|
+
px: worst.px,
|
|
2618
|
+
hh: worst.px / ctx.hh,
|
|
2619
|
+
restPx: covered(
|
|
2620
|
+
strand.irisOuter,
|
|
2621
|
+
strand.irisInner,
|
|
2622
|
+
strand.runOuter,
|
|
2623
|
+
strand.runFace
|
|
2624
|
+
),
|
|
2625
|
+
held: margins !== void 0 && margins.every((m) => m(depth) >= 0)
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
return overlap.left === void 0 && overlap.right === void 0 ? void 0 : overlap;
|
|
2629
|
+
};
|
|
2354
2630
|
const evaluateHold = (ratio, depths2) => {
|
|
2355
2631
|
const holdEdgeFrom = (base) => (deg) => base * (1 + (ratio - 1) * Math.abs(deg) / HEAD_TURN_MAX_DEG);
|
|
2356
2632
|
const travel = ctx.headHalfWidth === void 0 ? ctx.travel : Math.min(
|
|
@@ -2400,21 +2676,13 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2400
2676
|
if (edge !== void 0) return edge;
|
|
2401
2677
|
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side * holdBase : ctx.hairFrontSilhouette.x + side * ctx.hairFrontSilhouette.cropW / 2;
|
|
2402
2678
|
};
|
|
2403
|
-
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x :
|
|
2679
|
+
const hairFrontAt = (x) => ctx.hairFrontSilhouette === void 0 ? x : bangsLandingAt(
|
|
2680
|
+
{ holdBase, holdEdgeAt },
|
|
2681
|
+
plateLandingAt,
|
|
2682
|
+
-HEAD_TURN_MAX_DEG,
|
|
2683
|
+
-leadTipShift,
|
|
2404
2684
|
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
|
|
2685
|
+
ctx.eyeRowY
|
|
2418
2686
|
);
|
|
2419
2687
|
const landingOfRole = (role, x) => {
|
|
2420
2688
|
if (role === "hair_front") return hairFrontAt(x);
|
|
@@ -2434,7 +2702,14 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2434
2702
|
const shift = role === "face" ? 0 : -depths2[turnFamily(role)] * unit;
|
|
2435
2703
|
return carrierLandingX(
|
|
2436
2704
|
carrier,
|
|
2437
|
-
(nodeX, nodeY) =>
|
|
2705
|
+
(nodeX, nodeY) => groupNodeLanding(
|
|
2706
|
+
carrier.anchor,
|
|
2707
|
+
mapAt,
|
|
2708
|
+
-HEAD_TURN_MAX_DEG,
|
|
2709
|
+
shift,
|
|
2710
|
+
nodeX,
|
|
2711
|
+
nodeY
|
|
2712
|
+
).x,
|
|
2438
2713
|
x,
|
|
2439
2714
|
ctx.eyeRowY
|
|
2440
2715
|
);
|
|
@@ -2575,12 +2850,25 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2575
2850
|
"auto-rig: evaluateTurnCandidate: the hold it settled on folds"
|
|
2576
2851
|
);
|
|
2577
2852
|
}
|
|
2853
|
+
const strandSides = ctx.strands === void 0 ? void 0 : strandStopsOn(hold);
|
|
2854
|
+
let strandFeasible = true;
|
|
2855
|
+
let maxDepth;
|
|
2856
|
+
if (strandSides !== void 0) {
|
|
2857
|
+
const margins = strandSides.map((s) => s.margins);
|
|
2858
|
+
const cap = margins.every((m) => m !== void 0) ? strandDepthCap(
|
|
2859
|
+
margins.flatMap((m) => m),
|
|
2860
|
+
eyePlateCap
|
|
2861
|
+
) : void 0;
|
|
2862
|
+
strandFeasible = cap !== void 0;
|
|
2863
|
+
maxDepth = cap ?? 0;
|
|
2864
|
+
}
|
|
2578
2865
|
const eye = solveTurnDepthSigned(
|
|
2579
2866
|
hold.silhouetteCenterShift - ctx.eyeShift * ctx.hh,
|
|
2580
2867
|
ctx.landmarks.eye,
|
|
2581
2868
|
unit,
|
|
2582
2869
|
hold.mapAt,
|
|
2583
|
-
ctx.plateEdgeX
|
|
2870
|
+
ctx.plateEdgeX,
|
|
2871
|
+
maxDepth
|
|
2584
2872
|
);
|
|
2585
2873
|
const features = solveFeatureDepths(
|
|
2586
2874
|
ctx,
|
|
@@ -2601,25 +2889,19 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2601
2889
|
seed = depths;
|
|
2602
2890
|
depths = solved;
|
|
2603
2891
|
} else {
|
|
2604
|
-
const capOf = (marks) => familyReachPx(marks, ctx.plateEdgeX) / unit;
|
|
2605
2892
|
depths = {
|
|
2606
|
-
eye: aitken(
|
|
2607
|
-
seed.eye,
|
|
2608
|
-
depths.eye,
|
|
2609
|
-
solved.eye,
|
|
2610
|
-
capOf(ctx.landmarks.eye)
|
|
2611
|
-
),
|
|
2893
|
+
eye: aitken(seed.eye, depths.eye, solved.eye, eyePlateCap),
|
|
2612
2894
|
nose: aitken(
|
|
2613
2895
|
seed.nose,
|
|
2614
2896
|
depths.nose,
|
|
2615
2897
|
solved.nose,
|
|
2616
|
-
|
|
2898
|
+
plateCapOf(ctx.landmarks.nose)
|
|
2617
2899
|
),
|
|
2618
2900
|
mouth: aitken(
|
|
2619
2901
|
seed.mouth,
|
|
2620
2902
|
depths.mouth,
|
|
2621
2903
|
solved.mouth,
|
|
2622
|
-
|
|
2904
|
+
plateCapOf(ctx.landmarks.mouth)
|
|
2623
2905
|
)
|
|
2624
2906
|
};
|
|
2625
2907
|
seed = void 0;
|
|
@@ -2638,7 +2920,7 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2638
2920
|
if (silhouetteClamped && !ctx.clampSilhouetteRatio) {
|
|
2639
2921
|
return silhouetteMiss(span);
|
|
2640
2922
|
}
|
|
2641
|
-
if (!eye.reached && !ctx.clampEyeShift) {
|
|
2923
|
+
if (!eye.reached && eye.limit === "floor" && !ctx.clampEyeShift) {
|
|
2642
2924
|
return {
|
|
2643
2925
|
blocked: "eyeShift",
|
|
2644
2926
|
// eyeShift is a MAGNITUDE (the sign is ignored — see TurnTargets), so
|
|
@@ -2651,9 +2933,24 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2651
2933
|
};
|
|
2652
2934
|
}
|
|
2653
2935
|
const eyeShiftPx = -eye.depth * unit;
|
|
2654
|
-
const scaleOf = (l) => (landmarkLandingX(
|
|
2936
|
+
const scaleOf = (l) => (landmarkLandingX(
|
|
2937
|
+
l,
|
|
2938
|
+
mapAt,
|
|
2939
|
+
-HEAD_TURN_MAX_DEG,
|
|
2940
|
+
eyeShiftPx,
|
|
2941
|
+
l.x + l.w / 2,
|
|
2942
|
+
l.y ?? 0
|
|
2943
|
+
) - landmarkLandingX(
|
|
2944
|
+
l,
|
|
2945
|
+
mapAt,
|
|
2946
|
+
-HEAD_TURN_MAX_DEG,
|
|
2947
|
+
eyeShiftPx,
|
|
2948
|
+
l.x - l.w / 2,
|
|
2949
|
+
l.y ?? 0
|
|
2950
|
+
)) / l.w;
|
|
2655
2951
|
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
2656
2952
|
const near = ctx.landmarks.eye.reduce((a, b) => b.x > a.x ? b : a);
|
|
2953
|
+
const strandOverlap = strandSides === void 0 ? void 0 : strandOverlapOf(strandSides, eye.depth);
|
|
2657
2954
|
return {
|
|
2658
2955
|
radius,
|
|
2659
2956
|
ratio: scaleOf(far) / scaleOf(near),
|
|
@@ -2668,7 +2965,9 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2668
2965
|
mouth: features.mouth,
|
|
2669
2966
|
silhouetteClamped,
|
|
2670
2967
|
silhouetteCenterShift,
|
|
2671
|
-
renderedSilhouetteRatio
|
|
2968
|
+
renderedSilhouetteRatio,
|
|
2969
|
+
strandFeasible,
|
|
2970
|
+
...strandOverlap === void 0 ? {} : { strandOverlap }
|
|
2672
2971
|
};
|
|
2673
2972
|
}
|
|
2674
2973
|
}
|
|
@@ -2705,6 +3004,9 @@ function sweepTurnRadii(ctx) {
|
|
|
2705
3004
|
unsettled
|
|
2706
3005
|
};
|
|
2707
3006
|
}
|
|
3007
|
+
function runHolds(run, ratio) {
|
|
3008
|
+
return ratio >= run.ratios[0] - TURN_DEPTH_EPS && ratio <= run.ratios[1] + TURN_DEPTH_EPS;
|
|
3009
|
+
}
|
|
2708
3010
|
function ratioClusters(candidates) {
|
|
2709
3011
|
const clusters = [];
|
|
2710
3012
|
let run = [];
|
|
@@ -2727,7 +3029,7 @@ function ratioClusters(candidates) {
|
|
|
2727
3029
|
close();
|
|
2728
3030
|
return clusters;
|
|
2729
3031
|
}
|
|
2730
|
-
function fitTurnRadius(ctx, candidates, target) {
|
|
3032
|
+
function fitTurnRadius(ctx, candidates, target, accept) {
|
|
2731
3033
|
let lo = candidates[0];
|
|
2732
3034
|
let hi = candidates[candidates.length - 1];
|
|
2733
3035
|
for (let i = candidates.length - 2; i >= 0; i--) {
|
|
@@ -2737,13 +3039,42 @@ function fitTurnRadius(ctx, candidates, target) {
|
|
|
2737
3039
|
break;
|
|
2738
3040
|
}
|
|
2739
3041
|
}
|
|
3042
|
+
return bisectTurnRadius(
|
|
3043
|
+
lo,
|
|
3044
|
+
hi,
|
|
3045
|
+
target,
|
|
3046
|
+
(radius) => {
|
|
3047
|
+
const mid = evaluateTurnCandidate(ctx, radius);
|
|
3048
|
+
return "blocked" in mid ? void 0 : mid;
|
|
3049
|
+
},
|
|
3050
|
+
accept
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
function bisectTurnRadius(lo, hi, target, evaluate, accept) {
|
|
3054
|
+
if (!accept(lo) || !accept(hi)) {
|
|
3055
|
+
throw new Error(
|
|
3056
|
+
"auto-rig: bisectTurnRadius: a bracket end is not accepted"
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
const miss = (c) => Math.abs(c.ratio - target);
|
|
3060
|
+
let nearest = miss(lo) <= miss(hi) ? lo : hi;
|
|
2740
3061
|
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2741
|
-
const mid =
|
|
2742
|
-
if (
|
|
3062
|
+
const mid = evaluate(Math.sqrt(lo.radius * hi.radius));
|
|
3063
|
+
if (mid === void 0) break;
|
|
3064
|
+
if (accept(mid) && miss(mid) < miss(nearest)) nearest = mid;
|
|
2743
3065
|
if ((mid.ratio - target) * (lo.ratio - target) <= 0) hi = mid;
|
|
2744
3066
|
else lo = mid;
|
|
2745
3067
|
}
|
|
2746
|
-
|
|
3068
|
+
if (accept(lo) && accept(hi)) return miss(lo) <= miss(hi) ? lo : hi;
|
|
3069
|
+
return nearest;
|
|
3070
|
+
}
|
|
3071
|
+
function strandPreferredCandidates(candidates, farEyeRatio) {
|
|
3072
|
+
const feasible = candidates.filter((c) => c.strandFeasible);
|
|
3073
|
+
if (feasible.length === candidates.length) return candidates;
|
|
3074
|
+
const holds = ratioClusters(feasible).some(
|
|
3075
|
+
(run) => runHolds(run, farEyeRatio)
|
|
3076
|
+
);
|
|
3077
|
+
return holds ? feasible : candidates;
|
|
2747
3078
|
}
|
|
2748
3079
|
function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
2749
3080
|
const solve = (measured, marks, share) => solveTurnDepthSigned(
|
|
@@ -2758,13 +3089,38 @@ function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
|
2758
3089
|
mouth: solve(ctx.mouthShift, ctx.landmarks.mouth, MOUTH_SHIFT_SHARE)
|
|
2759
3090
|
};
|
|
2760
3091
|
}
|
|
2761
|
-
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges) {
|
|
3092
|
+
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges, strandEdges) {
|
|
2762
3093
|
if (targets.headHalfWidth !== void 0 && targets.headHalfWidth <= faceHalfWidth) {
|
|
2763
3094
|
throw new TurnTargetError(
|
|
2764
3095
|
`auto-rig: turnTargets.headHalfWidth (${targets.headHalfWidth}) must be wider than the face plate's own half-width (${faceHalfWidth})`
|
|
2765
3096
|
);
|
|
2766
3097
|
}
|
|
2767
3098
|
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
3099
|
+
let strands;
|
|
3100
|
+
const strandSides = ["left", "right"].filter(
|
|
3101
|
+
(key) => strandEdges?.[key] !== void 0
|
|
3102
|
+
);
|
|
3103
|
+
if (strandSides.length > 0) {
|
|
3104
|
+
if (hairFront === void 0) {
|
|
3105
|
+
throw new Error(
|
|
3106
|
+
"auto-rig: solveTurnModel: strandEdges was given, but there is no hair_front to land its runs on"
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
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);
|
|
3110
|
+
strands = strandSides.map((key) => {
|
|
3111
|
+
const carrier = irises.length === 2 ? irises[key === "left" ? 0 : 1] : void 0;
|
|
3112
|
+
if (carrier === void 0) {
|
|
3113
|
+
throw new Error(
|
|
3114
|
+
`auto-rig: solveTurnModel: strandEdges.${key} has no iris carrier to land`
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
return {
|
|
3118
|
+
...strandEdges[key],
|
|
3119
|
+
side: key === "left" ? -1 : 1,
|
|
3120
|
+
carrier
|
|
3121
|
+
};
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
2768
3124
|
const ctx = {
|
|
2769
3125
|
landmarks,
|
|
2770
3126
|
lattice,
|
|
@@ -2796,9 +3152,9 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
|
|
|
2796
3152
|
clampSilhouetteRatio: targets.defaulted.has("silhouetteRatio"),
|
|
2797
3153
|
hairFrontSilhouette: hairFront,
|
|
2798
3154
|
// A companion to a MEASURED headHalfWidth only — see its own doc.
|
|
2799
|
-
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges
|
|
3155
|
+
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges,
|
|
3156
|
+
strands
|
|
2800
3157
|
};
|
|
2801
|
-
const clamped = [];
|
|
2802
3158
|
const clampInto = (value, [lo, hi]) => Math.min(Math.max(value, lo), hi);
|
|
2803
3159
|
const pass = sweepTurnRadii(ctx);
|
|
2804
3160
|
if (pass.candidates.length === 0) {
|
|
@@ -2829,92 +3185,107 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
|
|
|
2829
3185
|
attainable: pass.heldRatioLower === Infinity || pass.heldRatioUpper === -Infinity ? void 0 : [pass.heldRatioLower, pass.heldRatioUpper]
|
|
2830
3186
|
};
|
|
2831
3187
|
}
|
|
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
|
|
3188
|
+
const fitFrom = (pool, accept) => {
|
|
3189
|
+
const clamped = [];
|
|
3190
|
+
const clusters = ratioClusters(pool);
|
|
3191
|
+
const distanceTo = (c) => Math.max(
|
|
3192
|
+
c.ratios[0] - targets.farEyeRatio,
|
|
3193
|
+
targets.farEyeRatio - c.ratios[1],
|
|
3194
|
+
0
|
|
2845
3195
|
);
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
3196
|
+
let farEyeRatio = targets.farEyeRatio;
|
|
3197
|
+
let cluster = clusters.find((c) => runHolds(c, farEyeRatio));
|
|
3198
|
+
if (cluster === void 0) {
|
|
3199
|
+
cluster = clusters.reduce(
|
|
3200
|
+
(a, b) => distanceTo(b) < distanceTo(a) ? b : a
|
|
3201
|
+
);
|
|
3202
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3203
|
+
return {
|
|
3204
|
+
unreachable: true,
|
|
3205
|
+
field: "farEyeRatio",
|
|
3206
|
+
value: targets.farEyeRatio,
|
|
3207
|
+
attainable: cluster.ratios
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
farEyeRatio = clampInto(farEyeRatio, cluster.ratios);
|
|
3211
|
+
clamped.push("farEyeRatio");
|
|
3212
|
+
}
|
|
3213
|
+
const best = fitTurnRadius(ctx, cluster.candidates, farEyeRatio, accept);
|
|
3214
|
+
if (Math.abs(best.ratio - farEyeRatio) > TURN_DEPTH_EPS) {
|
|
3215
|
+
if (!targets.defaulted.has("farEyeRatio")) {
|
|
3216
|
+
return {
|
|
3217
|
+
unreachable: true,
|
|
3218
|
+
field: "farEyeRatio",
|
|
3219
|
+
value: targets.farEyeRatio,
|
|
3220
|
+
attainable: cluster.ratios
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
if (!clamped.includes("farEyeRatio")) clamped.push("farEyeRatio");
|
|
3224
|
+
}
|
|
3225
|
+
if (!best.eye.reached) clamped.push("eyeShift");
|
|
3226
|
+
if (best.silhouetteClamped) clamped.push("silhouetteRatio");
|
|
3227
|
+
const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;
|
|
3228
|
+
for (const [field, solution] of [
|
|
3229
|
+
["noseShift", best.nose],
|
|
3230
|
+
["mouthShift", best.mouth]
|
|
3231
|
+
]) {
|
|
3232
|
+
if (solution.reached) continue;
|
|
3233
|
+
if (!targets.defaulted.has(field)) {
|
|
3234
|
+
return {
|
|
3235
|
+
unreachable: true,
|
|
3236
|
+
field,
|
|
3237
|
+
value: targets[field],
|
|
3238
|
+
// noseShift/mouthShift are MAGNITUDES too (see
|
|
3239
|
+
// solveFeatureDepths), so a negative lower bound here would
|
|
3240
|
+
// advertise a value the field's own contract already rules out; 0
|
|
3241
|
+
// is the true floor.
|
|
3242
|
+
attainable: [
|
|
3243
|
+
Math.max(
|
|
3244
|
+
0,
|
|
3245
|
+
(-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh
|
|
3246
|
+
),
|
|
3247
|
+
(-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh
|
|
3248
|
+
]
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
clamped.push(field);
|
|
2866
3252
|
}
|
|
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
|
-
|
|
3253
|
+
return {
|
|
3254
|
+
unreachable: false,
|
|
3255
|
+
radius: best.radius,
|
|
3256
|
+
holdBase: best.holdBase,
|
|
3257
|
+
holdEdgeAt: best.holdEdgeAt,
|
|
3258
|
+
travel: best.travel,
|
|
3259
|
+
surface: best.surface,
|
|
3260
|
+
depths: {
|
|
3261
|
+
eye: best.eye.depth,
|
|
3262
|
+
nose: best.nose.depth,
|
|
3263
|
+
mouth: best.mouth.depth
|
|
3264
|
+
},
|
|
3265
|
+
achieved: {
|
|
3266
|
+
eyeShift,
|
|
3267
|
+
farEyeRatio: best.ratio,
|
|
3268
|
+
silhouetteRatio: best.renderedSilhouetteRatio
|
|
3269
|
+
},
|
|
3270
|
+
clamped,
|
|
3271
|
+
...best.strandOverlap === void 0 ? {} : { strandOverlap: best.strandOverlap }
|
|
3272
|
+
};
|
|
3273
|
+
};
|
|
3274
|
+
const preferred = strandPreferredCandidates(
|
|
3275
|
+
pass.candidates,
|
|
3276
|
+
targets.farEyeRatio
|
|
3277
|
+
);
|
|
3278
|
+
if (preferred !== pass.candidates) {
|
|
3279
|
+
const fitted = fitFrom(preferred, (c) => c.strandFeasible);
|
|
3280
|
+
if (!fitted.unreachable && !fitted.clamped.includes("farEyeRatio")) {
|
|
3281
|
+
return fitted;
|
|
2893
3282
|
}
|
|
2894
|
-
clamped.push(field);
|
|
2895
3283
|
}
|
|
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
|
-
};
|
|
3284
|
+
return fitFrom(pass.candidates, () => true);
|
|
2915
3285
|
}
|
|
2916
3286
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
2917
3287
|
var HAIR_FRONT_DEPTH = 0.1;
|
|
3288
|
+
var MOUTH_TURN_TILT_DEG = 5;
|
|
2918
3289
|
var NOD_TRAVEL = 30;
|
|
2919
3290
|
var NOD_BEND = 0.5;
|
|
2920
3291
|
var HAIR_BACK_NOD_DEPTH = -0.135;
|
|
@@ -3521,10 +3892,16 @@ function turnSetup(layers) {
|
|
|
3521
3892
|
)
|
|
3522
3893
|
);
|
|
3523
3894
|
}
|
|
3895
|
+
const mouthAt = members.find((m) => m.role === "mouth").transform;
|
|
3896
|
+
const mouthAnchor = {
|
|
3897
|
+
x: mouthAt.x,
|
|
3898
|
+
y: mouthAt.y,
|
|
3899
|
+
tiltDeg: MOUTH_TURN_TILT_DEG
|
|
3900
|
+
};
|
|
3524
3901
|
const carriers = new Map(
|
|
3525
3902
|
members.map((m) => [
|
|
3526
3903
|
m.role,
|
|
3527
|
-
{ grid: groupGrids.get(m.group), part: m.part }
|
|
3904
|
+
m.group === "mouthWarp" ? { grid: groupGrids.get(m.group), part: m.part, anchor: mouthAnchor } : { grid: groupGrids.get(m.group), part: m.part }
|
|
3528
3905
|
])
|
|
3529
3906
|
);
|
|
3530
3907
|
const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
|
|
@@ -3595,6 +3972,7 @@ function solveInputsOf(s) {
|
|
|
3595
3972
|
}
|
|
3596
3973
|
function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
3597
3974
|
validateLayerInputs(layers, canvas);
|
|
3975
|
+
validateStrandEdges(layers, options.strandEdges);
|
|
3598
3976
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
3599
3977
|
const bodyLayer = layers.find((l) => l.role === "body");
|
|
3600
3978
|
const parameters = [...STANDARD_PARAMETERS];
|
|
@@ -3630,6 +4008,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3630
4008
|
plateGuardRows,
|
|
3631
4009
|
members,
|
|
3632
4010
|
groupGrids,
|
|
4011
|
+
carriers,
|
|
3633
4012
|
parallaxUnitY,
|
|
3634
4013
|
hasNose,
|
|
3635
4014
|
hasMouthOpen
|
|
@@ -3638,7 +4017,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3638
4017
|
const turn = hasNose ? solveTurnModel(
|
|
3639
4018
|
resolveTurnTargets(options.turnTargets),
|
|
3640
4019
|
...solveInputsOf(setup),
|
|
3641
|
-
options.headEdges
|
|
4020
|
+
options.headEdges,
|
|
4021
|
+
options.strandEdges
|
|
3642
4022
|
) : void 0;
|
|
3643
4023
|
if (turn?.unreachable) {
|
|
3644
4024
|
throw new TurnTargetError(
|
|
@@ -3656,7 +4036,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3656
4036
|
holdBase: turn.holdBase,
|
|
3657
4037
|
depths: turn.depths,
|
|
3658
4038
|
achieved: turn.achieved,
|
|
3659
|
-
clamped: turn.clamped
|
|
4039
|
+
clamped: turn.clamped,
|
|
4040
|
+
...turn.strandOverlap === void 0 ? {} : { strandOverlap: turn.strandOverlap }
|
|
3660
4041
|
});
|
|
3661
4042
|
}
|
|
3662
4043
|
const latticeReach = Math.max(
|
|
@@ -3729,7 +4110,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3729
4110
|
}
|
|
3730
4111
|
];
|
|
3731
4112
|
for (const [group, grid] of groupGrids) {
|
|
3732
|
-
const
|
|
4113
|
+
const { role } = members.find((m) => m.group === group);
|
|
4114
|
+
const family = turnFamily(role);
|
|
3733
4115
|
const depth = group === "faceWarp" ? 0 : turn?.depths[family] ?? 0;
|
|
3734
4116
|
deformers.push({
|
|
3735
4117
|
kind: "warp",
|
|
@@ -3741,7 +4123,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3741
4123
|
StandardParameter.AngleX,
|
|
3742
4124
|
StandardParameter.AngleY,
|
|
3743
4125
|
surface,
|
|
3744
|
-
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG
|
|
4126
|
+
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG,
|
|
4127
|
+
carriers.get(role).anchor
|
|
3745
4128
|
)
|
|
3746
4129
|
});
|
|
3747
4130
|
}
|