@ikijs/editor 0.5.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 +38 -7
- package/dist/index.d.mts +140 -13
- package/dist/index.d.ts +140 -13
- package/dist/index.js +1634 -711
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1634 -711
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1603,6 +1603,21 @@ import {
|
|
|
1603
1603
|
StandardParameter,
|
|
1604
1604
|
parseIkiModel as parseIkiModel3
|
|
1605
1605
|
} from "@ikijs/format";
|
|
1606
|
+
var TURN_GROUP_IDS = [
|
|
1607
|
+
"faceWarp",
|
|
1608
|
+
"eyeWarp_L",
|
|
1609
|
+
"eyeWarp_R",
|
|
1610
|
+
"mouthWarp",
|
|
1611
|
+
"noseWarp",
|
|
1612
|
+
"browWarp_L",
|
|
1613
|
+
"browWarp_R",
|
|
1614
|
+
"blushWarp_L",
|
|
1615
|
+
"blushWarp_R"
|
|
1616
|
+
];
|
|
1617
|
+
var TURN_GROUP_ID_SET = new Set(TURN_GROUP_IDS);
|
|
1618
|
+
function isTurnGroup(id) {
|
|
1619
|
+
return TURN_GROUP_ID_SET.has(id);
|
|
1620
|
+
}
|
|
1606
1621
|
var ROLE_TABLE = {
|
|
1607
1622
|
// The silhouette behind the face. It rides the rigid head (not faceWarp,
|
|
1608
1623
|
// whose grid it would stretch across the shoulders) as a MESH, so its ends
|
|
@@ -1616,34 +1631,43 @@ var ROLE_TABLE = {
|
|
|
1616
1631
|
// reading as a floating head. Drawn over the back hair so long hair falls
|
|
1617
1632
|
// behind the shoulders.
|
|
1618
1633
|
body: { deformer: "bodyDeformer", order: 5, mesh: false },
|
|
1634
|
+
// The face plate and everything painted on it ride per-group turn warps
|
|
1635
|
+
// under the head (TurnGroupId): the plate its own, each eye stack its
|
|
1636
|
+
// side's, both mouth drawings one, the nose, each brow and each blush their
|
|
1637
|
+
// own. A group's grid is sized to its members alone and baked from the one
|
|
1638
|
+
// solved surface with the family's turn depth in its keyforms, so a feature
|
|
1639
|
+
// slides across the plate on its own grid instead of a translate binding
|
|
1640
|
+
// (see generateIkiFromLayerSet).
|
|
1619
1641
|
face: { deformer: "faceWarp", order: 10, mesh: true },
|
|
1620
|
-
nose: { deformer: "
|
|
1621
|
-
blush_L: { deformer: "
|
|
1622
|
-
blush_R: { deformer: "
|
|
1623
|
-
mouth: { deformer: "
|
|
1642
|
+
nose: { deformer: "noseWarp", order: 15, mesh: true },
|
|
1643
|
+
blush_L: { deformer: "blushWarp_L", order: 20, mesh: true },
|
|
1644
|
+
blush_R: { deformer: "blushWarp_R", order: 20, mesh: true },
|
|
1645
|
+
mouth: { deformer: "mouthWarp", order: 25, mesh: true },
|
|
1624
1646
|
// An OPTIONAL second mouth drawing, open. When present the two cross-fade on
|
|
1625
1647
|
// MouthOpen instead of the closed one being stretched, which is the
|
|
1626
1648
|
// difference between a portrait rig and one that can lip-sync.
|
|
1627
|
-
mouth_open: { deformer: "
|
|
1628
|
-
eye_L: { deformer: "
|
|
1629
|
-
eye_R: { deformer: "
|
|
1630
|
-
iris_L: { deformer: "
|
|
1631
|
-
iris_R: { deformer: "
|
|
1632
|
-
pupil_L: { deformer: "
|
|
1633
|
-
pupil_R: { deformer: "
|
|
1634
|
-
highlight_L: { deformer: "
|
|
1635
|
-
highlight_R: { deformer: "
|
|
1649
|
+
mouth_open: { deformer: "mouthWarp", order: 26, mesh: true },
|
|
1650
|
+
eye_L: { deformer: "eyeWarp_L", order: 30, mesh: true, eyeSide: "L" },
|
|
1651
|
+
eye_R: { deformer: "eyeWarp_R", order: 30, mesh: true, eyeSide: "R" },
|
|
1652
|
+
iris_L: { deformer: "eyeWarp_L", order: 31, mesh: true, eyeSide: "L" },
|
|
1653
|
+
iris_R: { deformer: "eyeWarp_R", order: 31, mesh: true, eyeSide: "R" },
|
|
1654
|
+
pupil_L: { deformer: "eyeWarp_L", order: 32, mesh: true, eyeSide: "L" },
|
|
1655
|
+
pupil_R: { deformer: "eyeWarp_R", order: 32, mesh: true, eyeSide: "R" },
|
|
1656
|
+
highlight_L: { deformer: "eyeWarp_L", order: 33, mesh: true, eyeSide: "L" },
|
|
1657
|
+
highlight_R: { deformer: "eyeWarp_R", order: 33, mesh: true, eyeSide: "R" },
|
|
1636
1658
|
// Upper lashes: an OPTIONAL separate layer ABOVE the iris that folds down to
|
|
1637
1659
|
// the closed-eye seam (the same crease the white folds to), covering the cut
|
|
1638
1660
|
// eyeball cleanly. When absent, the white's own fold is the only closed line.
|
|
1639
|
-
lash_L: { deformer: "
|
|
1640
|
-
lash_R: { deformer: "
|
|
1641
|
-
brow_L: { deformer: "
|
|
1642
|
-
brow_R: { deformer: "
|
|
1643
|
-
//
|
|
1644
|
-
//
|
|
1645
|
-
//
|
|
1646
|
-
|
|
1661
|
+
lash_L: { deformer: "eyeWarp_L", order: 34, mesh: true, eyeSide: "L" },
|
|
1662
|
+
lash_R: { deformer: "eyeWarp_R", order: 34, mesh: true, eyeSide: "R" },
|
|
1663
|
+
brow_L: { deformer: "browWarp_L", order: 40, mesh: true },
|
|
1664
|
+
brow_R: { deformer: "browWarp_R", order: 40, mesh: true },
|
|
1665
|
+
// The bangs ride the rigid head, like the back hair, and bind to no grid:
|
|
1666
|
+
// every motion of their own — the silhouette hold, the turn lead, the nod
|
|
1667
|
+
// and the sway — is a per-vertex warp attached in generateIkiFromLayerSet,
|
|
1668
|
+
// so they draw the head's outline where the plate under them bends and
|
|
1669
|
+
// slides (see bakeHairFrontSilhouetteWarp).
|
|
1670
|
+
hair_front: { deformer: "headDeformer", order: 50, mesh: true }
|
|
1647
1671
|
};
|
|
1648
1672
|
var REQUIRED_ROLES = ["face", "eye_L", "eye_R", "mouth"];
|
|
1649
1673
|
var ALIAS_MAP = {
|
|
@@ -1731,6 +1755,146 @@ function validateLayerInputs(layers, canvas) {
|
|
|
1731
1755
|
`auto-rig: validateLayerInputs: role "${role}" canvas size (${canvasW}\xD7${canvasH}) does not match canvas arg (${canvas.width}\xD7${canvas.height})`
|
|
1732
1756
|
);
|
|
1733
1757
|
}
|
|
1758
|
+
if (layer.rowHalfWidths !== void 0) {
|
|
1759
|
+
const rows = layer.rowHalfWidths;
|
|
1760
|
+
if (!Array.isArray(rows)) {
|
|
1761
|
+
throw new Error(
|
|
1762
|
+
`auto-rig: validateLayerInputs: role "${role}" rowHalfWidths must be an array with one entry per crop row`
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
if (rows.length !== cropH) {
|
|
1766
|
+
throw new Error(
|
|
1767
|
+
`auto-rig: validateLayerInputs: role "${role}" rowHalfWidths has ${rows.length} entries, not one per crop row (cropH ${cropH})`
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1771
|
+
const a = rows[i];
|
|
1772
|
+
if (typeof a !== "number" || !Number.isFinite(a) || a < 0 || a > cropW / 2) {
|
|
1773
|
+
throw new Error(
|
|
1774
|
+
`auto-rig: validateLayerInputs: role "${role}" rowHalfWidths[${i}] is ${String(a)}, not a finite number in [0, ${cropW / 2}] (half the crop's width)`
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
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
|
+
}
|
|
1734
1898
|
}
|
|
1735
1899
|
}
|
|
1736
1900
|
function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
|
|
@@ -1786,20 +1950,16 @@ function meshCellsFor(w, h) {
|
|
|
1786
1950
|
);
|
|
1787
1951
|
return { cols: cells(w), rows: cells(h) };
|
|
1788
1952
|
}
|
|
1789
|
-
var
|
|
1953
|
+
var FACE_PLATE_CELLS = 10;
|
|
1954
|
+
var FEATURE_GRID_CELLS = 4;
|
|
1955
|
+
var TURN_LATTICE_CELL_PX = 16;
|
|
1956
|
+
var GRID_MARGIN = 0.12;
|
|
1790
1957
|
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
1791
1958
|
var HEAD_TURN_MAX_DEG = 30;
|
|
1792
1959
|
var HEAD_TURN_STOPS = [-30, -15, 0, 15, 30];
|
|
1793
1960
|
function headTurnParallaxUnit(radius) {
|
|
1794
1961
|
return radius * Math.sin(HEAD_TURN_MAX_DEG * (Math.PI / 180));
|
|
1795
1962
|
}
|
|
1796
|
-
function gridReach(grid, axis, center) {
|
|
1797
|
-
let reach = 0;
|
|
1798
|
-
for (let i = axis; i < grid.points.length; i += 2) {
|
|
1799
|
-
reach = Math.max(reach, Math.abs(grid.points[i] - center));
|
|
1800
|
-
}
|
|
1801
|
-
return reach;
|
|
1802
|
-
}
|
|
1803
1963
|
function pinnedCylinderBend(local, radius, theta) {
|
|
1804
1964
|
const alpha = Math.asin(Math.max(-1, Math.min(1, local / radius)));
|
|
1805
1965
|
return radius * Math.sin(alpha + theta) - local - radius * Math.sin(theta);
|
|
@@ -1812,27 +1972,107 @@ function boundedCylinderBend(local, radius, theta) {
|
|
|
1812
1972
|
function turnSlide(travel, angleX) {
|
|
1813
1973
|
return travel * angleX / HEAD_TURN_MAX_DEG;
|
|
1814
1974
|
}
|
|
1815
|
-
|
|
1975
|
+
var FACE_ROW_MIN_FRACTION = 0.15;
|
|
1976
|
+
var FACE_ROW_SMOOTHING = 0.05;
|
|
1977
|
+
var CHIN_SWING = 0.08;
|
|
1978
|
+
function faceRowProfile(face, faceCenterY) {
|
|
1979
|
+
const raw = face.rowHalfWidths;
|
|
1980
|
+
if (raw === void 0) return void 0;
|
|
1981
|
+
const n = raw.length;
|
|
1982
|
+
let aMax = 0;
|
|
1983
|
+
let widest = -1;
|
|
1984
|
+
for (let i = 0; i < n; i++) {
|
|
1985
|
+
if (raw[i] > 0 && raw[i] >= aMax) {
|
|
1986
|
+
aMax = raw[i];
|
|
1987
|
+
widest = i;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
if (widest < 0) return void 0;
|
|
1991
|
+
const filled = [];
|
|
1992
|
+
for (let i = 0; i < n; i++) {
|
|
1993
|
+
filled.push(i < widest ? aMax : raw[i] > 0 ? raw[i] : filled[i - 1]);
|
|
1994
|
+
}
|
|
1995
|
+
const half = Math.max(1, Math.round(FACE_ROW_SMOOTHING * n / 2));
|
|
1996
|
+
const floor = FACE_ROW_MIN_FRACTION * aMax;
|
|
1997
|
+
const rows = [];
|
|
1998
|
+
for (let i = 0; i < n; i++) {
|
|
1999
|
+
if (i < widest) {
|
|
2000
|
+
rows.push(aMax);
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
let sum = 0;
|
|
2004
|
+
for (let k = i - half; k <= i + half; k++) {
|
|
2005
|
+
sum += filled[Math.max(0, Math.min(n - 1, k))];
|
|
2006
|
+
}
|
|
2007
|
+
rows.push(Math.max(floor, sum / (2 * half + 1)));
|
|
2008
|
+
}
|
|
2009
|
+
const top = faceCenterY + face.cropH / 2;
|
|
2010
|
+
const rowAt = (y) => Math.max(0, Math.min(n - 1, top - y - 0.5));
|
|
2011
|
+
return {
|
|
2012
|
+
at: (y) => {
|
|
2013
|
+
const r = rowAt(y);
|
|
2014
|
+
const i = Math.floor(r);
|
|
2015
|
+
if (i + 1 >= n) return rows[n - 1];
|
|
2016
|
+
return rows[i] + (rows[i + 1] - rows[i]) * (r - i);
|
|
2017
|
+
},
|
|
2018
|
+
aMax,
|
|
2019
|
+
widestY: top - widest - 0.5,
|
|
2020
|
+
faceHalfWidth: face.cropW / 2
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
function turnSurface(spec) {
|
|
2024
|
+
const { faceCenterX, faceCenterY, radius, travel, nodRadius, lattice } = spec;
|
|
2025
|
+
const { profile } = spec;
|
|
2026
|
+
let radiusAt = (_y) => radius;
|
|
2027
|
+
let swingAt = (_y) => 0;
|
|
2028
|
+
if (profile !== void 0) {
|
|
2029
|
+
const aEye = profile.at(spec.eyeRowY ?? faceCenterY);
|
|
2030
|
+
radiusAt = (y) => radius * (profile.at(y) / aEye);
|
|
2031
|
+
swingAt = (y) => y < profile.widestY ? -CHIN_SWING * profile.faceHalfWidth * Math.max(0, 1 - profile.at(y) / profile.aMax) : 0;
|
|
2032
|
+
}
|
|
2033
|
+
return {
|
|
2034
|
+
faceCenterX,
|
|
2035
|
+
faceCenterY,
|
|
2036
|
+
radius,
|
|
2037
|
+
travel,
|
|
2038
|
+
nodRadius,
|
|
2039
|
+
lattice,
|
|
2040
|
+
radiusAt,
|
|
2041
|
+
swingAt,
|
|
2042
|
+
mapAt: (deg, y) => turnColumnMap(
|
|
2043
|
+
lattice,
|
|
2044
|
+
faceCenterX,
|
|
2045
|
+
radiusAt(y),
|
|
2046
|
+
deg,
|
|
2047
|
+
travel + swingAt(y)
|
|
2048
|
+
),
|
|
2049
|
+
nodBendAt: (y, angleY) => pinnedCylinderBend(
|
|
2050
|
+
y - faceCenterY,
|
|
2051
|
+
nodRadius,
|
|
2052
|
+
angleY * NOD_BEND * (Math.PI / 180)
|
|
2053
|
+
)
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
function bakeTurnGroupWarp2D(grid, parameterX, parameterY, surface, shiftAt, anchor) {
|
|
1816
2057
|
const STOPS = [...HEAD_TURN_STOPS];
|
|
1817
|
-
const RADIUS_Y = gridReach(grid, 1, centerY) * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
1818
2058
|
const pointCount = grid.points.length / 2;
|
|
1819
|
-
const DEG_TO_RAD = Math.PI / 180;
|
|
1820
2059
|
const keyforms2d = [];
|
|
1821
2060
|
for (const angleY of STOPS) {
|
|
1822
|
-
const thetaY = angleY * NOD_BEND * DEG_TO_RAD;
|
|
1823
2061
|
for (const angleX of STOPS) {
|
|
1824
|
-
const
|
|
1825
|
-
const
|
|
2062
|
+
const shift = shiftAt(angleX);
|
|
2063
|
+
const mapAt = (nodeY) => surface.mapAt(angleX, nodeY);
|
|
1826
2064
|
const offsets = [];
|
|
1827
2065
|
for (let i = 0; i < pointCount; i++) {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
);
|
|
2066
|
+
const x = grid.points[i * 2];
|
|
2067
|
+
const y = grid.points[i * 2 + 1];
|
|
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);
|
|
1836
2076
|
}
|
|
1837
2077
|
keyforms2d.push({ offsets });
|
|
1838
2078
|
}
|
|
@@ -1863,30 +2103,163 @@ function turnColumnMap(grid, faceCenterX, radiusX, angleX, travel) {
|
|
|
1863
2103
|
);
|
|
1864
2104
|
}
|
|
1865
2105
|
const cellFor = (v, edges) => {
|
|
1866
|
-
|
|
1867
|
-
|
|
2106
|
+
let lo = 0;
|
|
2107
|
+
let hi = grid.cols - 1;
|
|
2108
|
+
while (lo < hi) {
|
|
2109
|
+
const mid = lo + hi >> 1;
|
|
2110
|
+
if (v < edges[mid + 1]) hi = mid;
|
|
2111
|
+
else lo = mid + 1;
|
|
1868
2112
|
}
|
|
1869
|
-
return
|
|
2113
|
+
return lo;
|
|
2114
|
+
};
|
|
2115
|
+
const sample = (edges, values, v) => {
|
|
2116
|
+
const c = cellFor(v, edges);
|
|
2117
|
+
const s = Math.max(
|
|
2118
|
+
0,
|
|
2119
|
+
Math.min(1, (v - edges[c]) / (edges[c + 1] - edges[c]))
|
|
2120
|
+
);
|
|
2121
|
+
if (s === 0) return values[c];
|
|
2122
|
+
if (s === 1) return values[c + 1];
|
|
2123
|
+
return values[c] + (values[c + 1] - values[c]) * s;
|
|
1870
2124
|
};
|
|
1871
2125
|
return {
|
|
1872
2126
|
restX,
|
|
1873
2127
|
warpedX,
|
|
1874
|
-
mapX(x)
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2128
|
+
mapX: (x) => sample(restX, warpedX, x),
|
|
2129
|
+
invertX: (X) => sample(warpedX, restX, X)
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
function renderedLandingX(part, vertexLanding, px, py) {
|
|
2133
|
+
const { cols, rows } = part;
|
|
2134
|
+
const u = (px - part.x + part.cropW / 2) / part.cropW * cols;
|
|
2135
|
+
const v = (part.y + part.cropH / 2 - py) / part.cropH * rows;
|
|
2136
|
+
const col = Math.max(0, Math.min(cols - 1, Math.floor(u)));
|
|
2137
|
+
const row = Math.max(0, Math.min(rows - 1, Math.floor(v)));
|
|
2138
|
+
const fx = Math.max(0, Math.min(1, u - col));
|
|
2139
|
+
const fy = Math.max(0, Math.min(1, v - row));
|
|
2140
|
+
const cos = Math.cos(part.rotation * (Math.PI / 180));
|
|
2141
|
+
const sin = Math.sin(part.rotation * (Math.PI / 180));
|
|
2142
|
+
const landingAt = (c, r) => {
|
|
2143
|
+
const lx = (-part.cropW / 2 + c / cols * part.cropW) * part.scaleX;
|
|
2144
|
+
const ly = (part.cropH / 2 - r / rows * part.cropH) * part.scaleY;
|
|
2145
|
+
return vertexLanding(
|
|
2146
|
+
part.x + cos * lx - sin * ly,
|
|
2147
|
+
part.y + sin * lx + cos * ly
|
|
2148
|
+
);
|
|
2149
|
+
};
|
|
2150
|
+
const tl = landingAt(col, row);
|
|
2151
|
+
const br = landingAt(col + 1, row + 1);
|
|
2152
|
+
if (fx <= fy) {
|
|
2153
|
+
const bl = landingAt(col, row + 1);
|
|
2154
|
+
return tl + fy * (bl - tl) + fx * (br - bl);
|
|
2155
|
+
}
|
|
2156
|
+
const tr = landingAt(col + 1, row);
|
|
2157
|
+
return tl + fx * (tr - tl) + fy * (br - tr);
|
|
2158
|
+
}
|
|
2159
|
+
function gridChildLandingX(grid, nodeLanding, x, y) {
|
|
2160
|
+
const { cols, rows, points } = grid;
|
|
2161
|
+
const stride = cols + 1;
|
|
2162
|
+
let col = cols - 1;
|
|
2163
|
+
for (let c = 0; c < cols; c++) {
|
|
2164
|
+
if (x < points[(c + 1) * 2]) {
|
|
2165
|
+
col = c;
|
|
2166
|
+
break;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
let row = rows - 1;
|
|
2170
|
+
for (let r = 0; r < rows; r++) {
|
|
2171
|
+
if (y > points[(r + 1) * stride * 2 + 1]) {
|
|
2172
|
+
row = r;
|
|
2173
|
+
break;
|
|
1889
2174
|
}
|
|
2175
|
+
}
|
|
2176
|
+
const xLeft = points[col * 2];
|
|
2177
|
+
const xRight = points[(col + 1) * 2];
|
|
2178
|
+
const yTop = points[row * stride * 2 + 1];
|
|
2179
|
+
const yBottom = points[(row + 1) * stride * 2 + 1];
|
|
2180
|
+
const s = Math.max(0, Math.min(1, (x - xLeft) / (xRight - xLeft)));
|
|
2181
|
+
const t = Math.max(0, Math.min(1, (yTop - y) / (yTop - yBottom)));
|
|
2182
|
+
const at = (c, r) => nodeLanding(points[(r * stride + c) * 2], points[(r * stride + c) * 2 + 1]);
|
|
2183
|
+
const top = at(col, row) + (at(col + 1, row) - at(col, row)) * s;
|
|
2184
|
+
const bot = at(col, row + 1) + (at(col + 1, row + 1) - at(col, row + 1)) * s;
|
|
2185
|
+
return top + (bot - top) * t;
|
|
2186
|
+
}
|
|
2187
|
+
function carrierLandingX(carrier, nodeLanding, px, py) {
|
|
2188
|
+
return renderedLandingX(
|
|
2189
|
+
carrier.part,
|
|
2190
|
+
(vx, vy) => gridChildLandingX(carrier.grid, nodeLanding, vx, vy),
|
|
2191
|
+
px,
|
|
2192
|
+
py
|
|
2193
|
+
);
|
|
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
|
+
}
|
|
2211
|
+
function rowMapsOf(surface) {
|
|
2212
|
+
const maps = /* @__PURE__ */ new Map();
|
|
2213
|
+
return (deg, nodeY) => {
|
|
2214
|
+
let byRow = maps.get(deg);
|
|
2215
|
+
if (byRow === void 0) {
|
|
2216
|
+
byRow = /* @__PURE__ */ new Map();
|
|
2217
|
+
maps.set(deg, byRow);
|
|
2218
|
+
}
|
|
2219
|
+
let map = byRow.get(nodeY);
|
|
2220
|
+
if (map === void 0) {
|
|
2221
|
+
map = surface.mapAt(deg, nodeY);
|
|
2222
|
+
byRow.set(nodeY, map);
|
|
2223
|
+
}
|
|
2224
|
+
return map;
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
function plateLandingOn(plate, mapAt) {
|
|
2228
|
+
return (deg, x, y) => carrierLandingX(
|
|
2229
|
+
plate,
|
|
2230
|
+
(nodeX, nodeY) => mapAt(deg, nodeY).mapX(nodeX),
|
|
2231
|
+
x,
|
|
2232
|
+
y
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
function plateGuardRowsFor(face, hairFront) {
|
|
2236
|
+
const rowsOf = (part, y) => {
|
|
2237
|
+
const { rows } = meshCellsFor(part.cropW, part.cropH);
|
|
2238
|
+
return Array.from(
|
|
2239
|
+
{ length: rows + 1 },
|
|
2240
|
+
(_, r) => y + (part.cropH / 2 - r / rows * part.cropH)
|
|
2241
|
+
);
|
|
2242
|
+
};
|
|
2243
|
+
const faceRows = rowsOf(face, face.y);
|
|
2244
|
+
if (hairFront === void 0) return faceRows;
|
|
2245
|
+
const top = face.y + face.cropH / 2;
|
|
2246
|
+
const bottom = face.y - face.cropH / 2;
|
|
2247
|
+
return [
|
|
2248
|
+
...faceRows,
|
|
2249
|
+
...rowsOf(hairFront, hairFront.centerY).map(
|
|
2250
|
+
(y) => Math.max(bottom, Math.min(top, y))
|
|
2251
|
+
)
|
|
2252
|
+
];
|
|
2253
|
+
}
|
|
2254
|
+
function plateGuardsOf(carriers, faceHalfWidth, profile, hairFront) {
|
|
2255
|
+
const plate = carriers.get("face");
|
|
2256
|
+
if (plate === void 0) {
|
|
2257
|
+
throw new Error("auto-rig: plateGuardsOf: the carriers have no face");
|
|
2258
|
+
}
|
|
2259
|
+
return {
|
|
2260
|
+
plate,
|
|
2261
|
+
edgeAt: profile === void 0 ? () => faceHalfWidth : profile.at,
|
|
2262
|
+
plateGuardRows: plateGuardRowsFor(plate.part, hairFront)
|
|
1890
2263
|
};
|
|
1891
2264
|
}
|
|
1892
2265
|
var TurnTargetError = class extends Error {
|
|
@@ -1957,12 +2330,15 @@ function resolveTurnTargets(targets = {}) {
|
|
|
1957
2330
|
defaulted
|
|
1958
2331
|
};
|
|
1959
2332
|
}
|
|
1960
|
-
function turnLandmarks(layers) {
|
|
2333
|
+
function turnLandmarks(layers, carriers) {
|
|
1961
2334
|
const markOf = (role) => {
|
|
1962
2335
|
const layer = layers.find((l) => l.role === role);
|
|
1963
2336
|
if (!layer) return void 0;
|
|
1964
2337
|
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
1965
|
-
|
|
2338
|
+
const mark = { x: t.x, y: t.y, w: layer.cropW };
|
|
2339
|
+
const carrier = carriers?.get(role);
|
|
2340
|
+
if (carrier !== void 0) mark.carrier = carrier;
|
|
2341
|
+
return mark;
|
|
1966
2342
|
};
|
|
1967
2343
|
const pairOf = (left, right) => {
|
|
1968
2344
|
const l = markOf(left);
|
|
@@ -1979,23 +2355,67 @@ function turnLandmarks(layers) {
|
|
|
1979
2355
|
mouth: one("mouth")
|
|
1980
2356
|
};
|
|
1981
2357
|
}
|
|
2358
|
+
function eyeRowOf(landmarks, fallback) {
|
|
2359
|
+
const [l, r] = landmarks.eye;
|
|
2360
|
+
return landmarks.eye.length === 2 && l.y !== void 0 && r.y !== void 0 ? (l.y + r.y) / 2 : fallback;
|
|
2361
|
+
}
|
|
1982
2362
|
var TURN_BISECT_STEPS = 40;
|
|
1983
2363
|
var TURN_DEPTH_EPS = 1e-6;
|
|
1984
|
-
function
|
|
2364
|
+
function landmarkLandingX(landmark, mapAt, deg, shift, px, py) {
|
|
2365
|
+
const { carrier } = landmark;
|
|
2366
|
+
if (carrier === void 0) return mapAt(py).mapX(px + shift);
|
|
2367
|
+
return carrierLandingX(
|
|
2368
|
+
carrier,
|
|
2369
|
+
(nodeX, nodeY) => groupNodeLanding(carrier.anchor, mapAt, deg, shift, nodeX, nodeY).x,
|
|
2370
|
+
px,
|
|
2371
|
+
py
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
function familyReachPx(landmarks, plateEdgeX) {
|
|
2375
|
+
const farEdge = Math.min(...landmarks.map((l) => l.x - l.w / 2));
|
|
2376
|
+
return Math.max(0, farEdge - plateEdgeX);
|
|
2377
|
+
}
|
|
2378
|
+
function familyDepthCap(landmarks, plateEdgeX, unit) {
|
|
2379
|
+
return familyReachPx(landmarks, plateEdgeX) / unit;
|
|
2380
|
+
}
|
|
2381
|
+
function solveTurnDepthSigned(target, landmarks, unit, mapAt, plateEdgeX, maxDepth) {
|
|
1985
2382
|
if (landmarks.length === 0) {
|
|
1986
2383
|
throw new Error("auto-rig: solveTurnDepth: no landmark to slide");
|
|
1987
2384
|
}
|
|
1988
|
-
const
|
|
1989
|
-
const
|
|
1990
|
-
const
|
|
1991
|
-
|
|
2385
|
+
const plateCap = familyDepthCap(landmarks, plateEdgeX, unit);
|
|
2386
|
+
const cap = maxDepth === void 0 ? plateCap : Math.min(plateCap, maxDepth);
|
|
2387
|
+
const achieved = (d) => landmarks.reduce(
|
|
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),
|
|
2396
|
+
0
|
|
2397
|
+
) / landmarks.length;
|
|
1992
2398
|
const atCap = achieved(cap);
|
|
1993
2399
|
const atRest = achieved(0);
|
|
1994
2400
|
const attainable = [atCap, atRest];
|
|
1995
|
-
if (target > atRest + TURN_DEPTH_EPS)
|
|
1996
|
-
return {
|
|
1997
|
-
|
|
1998
|
-
|
|
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
|
+
}
|
|
1999
2419
|
const clampedTarget = Math.min(Math.max(target, atCap), atRest);
|
|
2000
2420
|
let lo = 0;
|
|
2001
2421
|
let hi = cap;
|
|
@@ -2011,68 +2431,52 @@ var TURN_SWEEP_MAX_FACTOR = 8;
|
|
|
2011
2431
|
var TURN_SWEEP_SAMPLES = 32;
|
|
2012
2432
|
var HOLD_CLEARANCE = 1;
|
|
2013
2433
|
var TURN_HOLD_SAMPLES = 16;
|
|
2014
|
-
var
|
|
2015
|
-
function hairFrontHoldTarget(x, faceCenterX,
|
|
2434
|
+
var TURN_SETTLE_PASSES = 24;
|
|
2435
|
+
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge) {
|
|
2016
2436
|
const local = x - faceCenterX;
|
|
2017
2437
|
const dist = Math.abs(local);
|
|
2018
2438
|
const side = Math.sign(local);
|
|
2019
|
-
const
|
|
2020
|
-
if (dist <=
|
|
2439
|
+
const painted = edgeAt(y);
|
|
2440
|
+
if (dist <= painted) return plateLandingAt(deg, x, y);
|
|
2021
2441
|
if (dist <= holdBase) {
|
|
2022
|
-
const inner =
|
|
2023
|
-
const u = (dist -
|
|
2024
|
-
return inner + (faceCenterX + side * holdEdge
|
|
2442
|
+
const inner = plateLandingAt(deg, faceCenterX + side * painted, y);
|
|
2443
|
+
const u = (dist - painted) / (holdBase - painted);
|
|
2444
|
+
return inner + (faceCenterX + side * holdEdge - inner) * u;
|
|
2025
2445
|
}
|
|
2026
|
-
return x + side * (holdEdge
|
|
2446
|
+
return x + side * (holdEdge - holdBase);
|
|
2027
2447
|
}
|
|
2028
|
-
function hairFrontLandingAt(x,
|
|
2448
|
+
function hairFrontLandingAt(x, y, hairFront, holdTargetAt, leadShift) {
|
|
2029
2449
|
const { cols, rows } = meshCellsFor(hairFront.cropW, hairFront.cropH);
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
lo,
|
|
2047
|
-
hi: Math.max(0, Math.min(cells, lo + 1)),
|
|
2048
|
-
t: Math.max(0, Math.min(1, v - lo))
|
|
2049
|
-
};
|
|
2050
|
-
};
|
|
2051
|
-
const col = bracket(
|
|
2052
|
-
(x - hairFront.x + hairFront.cropW / 2) / hairFront.cropW * cols,
|
|
2053
|
-
cols
|
|
2450
|
+
const top = hairFront.centerY + hairFront.cropH / 2;
|
|
2451
|
+
return renderedLandingX(
|
|
2452
|
+
{
|
|
2453
|
+
x: hairFront.x,
|
|
2454
|
+
y: hairFront.centerY,
|
|
2455
|
+
cropW: hairFront.cropW,
|
|
2456
|
+
cropH: hairFront.cropH,
|
|
2457
|
+
cols,
|
|
2458
|
+
rows,
|
|
2459
|
+
scaleX: 1,
|
|
2460
|
+
scaleY: 1,
|
|
2461
|
+
rotation: 0
|
|
2462
|
+
},
|
|
2463
|
+
(vx, vy) => holdTargetAt(vx, vy) + leadShift * Math.pow((top - vy) / hairFront.cropH, HAIR_SWAY_CURL),
|
|
2464
|
+
x,
|
|
2465
|
+
y
|
|
2054
2466
|
);
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
const tl = landingAt(col.lo, row.lo);
|
|
2058
|
-
const br = landingAt(col.hi, row.hi);
|
|
2059
|
-
if (col.t <= row.t) {
|
|
2060
|
-
const bl = landingAt(col.lo, row.hi);
|
|
2061
|
-
return tl + row.t * (bl - tl) + col.t * (br - bl);
|
|
2062
|
-
}
|
|
2063
|
-
const tr = landingAt(col.hi, row.lo);
|
|
2064
|
-
return tl + col.t * (tr - tl) + row.t * (br - tr);
|
|
2065
|
-
}
|
|
2066
|
-
function shellTravelCap(faceCenterX, faceHalfWidth, askedTravel, bendOnlyMapAt, holdEdgeAt) {
|
|
2467
|
+
}
|
|
2468
|
+
function shellTravelCap(faceCenterX, edgeAt, plateGuardRows, askedTravel, bendOnlyLandingAt, holdEdgeAt) {
|
|
2067
2469
|
let cap = Infinity;
|
|
2068
2470
|
for (const deg of HEAD_TURN_STOPS) {
|
|
2069
2471
|
if (deg === 0) continue;
|
|
2070
|
-
const map = bendOnlyMapAt(deg);
|
|
2071
2472
|
const slideSign = turnSlide(askedTravel, deg) < 0 ? -1 : 1;
|
|
2072
|
-
const towardReach =
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2473
|
+
const towardReach = plateSideReachAt(
|
|
2474
|
+
bendOnlyLandingAt,
|
|
2475
|
+
faceCenterX,
|
|
2476
|
+
edgeAt,
|
|
2477
|
+
plateGuardRows,
|
|
2478
|
+
deg,
|
|
2479
|
+
slideSign
|
|
2076
2480
|
);
|
|
2077
2481
|
cap = Math.min(
|
|
2078
2482
|
cap,
|
|
@@ -2081,8 +2485,39 @@ function shellTravelCap(faceCenterX, faceHalfWidth, askedTravel, bendOnlyMapAt,
|
|
|
2081
2485
|
}
|
|
2082
2486
|
return cap;
|
|
2083
2487
|
}
|
|
2488
|
+
function aitken(x0, x1, x2, cap) {
|
|
2489
|
+
const d1 = x1 - x0;
|
|
2490
|
+
const d2 = x2 - x1;
|
|
2491
|
+
const denominator = d2 - d1;
|
|
2492
|
+
if (denominator === 0) return x2;
|
|
2493
|
+
const x = x2 - d2 * d2 / denominator;
|
|
2494
|
+
return Number.isFinite(x) ? Math.min(cap, Math.max(0, x)) : x2;
|
|
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
|
+
}
|
|
2084
2509
|
function evaluateTurnCandidate(ctx, radius) {
|
|
2085
|
-
const
|
|
2510
|
+
const surfaceAt = (travel) => turnSurface({
|
|
2511
|
+
faceCenterX: ctx.faceCenterX,
|
|
2512
|
+
faceCenterY: ctx.faceCenterY,
|
|
2513
|
+
radius,
|
|
2514
|
+
travel,
|
|
2515
|
+
nodRadius: ctx.nodRadius,
|
|
2516
|
+
lattice: ctx.lattice,
|
|
2517
|
+
profile: ctx.profile,
|
|
2518
|
+
eyeRowY: ctx.eyeRowY
|
|
2519
|
+
});
|
|
2520
|
+
const bendOnlyLandingAt = plateLandingOn(ctx.plate, rowMapsOf(surfaceAt(0)));
|
|
2086
2521
|
const measuredEdgeAt = (side) => {
|
|
2087
2522
|
const candidates = side < 0 ? ctx.headEdges?.left : ctx.headEdges?.right;
|
|
2088
2523
|
if (candidates === void 0 || candidates.length === 0) return void 0;
|
|
@@ -2094,66 +2529,160 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2094
2529
|
return edge === void 0 ? ctx.headHalfWidth : side * (edge - ctx.faceCenterX);
|
|
2095
2530
|
};
|
|
2096
2531
|
const unit = headTurnParallaxUnit(radius);
|
|
2097
|
-
const
|
|
2098
|
-
const
|
|
2099
|
-
const
|
|
2532
|
+
const plateCapOf = (marks) => familyDepthCap(marks, ctx.plateEdgeX, unit);
|
|
2533
|
+
const eyePlateCap = plateCapOf(ctx.landmarks.eye);
|
|
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
|
+
};
|
|
2630
|
+
const evaluateHold = (ratio, depths2) => {
|
|
2100
2631
|
const holdEdgeFrom = (base) => (deg) => base * (1 + (ratio - 1) * Math.abs(deg) / HEAD_TURN_MAX_DEG);
|
|
2101
|
-
const
|
|
2632
|
+
const travel = ctx.headHalfWidth === void 0 ? ctx.travel : Math.min(
|
|
2102
2633
|
ctx.travel,
|
|
2103
2634
|
shellTravelCap(
|
|
2104
2635
|
ctx.faceCenterX,
|
|
2105
|
-
ctx.
|
|
2636
|
+
ctx.edgeAt,
|
|
2637
|
+
ctx.plateGuardRows,
|
|
2106
2638
|
ctx.travel,
|
|
2107
|
-
|
|
2639
|
+
bendOnlyLandingAt,
|
|
2108
2640
|
(deg, side) => {
|
|
2109
2641
|
const rest = shellHalfWidthAt(side);
|
|
2110
2642
|
return Math.min(rest, holdEdgeFrom(rest)(deg));
|
|
2111
2643
|
}
|
|
2112
2644
|
)
|
|
2113
2645
|
);
|
|
2114
|
-
const
|
|
2115
|
-
const
|
|
2116
|
-
const
|
|
2646
|
+
const surface = surfaceAt(travel);
|
|
2647
|
+
const rowMaps = rowMapsOf(surface);
|
|
2648
|
+
const mapAt = (y) => rowMaps(-HEAD_TURN_MAX_DEG, y);
|
|
2649
|
+
const plateLandingAt = plateLandingOn(ctx.plate, rowMaps);
|
|
2650
|
+
const holdBase = ctx.headHalfWidth ?? plateReach(
|
|
2651
|
+
plateLandingAt,
|
|
2652
|
+
ctx.faceCenterX,
|
|
2653
|
+
ctx.edgeAt,
|
|
2654
|
+
ctx.plateGuardRows
|
|
2655
|
+
) + HOLD_CLEARANCE;
|
|
2656
|
+
const holdEdgeAt = holdEdgeFrom(holdBase);
|
|
2117
2657
|
let foldLowerBound = -Infinity;
|
|
2118
2658
|
let folds = false;
|
|
2119
2659
|
for (const deg of HEAD_TURN_STOPS) {
|
|
2120
2660
|
if (deg === 0) continue;
|
|
2121
2661
|
const reach = plateReachAt(
|
|
2122
|
-
|
|
2662
|
+
plateLandingAt,
|
|
2123
2663
|
ctx.faceCenterX,
|
|
2124
|
-
ctx.
|
|
2664
|
+
ctx.edgeAt,
|
|
2665
|
+
ctx.plateGuardRows,
|
|
2666
|
+
deg
|
|
2125
2667
|
);
|
|
2126
2668
|
foldLowerBound = Math.max(
|
|
2127
2669
|
foldLowerBound,
|
|
2128
|
-
1 + ((reach + HOLD_CLEARANCE) /
|
|
2670
|
+
1 + ((reach + HOLD_CLEARANCE) / holdBase - 1) * (HEAD_TURN_MAX_DEG / Math.abs(deg))
|
|
2129
2671
|
);
|
|
2130
|
-
if (
|
|
2672
|
+
if (holdEdgeAt(deg) <= reach) folds = true;
|
|
2131
2673
|
}
|
|
2132
|
-
const map2 = columnMapAt(-HEAD_TURN_MAX_DEG);
|
|
2133
|
-
const requestedHoldEdge = holdEdgeAt2(HEAD_TURN_MAX_DEG);
|
|
2134
|
-
const gridReach2 = [
|
|
2135
|
-
Math.abs(map2.warpedX[0] - ctx.faceCenterX),
|
|
2136
|
-
Math.abs(map2.warpedX[map2.warpedX.length - 1] - ctx.faceCenterX)
|
|
2137
|
-
];
|
|
2138
|
-
const cappedHoldEdge = [
|
|
2139
|
-
Math.min(requestedHoldEdge, gridReach2[0] - HOLD_CLEARANCE),
|
|
2140
|
-
Math.min(requestedHoldEdge, gridReach2[1] - HOLD_CLEARANCE)
|
|
2141
|
-
];
|
|
2142
2674
|
const restAt = (side) => {
|
|
2143
2675
|
const edge = measuredEdgeAt(side);
|
|
2144
2676
|
if (edge !== void 0) return edge;
|
|
2145
|
-
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side *
|
|
2677
|
+
return ctx.headHalfWidth !== void 0 || ctx.hairFrontSilhouette === void 0 ? ctx.faceCenterX + side * holdBase : ctx.hairFrontSilhouette.x + side * ctx.hairFrontSilhouette.cropW / 2;
|
|
2146
2678
|
};
|
|
2147
|
-
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,
|
|
2148
2684
|
x,
|
|
2149
|
-
ctx.
|
|
2150
|
-
ctx.faceCenterX,
|
|
2151
|
-
ctx.faceHalfWidth,
|
|
2152
|
-
holdBase2,
|
|
2153
|
-
cappedHoldEdge,
|
|
2154
|
-
map2,
|
|
2155
|
-
leadTipShift,
|
|
2156
|
-
leadRow
|
|
2685
|
+
ctx.eyeRowY
|
|
2157
2686
|
);
|
|
2158
2687
|
const landingOfRole = (role, x) => {
|
|
2159
2688
|
if (role === "hair_front") return hairFrontAt(x);
|
|
@@ -2161,10 +2690,29 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2161
2690
|
return x;
|
|
2162
2691
|
}
|
|
2163
2692
|
if (role === "body") {
|
|
2164
|
-
return x - BODY_TURN_FOLLOW *
|
|
2693
|
+
return x - BODY_TURN_FOLLOW * travel;
|
|
2165
2694
|
}
|
|
2166
2695
|
if (role === "face" || FEATURE_NOD_DEPTH[roleFamily(role)] !== void 0) {
|
|
2167
|
-
|
|
2696
|
+
const carrier = ctx.carriers.get(role);
|
|
2697
|
+
if (carrier === void 0) {
|
|
2698
|
+
throw new Error(
|
|
2699
|
+
`auto-rig: evaluateTurnCandidate: headEdges names "${role}", which this layer set has no layer for`
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2702
|
+
const shift = role === "face" ? 0 : -depths2[turnFamily(role)] * unit;
|
|
2703
|
+
return carrierLandingX(
|
|
2704
|
+
carrier,
|
|
2705
|
+
(nodeX, nodeY) => groupNodeLanding(
|
|
2706
|
+
carrier.anchor,
|
|
2707
|
+
mapAt,
|
|
2708
|
+
-HEAD_TURN_MAX_DEG,
|
|
2709
|
+
shift,
|
|
2710
|
+
nodeX,
|
|
2711
|
+
nodeY
|
|
2712
|
+
).x,
|
|
2713
|
+
x,
|
|
2714
|
+
ctx.eyeRowY
|
|
2715
|
+
);
|
|
2168
2716
|
}
|
|
2169
2717
|
throw new Error(
|
|
2170
2718
|
`auto-rig: evaluateTurnCandidate: unrecognised role "${role}"`
|
|
@@ -2180,13 +2728,13 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2180
2728
|
};
|
|
2181
2729
|
return {
|
|
2182
2730
|
ratio,
|
|
2183
|
-
travel
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2731
|
+
travel,
|
|
2732
|
+
mapAt,
|
|
2733
|
+
surface,
|
|
2734
|
+
holdBase,
|
|
2735
|
+
holdEdgeAt,
|
|
2187
2736
|
folds,
|
|
2188
2737
|
foldLowerBound,
|
|
2189
|
-
gridReach: gridReach2,
|
|
2190
2738
|
// How far the silhouette's own centre drifts off the face centre at full
|
|
2191
2739
|
// turn, against how far it already sat off centre at rest (zero unless
|
|
2192
2740
|
// hair_front's crop is itself off-centre) — the held shell keeps its own
|
|
@@ -2203,24 +2751,18 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2203
2751
|
restSpan: restAt(1) - restAt(-1)
|
|
2204
2752
|
};
|
|
2205
2753
|
};
|
|
2206
|
-
const
|
|
2207
|
-
|
|
2208
|
-
for (let i = 0; i < TURN_HOLD_SATURATION_PASSES; i++) {
|
|
2209
|
-
const needed = (Math.max(hold2.gridReach[0], hold2.gridReach[1]) - HOLD_CLEARANCE) / hold2.holdBase;
|
|
2210
|
-
if (needed <= hold2.ratio) break;
|
|
2211
|
-
hold2 = evaluateHold(needed);
|
|
2212
|
-
}
|
|
2213
|
-
return hold2;
|
|
2214
|
-
};
|
|
2215
|
-
const holdRange = () => {
|
|
2216
|
-
const ceiling = saturatedHold();
|
|
2754
|
+
const holdRange = (depths2) => {
|
|
2755
|
+
const ceiling = evaluateHold(TURN_RATIO_MAX, depths2);
|
|
2217
2756
|
if (ceiling.folds) return void 0;
|
|
2218
|
-
let floor = evaluateHold(
|
|
2757
|
+
let floor = evaluateHold(
|
|
2758
|
+
Math.min(ceiling.foldLowerBound, ceiling.ratio),
|
|
2759
|
+
depths2
|
|
2760
|
+
);
|
|
2219
2761
|
if (floor.folds) {
|
|
2220
2762
|
let lo = floor;
|
|
2221
2763
|
let hi = ceiling;
|
|
2222
2764
|
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2223
|
-
const mid = evaluateHold((lo.ratio + hi.ratio) / 2);
|
|
2765
|
+
const mid = evaluateHold((lo.ratio + hi.ratio) / 2, depths2);
|
|
2224
2766
|
if (mid.folds) lo = mid;
|
|
2225
2767
|
else hi = mid;
|
|
2226
2768
|
}
|
|
@@ -2229,7 +2771,8 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2229
2771
|
const samples = [floor];
|
|
2230
2772
|
for (let i = 1; i < TURN_HOLD_SAMPLES; i++) {
|
|
2231
2773
|
const sample = evaluateHold(
|
|
2232
|
-
floor.ratio + (ceiling.ratio - floor.ratio) * i / TURN_HOLD_SAMPLES
|
|
2774
|
+
floor.ratio + (ceiling.ratio - floor.ratio) * i / TURN_HOLD_SAMPLES,
|
|
2775
|
+
depths2
|
|
2233
2776
|
);
|
|
2234
2777
|
if (!sample.folds) samples.push(sample);
|
|
2235
2778
|
}
|
|
@@ -2242,15 +2785,14 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2242
2785
|
return lower > upper ? { blocked: "silhouetteRatio", lower: Infinity, upper: -Infinity } : { blocked: "silhouetteRatio", lower, upper };
|
|
2243
2786
|
};
|
|
2244
2787
|
const asked = ctx.silhouetteRatio;
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
if (samples === void 0) return silhouetteMiss();
|
|
2788
|
+
const fitHold = (depths2) => {
|
|
2789
|
+
const natural = evaluateHold(asked, depths2);
|
|
2790
|
+
const eps = TURN_DEPTH_EPS / natural.restSpan;
|
|
2791
|
+
if (!natural.folds && Math.abs(natural.renderedSilhouetteRatio - asked) <= eps) {
|
|
2792
|
+
return { hold: natural, silhouetteClamped: false };
|
|
2793
|
+
}
|
|
2794
|
+
const samples = holdRange(depths2);
|
|
2795
|
+
if (samples === void 0) return void 0;
|
|
2254
2796
|
const rendered = samples.map((h) => h.renderedSilhouetteRatio);
|
|
2255
2797
|
const span = [
|
|
2256
2798
|
Math.min(...rendered),
|
|
@@ -2265,91 +2807,169 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2265
2807
|
return ea <= eb ? a : b;
|
|
2266
2808
|
};
|
|
2267
2809
|
if (asked < span[0] - eps || asked > span[1] + eps) {
|
|
2268
|
-
if (!ctx.clampSilhouetteRatio) return silhouetteMiss(span);
|
|
2269
2810
|
const wanted = asked < span[0] ? span[0] : span[1];
|
|
2270
|
-
hold = samples.reduce(
|
|
2811
|
+
const hold = samples.reduce(
|
|
2271
2812
|
(a, b) => Math.abs(b.renderedSilhouetteRatio - wanted) < Math.abs(a.renderedSilhouetteRatio - wanted) ? b : a
|
|
2272
2813
|
);
|
|
2273
|
-
silhouetteClamped
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
}
|
|
2814
|
+
return { hold, silhouetteClamped: true, span };
|
|
2815
|
+
}
|
|
2816
|
+
let best = samples.reduce(nearer);
|
|
2817
|
+
let lo;
|
|
2818
|
+
let hi;
|
|
2819
|
+
for (let i = 0; i + 1 < samples.length; i++) {
|
|
2820
|
+
const a = samples[i].renderedSilhouetteRatio - asked;
|
|
2821
|
+
const b = samples[i + 1].renderedSilhouetteRatio - asked;
|
|
2822
|
+
if (a * b <= 0) {
|
|
2823
|
+
lo = samples[i];
|
|
2824
|
+
hi = samples[i + 1];
|
|
2825
|
+
break;
|
|
2286
2826
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2827
|
+
}
|
|
2828
|
+
for (let i = 0; lo !== void 0 && hi !== void 0; i++) {
|
|
2829
|
+
if (i >= TURN_BISECT_STEPS || Math.abs(best.renderedSilhouetteRatio - asked) <= eps) {
|
|
2830
|
+
break;
|
|
2831
|
+
}
|
|
2832
|
+
const mid = evaluateHold((lo.ratio + hi.ratio) / 2, depths2);
|
|
2833
|
+
if (!mid.folds) best = nearer(best, mid);
|
|
2834
|
+
if ((mid.renderedSilhouetteRatio - asked) * (lo.renderedSilhouetteRatio - asked) > 0) {
|
|
2835
|
+
lo = mid;
|
|
2836
|
+
} else {
|
|
2837
|
+
hi = mid;
|
|
2298
2838
|
}
|
|
2299
|
-
hold = best;
|
|
2300
2839
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2840
|
+
return { hold: best, silhouetteClamped: false };
|
|
2841
|
+
};
|
|
2842
|
+
let depths = { eye: 0, nose: 0, mouth: 0 };
|
|
2843
|
+
let seed;
|
|
2844
|
+
for (let pass = 0; ; pass++) {
|
|
2845
|
+
const fitted = fitHold(depths);
|
|
2846
|
+
if (fitted === void 0) return silhouetteMiss();
|
|
2847
|
+
const { hold, silhouetteClamped, span } = fitted;
|
|
2848
|
+
if (hold.folds) {
|
|
2849
|
+
throw new Error(
|
|
2850
|
+
"auto-rig: evaluateTurnCandidate: the hold it settled on folds"
|
|
2851
|
+
);
|
|
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
|
+
}
|
|
2865
|
+
const eye = solveTurnDepthSigned(
|
|
2866
|
+
hold.silhouetteCenterShift - ctx.eyeShift * ctx.hh,
|
|
2867
|
+
ctx.landmarks.eye,
|
|
2868
|
+
unit,
|
|
2869
|
+
hold.mapAt,
|
|
2870
|
+
ctx.plateEdgeX,
|
|
2871
|
+
maxDepth
|
|
2305
2872
|
);
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2873
|
+
const features = solveFeatureDepths(
|
|
2874
|
+
ctx,
|
|
2875
|
+
(-eye.achieved + hold.silhouetteCenterShift) / ctx.hh,
|
|
2876
|
+
unit,
|
|
2877
|
+
hold.mapAt,
|
|
2878
|
+
hold.silhouetteCenterShift
|
|
2879
|
+
);
|
|
2880
|
+
const solved = {
|
|
2881
|
+
eye: eye.depth,
|
|
2882
|
+
nose: features.nose.depth,
|
|
2883
|
+
mouth: features.mouth.depth
|
|
2884
|
+
};
|
|
2885
|
+
const settled = evaluateHold(hold.ratio, solved);
|
|
2886
|
+
if (Math.abs(settled.silhouetteCenterShift - hold.silhouetteCenterShift) > TURN_DEPTH_EPS || Math.abs(settled.renderedSilhouetteRatio - hold.renderedSilhouetteRatio) * hold.restSpan > TURN_DEPTH_EPS) {
|
|
2887
|
+
if (pass + 1 >= TURN_SETTLE_PASSES) return { blocked: "settle" };
|
|
2888
|
+
if (seed === void 0) {
|
|
2889
|
+
seed = depths;
|
|
2890
|
+
depths = solved;
|
|
2891
|
+
} else {
|
|
2892
|
+
depths = {
|
|
2893
|
+
eye: aitken(seed.eye, depths.eye, solved.eye, eyePlateCap),
|
|
2894
|
+
nose: aitken(
|
|
2895
|
+
seed.nose,
|
|
2896
|
+
depths.nose,
|
|
2897
|
+
solved.nose,
|
|
2898
|
+
plateCapOf(ctx.landmarks.nose)
|
|
2899
|
+
),
|
|
2900
|
+
mouth: aitken(
|
|
2901
|
+
seed.mouth,
|
|
2902
|
+
depths.mouth,
|
|
2903
|
+
solved.mouth,
|
|
2904
|
+
plateCapOf(ctx.landmarks.mouth)
|
|
2905
|
+
)
|
|
2906
|
+
};
|
|
2907
|
+
seed = void 0;
|
|
2908
|
+
}
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2911
|
+
const {
|
|
2912
|
+
travel,
|
|
2913
|
+
mapAt,
|
|
2914
|
+
surface,
|
|
2915
|
+
holdBase,
|
|
2916
|
+
holdEdgeAt,
|
|
2917
|
+
silhouetteCenterShift,
|
|
2918
|
+
renderedSilhouetteRatio
|
|
2919
|
+
} = settled;
|
|
2920
|
+
if (silhouetteClamped && !ctx.clampSilhouetteRatio) {
|
|
2921
|
+
return silhouetteMiss(span);
|
|
2922
|
+
}
|
|
2923
|
+
if (!eye.reached && eye.limit === "floor" && !ctx.clampEyeShift) {
|
|
2924
|
+
return {
|
|
2925
|
+
blocked: "eyeShift",
|
|
2926
|
+
// eyeShift is a MAGNITUDE (the sign is ignored — see TurnTargets), so
|
|
2927
|
+
// a negative lower bound here would advertise a value the field's own
|
|
2928
|
+
// contract already rules out; 0 is the true floor.
|
|
2929
|
+
offeredShift: [
|
|
2930
|
+
Math.max(0, (-eye.attainable[1] + silhouetteCenterShift) / ctx.hh),
|
|
2931
|
+
(-eye.attainable[0] + silhouetteCenterShift) / ctx.hh
|
|
2932
|
+
]
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
const eyeShiftPx = -eye.depth * unit;
|
|
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;
|
|
2951
|
+
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
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);
|
|
2325
2954
|
return {
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2955
|
+
radius,
|
|
2956
|
+
ratio: scaleOf(far) / scaleOf(near),
|
|
2957
|
+
unit,
|
|
2958
|
+
travel,
|
|
2959
|
+
mapAt,
|
|
2960
|
+
surface,
|
|
2961
|
+
holdBase,
|
|
2962
|
+
holdEdgeAt,
|
|
2963
|
+
eye,
|
|
2964
|
+
nose: features.nose,
|
|
2965
|
+
mouth: features.mouth,
|
|
2966
|
+
silhouetteClamped,
|
|
2967
|
+
silhouetteCenterShift,
|
|
2968
|
+
renderedSilhouetteRatio,
|
|
2969
|
+
strandFeasible,
|
|
2970
|
+
...strandOverlap === void 0 ? {} : { strandOverlap }
|
|
2334
2971
|
};
|
|
2335
2972
|
}
|
|
2336
|
-
const scaleOf = (l) => (map.mapX(l.x + l.w / 2 - eye.depth * unit) - map.mapX(l.x - l.w / 2 - eye.depth * unit)) / l.w;
|
|
2337
|
-
const far = ctx.landmarks.eye.reduce((a, b) => b.x < a.x ? b : a);
|
|
2338
|
-
const near = ctx.landmarks.eye.reduce((a, b) => b.x > a.x ? b : a);
|
|
2339
|
-
return {
|
|
2340
|
-
radius,
|
|
2341
|
-
ratio: scaleOf(far) / scaleOf(near),
|
|
2342
|
-
unit,
|
|
2343
|
-
travel,
|
|
2344
|
-
map,
|
|
2345
|
-
holdBase,
|
|
2346
|
-
holdEdgeAt,
|
|
2347
|
-
holdEdgeX,
|
|
2348
|
-
eye,
|
|
2349
|
-
silhouetteClamped,
|
|
2350
|
-
silhouetteCenterShift,
|
|
2351
|
-
renderedSilhouetteRatio
|
|
2352
|
-
};
|
|
2353
2973
|
}
|
|
2354
2974
|
function sweepTurnRadii(ctx) {
|
|
2355
2975
|
const minRadius = ctx.faceHalfWidth * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
@@ -2358,6 +2978,7 @@ function sweepTurnRadii(ctx) {
|
|
|
2358
2978
|
let heldRatioLower = Infinity;
|
|
2359
2979
|
let heldRatioUpper = -Infinity;
|
|
2360
2980
|
let offeredShift;
|
|
2981
|
+
let unsettled = 0;
|
|
2361
2982
|
for (let i = 0; i < TURN_SWEEP_SAMPLES; i++) {
|
|
2362
2983
|
const radius = minRadius * Math.pow(maxRadius / minRadius, i / (TURN_SWEEP_SAMPLES - 1));
|
|
2363
2984
|
const result = evaluateTurnCandidate(ctx, radius);
|
|
@@ -2366,6 +2987,8 @@ function sweepTurnRadii(ctx) {
|
|
|
2366
2987
|
} else if (result.blocked === "silhouetteRatio") {
|
|
2367
2988
|
heldRatioLower = Math.min(heldRatioLower, result.lower);
|
|
2368
2989
|
heldRatioUpper = Math.max(heldRatioUpper, result.upper);
|
|
2990
|
+
} else if (result.blocked === "settle") {
|
|
2991
|
+
unsettled++;
|
|
2369
2992
|
} else {
|
|
2370
2993
|
offeredShift = offeredShift ? [
|
|
2371
2994
|
Math.min(offeredShift[0], result.offeredShift[0]),
|
|
@@ -2373,7 +2996,16 @@ function sweepTurnRadii(ctx) {
|
|
|
2373
2996
|
] : result.offeredShift;
|
|
2374
2997
|
}
|
|
2375
2998
|
}
|
|
2376
|
-
return {
|
|
2999
|
+
return {
|
|
3000
|
+
candidates,
|
|
3001
|
+
heldRatioLower,
|
|
3002
|
+
heldRatioUpper,
|
|
3003
|
+
offeredShift,
|
|
3004
|
+
unsettled
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
function runHolds(run, ratio) {
|
|
3008
|
+
return ratio >= run.ratios[0] - TURN_DEPTH_EPS && ratio <= run.ratios[1] + TURN_DEPTH_EPS;
|
|
2377
3009
|
}
|
|
2378
3010
|
function ratioClusters(candidates) {
|
|
2379
3011
|
const clusters = [];
|
|
@@ -2397,7 +3029,7 @@ function ratioClusters(candidates) {
|
|
|
2397
3029
|
close();
|
|
2398
3030
|
return clusters;
|
|
2399
3031
|
}
|
|
2400
|
-
function fitTurnRadius(ctx, candidates, target) {
|
|
3032
|
+
function fitTurnRadius(ctx, candidates, target, accept) {
|
|
2401
3033
|
let lo = candidates[0];
|
|
2402
3034
|
let hi = candidates[candidates.length - 1];
|
|
2403
3035
|
for (let i = candidates.length - 2; i >= 0; i--) {
|
|
@@ -2407,79 +3039,102 @@ function fitTurnRadius(ctx, candidates, target) {
|
|
|
2407
3039
|
break;
|
|
2408
3040
|
}
|
|
2409
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;
|
|
2410
3061
|
for (let i = 0; i < TURN_BISECT_STEPS; i++) {
|
|
2411
|
-
const mid =
|
|
2412
|
-
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;
|
|
2413
3065
|
if ((mid.ratio - target) * (lo.ratio - target) <= 0) hi = mid;
|
|
2414
3066
|
else lo = mid;
|
|
2415
3067
|
}
|
|
2416
|
-
|
|
3068
|
+
if (accept(lo) && accept(hi)) return miss(lo) <= miss(hi) ? lo : hi;
|
|
3069
|
+
return nearest;
|
|
2417
3070
|
}
|
|
2418
|
-
function
|
|
2419
|
-
const
|
|
2420
|
-
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
)
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
best.holdEdgeX,
|
|
2435
|
-
ctx.plateEdgeX
|
|
2436
|
-
);
|
|
2437
|
-
if (!solution.reached) {
|
|
2438
|
-
if (!targets.defaulted.has(field)) {
|
|
2439
|
-
return {
|
|
2440
|
-
blocked: field,
|
|
2441
|
-
// noseShift/mouthShift are MAGNITUDES too (see the correction
|
|
2442
|
-
// above), so a negative lower bound here would advertise a value
|
|
2443
|
-
// the field's own contract already rules out; 0 is the true floor.
|
|
2444
|
-
attainable: [
|
|
2445
|
-
Math.max(
|
|
2446
|
-
0,
|
|
2447
|
-
(-solution.attainable[1] + best.silhouetteCenterShift) / ctx.hh
|
|
2448
|
-
),
|
|
2449
|
-
(-solution.attainable[0] + best.silhouetteCenterShift) / ctx.hh
|
|
2450
|
-
]
|
|
2451
|
-
};
|
|
2452
|
-
}
|
|
2453
|
-
clamped.push(field);
|
|
2454
|
-
}
|
|
2455
|
-
solved[field] = solution.depth;
|
|
2456
|
-
}
|
|
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;
|
|
3078
|
+
}
|
|
3079
|
+
function solveFeatureDepths(ctx, eyeShift, unit, mapAt, silhouetteCenterShift) {
|
|
3080
|
+
const solve = (measured, marks, share) => solveTurnDepthSigned(
|
|
3081
|
+
silhouetteCenterShift - (measured ?? Math.abs(share * eyeShift)) * ctx.hh,
|
|
3082
|
+
marks,
|
|
3083
|
+
unit,
|
|
3084
|
+
mapAt,
|
|
3085
|
+
ctx.plateEdgeX
|
|
3086
|
+
);
|
|
2457
3087
|
return {
|
|
2458
|
-
nose:
|
|
2459
|
-
mouth:
|
|
2460
|
-
clamped
|
|
3088
|
+
nose: solve(ctx.noseShift, ctx.landmarks.nose, NOSE_SHIFT_SHARE),
|
|
3089
|
+
mouth: solve(ctx.mouthShift, ctx.landmarks.mouth, MOUTH_SHIFT_SHARE)
|
|
2461
3090
|
};
|
|
2462
3091
|
}
|
|
2463
|
-
function solveTurnModel(targets, landmarks,
|
|
3092
|
+
function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth, faceCenterY, nodRadius, carriers, profile, hairFront, headEdges, strandEdges) {
|
|
2464
3093
|
if (targets.headHalfWidth !== void 0 && targets.headHalfWidth <= faceHalfWidth) {
|
|
2465
3094
|
throw new TurnTargetError(
|
|
2466
3095
|
`auto-rig: turnTargets.headHalfWidth (${targets.headHalfWidth}) must be wider than the face plate's own half-width (${faceHalfWidth})`
|
|
2467
3096
|
);
|
|
2468
3097
|
}
|
|
2469
|
-
const eyeRowY =
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
1,
|
|
2474
|
-
(hairFront.centerY + hairFront.cropH / 2 - eyeRowY) / hairFront.cropH
|
|
2475
|
-
)
|
|
3098
|
+
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
3099
|
+
let strands;
|
|
3100
|
+
const strandSides = ["left", "right"].filter(
|
|
3101
|
+
(key) => strandEdges?.[key] !== void 0
|
|
2476
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
|
+
}
|
|
2477
3124
|
const ctx = {
|
|
2478
3125
|
landmarks,
|
|
2479
|
-
|
|
3126
|
+
lattice,
|
|
2480
3127
|
faceCenterX,
|
|
3128
|
+
faceCenterY,
|
|
3129
|
+
nodRadius,
|
|
3130
|
+
eyeRowY,
|
|
2481
3131
|
faceHalfWidth,
|
|
3132
|
+
// The slide's hard stop is the CROP's edge, profile or not: the far eye
|
|
3133
|
+
// may sit on the plate's transparent margin, as it does on the hero.
|
|
2482
3134
|
plateEdgeX: faceCenterX - faceHalfWidth,
|
|
3135
|
+
carriers,
|
|
3136
|
+
profile,
|
|
3137
|
+
...plateGuardsOf(carriers, faceHalfWidth, profile, hairFront),
|
|
2483
3138
|
// What this plate asks the turn for; each candidate caps it to its own
|
|
2484
3139
|
// shell — see TurnSolveContext.travel.
|
|
2485
3140
|
travel: headTurnTravel(faceHalfWidth),
|
|
@@ -2492,16 +3147,23 @@ function solveTurnModel(targets, landmarks, faceGrid, faceCenterX, faceHalfWidth
|
|
|
2492
3147
|
// every downstream use already has the magnitude.
|
|
2493
3148
|
eyeShift: Math.abs(targets.eyeShift),
|
|
2494
3149
|
clampEyeShift: targets.defaulted.has("eyeShift"),
|
|
3150
|
+
noseShift: targets.defaulted.has("noseShift") ? void 0 : Math.abs(targets.noseShift),
|
|
3151
|
+
mouthShift: targets.defaulted.has("mouthShift") ? void 0 : Math.abs(targets.mouthShift),
|
|
2495
3152
|
clampSilhouetteRatio: targets.defaulted.has("silhouetteRatio"),
|
|
2496
|
-
|
|
2497
|
-
hairFrontSilhouette: hairFront === void 0 ? void 0 : { x: hairFront.x, cropW: hairFront.cropW, cropH: hairFront.cropH },
|
|
3153
|
+
hairFrontSilhouette: hairFront,
|
|
2498
3154
|
// A companion to a MEASURED headHalfWidth only — see its own doc.
|
|
2499
|
-
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges
|
|
3155
|
+
headEdges: targets.headHalfWidth === void 0 ? void 0 : headEdges,
|
|
3156
|
+
strands
|
|
2500
3157
|
};
|
|
2501
|
-
const clamped = [];
|
|
2502
3158
|
const clampInto = (value, [lo, hi]) => Math.min(Math.max(value, lo), hi);
|
|
2503
3159
|
const pass = sweepTurnRadii(ctx);
|
|
2504
3160
|
if (pass.candidates.length === 0) {
|
|
3161
|
+
if (pass.unsettled > 0) {
|
|
3162
|
+
const refused = TURN_SWEEP_SAMPLES - pass.unsettled;
|
|
3163
|
+
throw new TurnTargetError(
|
|
3164
|
+
`auto-rig: headEdges: no sampled radius carries the turn \u2014 the silhouette and the depths its headEdges owners ride on did not settle within ${TURN_SETTLE_PASSES} passes at ${pass.unsettled} of the ${TURN_SWEEP_SAMPLES} radii${refused > 0 ? `, and the targets refused the other ${refused}` : ""}`
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
2505
3167
|
return pass.offeredShift ? {
|
|
2506
3168
|
unreachable: true,
|
|
2507
3169
|
field: "eyeShift",
|
|
@@ -2523,72 +3185,107 @@ function solveTurnModel(targets, landmarks, faceGrid, faceCenterX, faceHalfWidth
|
|
|
2523
3185
|
attainable: pass.heldRatioLower === Infinity || pass.heldRatioUpper === -Infinity ? void 0 : [pass.heldRatioLower, pass.heldRatioUpper]
|
|
2524
3186
|
};
|
|
2525
3187
|
}
|
|
2526
|
-
const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
let cluster = clusters.find(
|
|
2534
|
-
(c) => farEyeRatio >= c.ratios[0] - TURN_DEPTH_EPS && farEyeRatio <= c.ratios[1] + TURN_DEPTH_EPS
|
|
2535
|
-
);
|
|
2536
|
-
if (cluster === void 0) {
|
|
2537
|
-
cluster = clusters.reduce(
|
|
2538
|
-
(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
|
|
2539
3195
|
);
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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");
|
|
2547
3212
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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);
|
|
2560
3252
|
}
|
|
2561
|
-
if (!clamped.includes("farEyeRatio")) clamped.push("farEyeRatio");
|
|
2562
|
-
}
|
|
2563
|
-
if (!best.eye.reached) clamped.push("eyeShift");
|
|
2564
|
-
if (best.silhouetteClamped) clamped.push("silhouetteRatio");
|
|
2565
|
-
const eyeShift = (-best.eye.achieved + best.silhouetteCenterShift) / ctx.hh;
|
|
2566
|
-
const features = solveFeatureDepths(ctx, targets, best, eyeShift);
|
|
2567
|
-
if ("blocked" in features) {
|
|
2568
3253
|
return {
|
|
2569
|
-
unreachable:
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
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 }
|
|
2573
3272
|
};
|
|
2574
|
-
}
|
|
2575
|
-
return {
|
|
2576
|
-
unreachable: false,
|
|
2577
|
-
radius: best.radius,
|
|
2578
|
-
holdBase: best.holdBase,
|
|
2579
|
-
holdEdgeAt: best.holdEdgeAt,
|
|
2580
|
-
travel: best.travel,
|
|
2581
|
-
depths: { eye: best.eye.depth, nose: features.nose, mouth: features.mouth },
|
|
2582
|
-
achieved: {
|
|
2583
|
-
eyeShift,
|
|
2584
|
-
farEyeRatio: best.ratio,
|
|
2585
|
-
silhouetteRatio: best.renderedSilhouetteRatio
|
|
2586
|
-
},
|
|
2587
|
-
clamped: [...clamped, ...features.clamped]
|
|
2588
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;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
return fitFrom(pass.candidates, () => true);
|
|
2589
3285
|
}
|
|
2590
3286
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
2591
3287
|
var HAIR_FRONT_DEPTH = 0.1;
|
|
3288
|
+
var MOUTH_TURN_TILT_DEG = 5;
|
|
2592
3289
|
var NOD_TRAVEL = 30;
|
|
2593
3290
|
var NOD_BEND = 0.5;
|
|
2594
3291
|
var HAIR_BACK_NOD_DEPTH = -0.135;
|
|
@@ -2621,27 +3318,16 @@ function featureParallaxBindings(role, options) {
|
|
|
2621
3318
|
if (!options.hasNose) return [];
|
|
2622
3319
|
const nodDepth = FEATURE_NOD_DEPTH[roleFamily(role)];
|
|
2623
3320
|
if (nodDepth === void 0) return [];
|
|
2624
|
-
const bindings = [];
|
|
2625
|
-
const turnDepth = options.turnDepths?.[turnFamily(role)] ?? 0;
|
|
2626
|
-
const shiftX = turnDepth * (options.parallaxUnit ?? 0);
|
|
2627
|
-
if (shiftX !== 0) {
|
|
2628
|
-
bindings.push({
|
|
2629
|
-
parameter: StandardParameter.AngleX,
|
|
2630
|
-
channel: "translateX",
|
|
2631
|
-
from: -shiftX,
|
|
2632
|
-
to: shiftX
|
|
2633
|
-
});
|
|
2634
|
-
}
|
|
2635
3321
|
const shiftY = nodDepth * (options.parallaxUnitY ?? 0);
|
|
2636
|
-
if (shiftY
|
|
2637
|
-
|
|
3322
|
+
if (shiftY === 0) return [];
|
|
3323
|
+
return [
|
|
3324
|
+
{
|
|
2638
3325
|
parameter: StandardParameter.AngleY,
|
|
2639
3326
|
channel: "translateY",
|
|
2640
3327
|
from: -shiftY,
|
|
2641
3328
|
to: shiftY
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
return bindings;
|
|
3329
|
+
}
|
|
3330
|
+
];
|
|
2645
3331
|
}
|
|
2646
3332
|
function bindingsForRole(spec, role, cropW, cropH, options = {}) {
|
|
2647
3333
|
return [
|
|
@@ -2749,20 +3435,17 @@ function roleOwnBindings(spec, role, cropW, cropH, options) {
|
|
|
2749
3435
|
];
|
|
2750
3436
|
}
|
|
2751
3437
|
}
|
|
2752
|
-
if (role === "
|
|
2753
|
-
const
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
if (shiftY !== 0) {
|
|
2758
|
-
parallax.push({
|
|
3438
|
+
if (role === "hair_back") {
|
|
3439
|
+
const shiftY = HAIR_BACK_NOD_DEPTH * (options.parallaxUnitY ?? 0);
|
|
3440
|
+
if (shiftY === 0) return [];
|
|
3441
|
+
return [
|
|
3442
|
+
{
|
|
2759
3443
|
parameter: StandardParameter.AngleY,
|
|
2760
3444
|
channel: "translateY",
|
|
2761
3445
|
from: -shiftY,
|
|
2762
3446
|
to: shiftY
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
return parallax;
|
|
3447
|
+
}
|
|
3448
|
+
];
|
|
2766
3449
|
}
|
|
2767
3450
|
return [];
|
|
2768
3451
|
}
|
|
@@ -2785,64 +3468,98 @@ function bakeEyelidFoldWarp(mesh, parameter, creaseOffsetY, k) {
|
|
|
2785
3468
|
]
|
|
2786
3469
|
};
|
|
2787
3470
|
}
|
|
2788
|
-
function
|
|
3471
|
+
function bakeNodWarp(mesh, partY, surface, nodTravel) {
|
|
3472
|
+
const keyforms = HEAD_TURN_STOPS.map((deg) => {
|
|
3473
|
+
const t = nodTravel * deg / HEAD_TURN_MAX_DEG;
|
|
3474
|
+
const offsets = [];
|
|
3475
|
+
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
3476
|
+
const y = partY + mesh.vertices[i + 1];
|
|
3477
|
+
offsets.push(0, deg === 0 ? 0 : t + surface.nodBendAt(y + t, deg));
|
|
3478
|
+
}
|
|
3479
|
+
return { value: deg, offsets };
|
|
3480
|
+
});
|
|
3481
|
+
return { parameter: StandardParameter.AngleY, keyforms };
|
|
3482
|
+
}
|
|
3483
|
+
function plateReach(plateLandingAt, faceCenterX, edgeAt, plateGuardRows) {
|
|
2789
3484
|
let reach = 0;
|
|
2790
3485
|
for (const deg of HEAD_TURN_STOPS) {
|
|
2791
3486
|
reach = Math.max(
|
|
2792
3487
|
reach,
|
|
2793
|
-
plateReachAt(
|
|
3488
|
+
plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg)
|
|
2794
3489
|
);
|
|
2795
3490
|
}
|
|
2796
3491
|
return reach;
|
|
2797
3492
|
}
|
|
2798
|
-
function
|
|
3493
|
+
function plateSideReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg, side) {
|
|
2799
3494
|
let reach = 0;
|
|
2800
|
-
for (const
|
|
2801
|
-
const dest =
|
|
3495
|
+
for (const y of plateGuardRows) {
|
|
3496
|
+
const dest = plateLandingAt(deg, faceCenterX + side * edgeAt(y), y);
|
|
2802
3497
|
reach = Math.max(reach, Math.abs(dest - faceCenterX));
|
|
2803
3498
|
}
|
|
2804
3499
|
return reach;
|
|
2805
3500
|
}
|
|
2806
|
-
function
|
|
3501
|
+
function plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg) {
|
|
3502
|
+
return Math.max(
|
|
3503
|
+
plateSideReachAt(
|
|
3504
|
+
plateLandingAt,
|
|
3505
|
+
faceCenterX,
|
|
3506
|
+
edgeAt,
|
|
3507
|
+
plateGuardRows,
|
|
3508
|
+
deg,
|
|
3509
|
+
-1
|
|
3510
|
+
),
|
|
3511
|
+
plateSideReachAt(
|
|
3512
|
+
plateLandingAt,
|
|
3513
|
+
faceCenterX,
|
|
3514
|
+
edgeAt,
|
|
3515
|
+
plateGuardRows,
|
|
3516
|
+
deg,
|
|
3517
|
+
1
|
|
3518
|
+
)
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows) {
|
|
2807
3522
|
if (holdEdgeAt(0) !== holdBase) {
|
|
2808
3523
|
throw new Error(
|
|
2809
3524
|
`auto-rig: bakeHairFrontSilhouetteWarp: holdEdgeAt(0) is ${holdEdgeAt(0)}, not the hold edge's own rest distance ${holdBase}, so the bangs would move in the rest pose`
|
|
2810
3525
|
);
|
|
2811
3526
|
}
|
|
2812
|
-
const SIDES = [-1, 1];
|
|
2813
3527
|
const keyforms = HEAD_TURN_STOPS.map((deg) => {
|
|
2814
|
-
const
|
|
2815
|
-
const
|
|
2816
|
-
|
|
2817
|
-
|
|
3528
|
+
const holdEdge = holdEdgeAt(deg);
|
|
3529
|
+
const reach = [-1, 1].map(
|
|
3530
|
+
(side) => plateSideReachAt(
|
|
3531
|
+
plateLandingAt,
|
|
3532
|
+
faceCenterX,
|
|
3533
|
+
edgeAt,
|
|
3534
|
+
plateGuardRows,
|
|
3535
|
+
deg,
|
|
3536
|
+
side
|
|
3537
|
+
)
|
|
2818
3538
|
);
|
|
2819
|
-
const reached =
|
|
2820
|
-
if (
|
|
2821
|
-
const side =
|
|
3539
|
+
const reached = Math.max(reach[0], reach[1]);
|
|
3540
|
+
if (holdEdge <= reached) {
|
|
3541
|
+
const side = reach[1] > reach[0] ? 1 : -1;
|
|
2822
3542
|
throw new Error(
|
|
2823
|
-
`auto-rig: bakeHairFrontSilhouetteWarp: at ${deg}\xB0 on the ${side < 0 ? "-x" : "+x"} side the hold edge sits ${
|
|
3543
|
+
`auto-rig: bakeHairFrontSilhouetteWarp: at ${deg}\xB0 on the ${side < 0 ? "-x" : "+x"} side the hold edge sits ${holdEdge} from the face centre but the plate's edge maps to ${reached}, so the ramp between them would fold`
|
|
2824
3544
|
);
|
|
2825
3545
|
}
|
|
2826
|
-
const holdEdge = deg === 0 ? [requestedHoldEdge, requestedHoldEdge] : SIDES.map(
|
|
2827
|
-
(_side, idx) => Math.min(
|
|
2828
|
-
requestedHoldEdge,
|
|
2829
|
-
Math.abs(
|
|
2830
|
-
map.warpedX[idx === 0 ? 0 : map.warpedX.length - 1] - faceCenterX
|
|
2831
|
-
) - HOLD_CLEARANCE
|
|
2832
|
-
)
|
|
2833
|
-
);
|
|
2834
3546
|
const offsets = [];
|
|
2835
3547
|
for (let i = 0; i < mesh.vertices.length; i += 2) {
|
|
2836
3548
|
const x = partX + mesh.vertices[i];
|
|
2837
|
-
const
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
3549
|
+
const y = partY + mesh.vertices[i + 1];
|
|
3550
|
+
offsets.push(
|
|
3551
|
+
deg === 0 ? 0 : hairFrontHoldTarget(
|
|
3552
|
+
x,
|
|
3553
|
+
y,
|
|
3554
|
+
deg,
|
|
3555
|
+
faceCenterX,
|
|
3556
|
+
plateLandingAt,
|
|
3557
|
+
edgeAt,
|
|
3558
|
+
holdBase,
|
|
3559
|
+
holdEdge
|
|
3560
|
+
) - x,
|
|
3561
|
+
0
|
|
2844
3562
|
);
|
|
2845
|
-
offsets.push(map.invertX(target) - x, 0);
|
|
2846
3563
|
}
|
|
2847
3564
|
return { value: deg, offsets };
|
|
2848
3565
|
});
|
|
@@ -2851,7 +3568,6 @@ function bakeHairFrontSilhouetteWarp(mesh, partX, faceCenterX, faceHalfWidth, ho
|
|
|
2851
3568
|
var HAIR_SWAY_TIP_FRACTION = 0.09;
|
|
2852
3569
|
var HAIR_SWAY_CURL = 1.5;
|
|
2853
3570
|
var HAIR_SWAY_RANGE = 20;
|
|
2854
|
-
var HAIR_SWAY_PEAK_FRACTION = 0.56;
|
|
2855
3571
|
function bakeHairSwayWarp(mesh, parameter, tipShift, range) {
|
|
2856
3572
|
let top = -Infinity;
|
|
2857
3573
|
let bottom = Infinity;
|
|
@@ -2881,112 +3597,385 @@ function headTurnTravel(faceHalfWidth) {
|
|
|
2881
3597
|
var BODY_TURN_FOLLOW = 0.3;
|
|
2882
3598
|
var HEAD_BREATH_BOB = -12;
|
|
2883
3599
|
var BODY_BREATH_FOLLOW = 0.5;
|
|
3600
|
+
var STANDARD_PARAMETERS = [
|
|
3601
|
+
{
|
|
3602
|
+
id: StandardParameter.MouthOpen,
|
|
3603
|
+
name: "Mouth Open",
|
|
3604
|
+
min: 0,
|
|
3605
|
+
max: 1,
|
|
3606
|
+
default: 0
|
|
3607
|
+
},
|
|
3608
|
+
{
|
|
3609
|
+
id: StandardParameter.MouthForm,
|
|
3610
|
+
name: "Mouth Form",
|
|
3611
|
+
min: -1,
|
|
3612
|
+
max: 1,
|
|
3613
|
+
default: 0
|
|
3614
|
+
},
|
|
3615
|
+
{
|
|
3616
|
+
id: StandardParameter.EyeOpenLeft,
|
|
3617
|
+
name: "Eye L",
|
|
3618
|
+
min: 0,
|
|
3619
|
+
max: 1,
|
|
3620
|
+
default: 1
|
|
3621
|
+
},
|
|
3622
|
+
{
|
|
3623
|
+
id: StandardParameter.EyeOpenRight,
|
|
3624
|
+
name: "Eye R",
|
|
3625
|
+
min: 0,
|
|
3626
|
+
max: 1,
|
|
3627
|
+
default: 1
|
|
3628
|
+
},
|
|
3629
|
+
{
|
|
3630
|
+
id: StandardParameter.EyeballX,
|
|
3631
|
+
name: "Gaze X",
|
|
3632
|
+
min: -1,
|
|
3633
|
+
max: 1,
|
|
3634
|
+
default: 0
|
|
3635
|
+
},
|
|
3636
|
+
{
|
|
3637
|
+
id: StandardParameter.EyeballY,
|
|
3638
|
+
name: "Gaze Y",
|
|
3639
|
+
min: -1,
|
|
3640
|
+
max: 1,
|
|
3641
|
+
default: 0
|
|
3642
|
+
},
|
|
3643
|
+
{
|
|
3644
|
+
id: StandardParameter.AngleX,
|
|
3645
|
+
name: "Head Angle",
|
|
3646
|
+
min: -30,
|
|
3647
|
+
max: 30,
|
|
3648
|
+
default: 0
|
|
3649
|
+
},
|
|
3650
|
+
{
|
|
3651
|
+
id: StandardParameter.AngleY,
|
|
3652
|
+
name: "Head Angle Y",
|
|
3653
|
+
min: -30,
|
|
3654
|
+
max: 30,
|
|
3655
|
+
default: 0
|
|
3656
|
+
},
|
|
3657
|
+
{
|
|
3658
|
+
id: StandardParameter.AngleZ,
|
|
3659
|
+
name: "Head Angle Z",
|
|
3660
|
+
min: -30,
|
|
3661
|
+
max: 30,
|
|
3662
|
+
default: 0
|
|
3663
|
+
},
|
|
3664
|
+
{
|
|
3665
|
+
id: StandardParameter.Breath,
|
|
3666
|
+
name: "Breath",
|
|
3667
|
+
min: 0,
|
|
3668
|
+
max: 1,
|
|
3669
|
+
default: 0
|
|
3670
|
+
},
|
|
3671
|
+
{
|
|
3672
|
+
id: StandardParameter.BrowLeftY,
|
|
3673
|
+
name: "Brow L Y",
|
|
3674
|
+
min: -1,
|
|
3675
|
+
max: 1,
|
|
3676
|
+
default: 0
|
|
3677
|
+
},
|
|
3678
|
+
{
|
|
3679
|
+
id: StandardParameter.BrowRightY,
|
|
3680
|
+
name: "Brow R Y",
|
|
3681
|
+
min: -1,
|
|
3682
|
+
max: 1,
|
|
3683
|
+
default: 0
|
|
3684
|
+
},
|
|
3685
|
+
{
|
|
3686
|
+
id: StandardParameter.BrowLeftAngle,
|
|
3687
|
+
name: "Brow L Angle",
|
|
3688
|
+
min: -1,
|
|
3689
|
+
max: 1,
|
|
3690
|
+
default: 0
|
|
3691
|
+
},
|
|
3692
|
+
{
|
|
3693
|
+
id: StandardParameter.BrowRightAngle,
|
|
3694
|
+
name: "Brow R Angle",
|
|
3695
|
+
min: -1,
|
|
3696
|
+
max: 1,
|
|
3697
|
+
default: 0
|
|
3698
|
+
}
|
|
3699
|
+
];
|
|
3700
|
+
function transformUnder(bindings, valueOf) {
|
|
3701
|
+
const trs = { x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1 };
|
|
3702
|
+
bindings.forEach((binding, i) => {
|
|
3703
|
+
const value = valueOf(binding, i);
|
|
3704
|
+
switch (binding.channel) {
|
|
3705
|
+
case "translateX":
|
|
3706
|
+
trs.x += value;
|
|
3707
|
+
break;
|
|
3708
|
+
case "translateY":
|
|
3709
|
+
trs.y += value;
|
|
3710
|
+
break;
|
|
3711
|
+
case "rotate":
|
|
3712
|
+
trs.rotation += value;
|
|
3713
|
+
break;
|
|
3714
|
+
case "scaleX":
|
|
3715
|
+
trs.scaleX += value;
|
|
3716
|
+
break;
|
|
3717
|
+
case "scaleY":
|
|
3718
|
+
trs.scaleY += value;
|
|
3719
|
+
break;
|
|
3720
|
+
case "opacity":
|
|
3721
|
+
break;
|
|
3722
|
+
}
|
|
3723
|
+
});
|
|
3724
|
+
return trs;
|
|
3725
|
+
}
|
|
3726
|
+
function defaultPoseOf(bindings) {
|
|
3727
|
+
return transformUnder(bindings, (binding) => {
|
|
3728
|
+
const p = STANDARD_PARAMETERS.find((p2) => p2.id === binding.parameter);
|
|
3729
|
+
if (p === void 0) {
|
|
3730
|
+
throw new Error(
|
|
3731
|
+
`auto-rig: defaultPoseOf: "${binding.parameter}" is not a standard parameter`
|
|
3732
|
+
);
|
|
3733
|
+
}
|
|
3734
|
+
const t = (p.default - p.min) / (p.max - p.min);
|
|
3735
|
+
return binding.from + (binding.to - binding.from) * t;
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
|
|
3739
|
+
let minX = Infinity;
|
|
3740
|
+
let maxX = -Infinity;
|
|
3741
|
+
let minY = Infinity;
|
|
3742
|
+
let maxY = -Infinity;
|
|
3743
|
+
for (const member of members) {
|
|
3744
|
+
const rest = member.mesh.vertices;
|
|
3745
|
+
const shapes = [rest];
|
|
3746
|
+
for (const warp of member.warps) {
|
|
3747
|
+
for (const keyform of warp.keyforms) {
|
|
3748
|
+
shapes.push(rest.map((v, i) => v + keyform.offsets[i]));
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
const geometric = member.bindings.filter((b) => b.channel !== "opacity");
|
|
3752
|
+
for (let corner = 0; corner < 1 << geometric.length; corner++) {
|
|
3753
|
+
const trs = transformUnder(
|
|
3754
|
+
geometric,
|
|
3755
|
+
(b, i) => (corner & 1 << i) === 0 ? b.from : b.to
|
|
3756
|
+
);
|
|
3757
|
+
const cos = Math.cos(trs.rotation * (Math.PI / 180));
|
|
3758
|
+
const sin = Math.sin(trs.rotation * (Math.PI / 180));
|
|
3759
|
+
for (const shape of shapes) {
|
|
3760
|
+
for (let i = 0; i < shape.length; i += 2) {
|
|
3761
|
+
const sx = shape[i] * trs.scaleX;
|
|
3762
|
+
const sy = shape[i + 1] * trs.scaleY;
|
|
3763
|
+
const x = member.transform.x + trs.x + cos * sx - sin * sy;
|
|
3764
|
+
const y = member.transform.y + trs.y + sin * sx + cos * sy;
|
|
3765
|
+
minX = Math.min(minX, x);
|
|
3766
|
+
maxX = Math.max(maxX, x);
|
|
3767
|
+
minY = Math.min(minY, y);
|
|
3768
|
+
maxY = Math.max(maxY, y);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
minX -= turnShiftPx;
|
|
3774
|
+
maxX += turnShiftPx;
|
|
3775
|
+
minY -= nodShiftPx;
|
|
3776
|
+
maxY += nodShiftPx;
|
|
3777
|
+
const marginX = (maxX - minX) * GRID_MARGIN + 1;
|
|
3778
|
+
const marginY = (maxY - minY) * GRID_MARGIN + 1;
|
|
3779
|
+
return {
|
|
3780
|
+
cols: cells,
|
|
3781
|
+
rows: cells,
|
|
3782
|
+
points: generateGridPoints(
|
|
3783
|
+
cells,
|
|
3784
|
+
cells,
|
|
3785
|
+
minX - marginX,
|
|
3786
|
+
maxX + marginX,
|
|
3787
|
+
minY - marginY,
|
|
3788
|
+
maxY + marginY
|
|
3789
|
+
)
|
|
3790
|
+
};
|
|
3791
|
+
}
|
|
3792
|
+
function eyelidFoldFor(layers, layer, mesh, t) {
|
|
3793
|
+
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
3794
|
+
const isLash = layer.role.startsWith("lash_");
|
|
3795
|
+
if (side === void 0 || !(layer.role.startsWith("eye_") || isLash)) {
|
|
3796
|
+
return void 0;
|
|
3797
|
+
}
|
|
3798
|
+
const white = layers.find((l) => l.role === `eye_${side}`);
|
|
3799
|
+
const creaseWorldY = bboxToTransform(white.bbox, white.canvasW, white.canvasH, white.role).y - EYELID_FOLD_CREASE * white.cropH;
|
|
3800
|
+
const hasLash = layers.some((l) => l.role === `lash_${side}`);
|
|
3801
|
+
return bakeEyelidFoldWarp(
|
|
3802
|
+
mesh,
|
|
3803
|
+
side === "L" ? StandardParameter.EyeOpenLeft : StandardParameter.EyeOpenRight,
|
|
3804
|
+
creaseWorldY - t.y,
|
|
3805
|
+
isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
function turnSetup(layers) {
|
|
3809
|
+
const faceLayer = layers.find((l) => l.role === "face");
|
|
3810
|
+
const faceTransform = bboxToTransform(
|
|
3811
|
+
faceLayer.bbox,
|
|
3812
|
+
faceLayer.canvasW,
|
|
3813
|
+
faceLayer.canvasH,
|
|
3814
|
+
"face"
|
|
3815
|
+
);
|
|
3816
|
+
const faceCenterX = faceTransform.x;
|
|
3817
|
+
const faceCenterY = faceTransform.y;
|
|
3818
|
+
const faceHalfWidth = faceLayer.cropW / 2;
|
|
3819
|
+
const plateEdgeX = faceCenterX - faceHalfWidth;
|
|
3820
|
+
const hasNose = layers.some((l) => l.role === "nose");
|
|
3821
|
+
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
3822
|
+
const turnLayers = layers.filter(
|
|
3823
|
+
(l) => isTurnGroup(ROLE_TABLE[l.role].deformer) || l.role === "hair_front"
|
|
3824
|
+
);
|
|
3825
|
+
const turnYs = turnLayers.map((l) => ({
|
|
3826
|
+
y: bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role).y,
|
|
3827
|
+
h: l.cropH
|
|
3828
|
+
}));
|
|
3829
|
+
let unionMinY = Math.min(...turnYs.map(({ y, h }) => y - h / 2));
|
|
3830
|
+
let unionMaxY = Math.max(...turnYs.map(({ y, h }) => y + h / 2));
|
|
3831
|
+
const spanY = unionMaxY - unionMinY;
|
|
3832
|
+
unionMinY -= spanY * GRID_MARGIN;
|
|
3833
|
+
unionMaxY += spanY * GRID_MARGIN;
|
|
3834
|
+
const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);
|
|
3835
|
+
const nodRadius = halfH * HEAD_CYLINDER_RADIUS_FACTOR;
|
|
3836
|
+
const parallaxUnitY = headNodParallaxUnit(halfH);
|
|
3837
|
+
const members = [];
|
|
3838
|
+
for (const layer of layers) {
|
|
3839
|
+
const spec = ROLE_TABLE[layer.role];
|
|
3840
|
+
if (!isTurnGroup(spec.deformer)) continue;
|
|
3841
|
+
const { role, cropW, cropH } = layer;
|
|
3842
|
+
const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
|
|
3843
|
+
const { cols, rows } = meshCellsFor(cropW, cropH);
|
|
3844
|
+
const mesh = createPixelGridMesh(cols, rows, cropW, cropH);
|
|
3845
|
+
const bindings = bindingsForRole(spec, role, cropW, cropH, {
|
|
3846
|
+
hasMouthOpen
|
|
3847
|
+
});
|
|
3848
|
+
const fold = eyelidFoldFor(layers, layer, mesh, t);
|
|
3849
|
+
const pose = defaultPoseOf(bindings);
|
|
3850
|
+
members.push({
|
|
3851
|
+
role,
|
|
3852
|
+
group: spec.deformer,
|
|
3853
|
+
transform: t,
|
|
3854
|
+
part: {
|
|
3855
|
+
x: t.x + pose.x,
|
|
3856
|
+
y: t.y + pose.y,
|
|
3857
|
+
cropW,
|
|
3858
|
+
cropH,
|
|
3859
|
+
cols,
|
|
3860
|
+
rows,
|
|
3861
|
+
scaleX: pose.scaleX,
|
|
3862
|
+
scaleY: pose.scaleY,
|
|
3863
|
+
rotation: pose.rotation
|
|
3864
|
+
},
|
|
3865
|
+
mesh,
|
|
3866
|
+
bindings,
|
|
3867
|
+
warps: fold === void 0 ? [] : [fold]
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
const landmarks = turnLandmarks(layers);
|
|
3871
|
+
const turnShiftOf = (marks) => hasNose && marks.length > 0 ? familyReachPx(marks, plateEdgeX) : 0;
|
|
3872
|
+
const turnShift = {
|
|
3873
|
+
eye: turnShiftOf(landmarks.eye),
|
|
3874
|
+
nose: turnShiftOf(landmarks.nose),
|
|
3875
|
+
mouth: turnShiftOf(landmarks.mouth)
|
|
3876
|
+
};
|
|
3877
|
+
const groupGrids = /* @__PURE__ */ new Map();
|
|
3878
|
+
for (const { group, role } of members) {
|
|
3879
|
+
if (groupGrids.has(group)) continue;
|
|
3880
|
+
const own = members.filter((m) => m.group === group);
|
|
3881
|
+
const isPlate = group === "faceWarp";
|
|
3882
|
+
const nodShiftPx = hasNose ? Math.max(
|
|
3883
|
+
...own.map((m) => FEATURE_NOD_DEPTH[roleFamily(m.role)] ?? 0)
|
|
3884
|
+
) * parallaxUnitY : 0;
|
|
3885
|
+
groupGrids.set(
|
|
3886
|
+
group,
|
|
3887
|
+
groupGridFor(
|
|
3888
|
+
own,
|
|
3889
|
+
isPlate ? FACE_PLATE_CELLS : FEATURE_GRID_CELLS,
|
|
3890
|
+
isPlate ? 0 : turnShift[turnFamily(role)],
|
|
3891
|
+
nodShiftPx
|
|
3892
|
+
)
|
|
3893
|
+
);
|
|
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
|
+
};
|
|
3901
|
+
const carriers = new Map(
|
|
3902
|
+
members.map((m) => [
|
|
3903
|
+
m.role,
|
|
3904
|
+
m.group === "mouthWarp" ? { grid: groupGrids.get(m.group), part: m.part, anchor: mouthAnchor } : { grid: groupGrids.get(m.group), part: m.part }
|
|
3905
|
+
])
|
|
3906
|
+
);
|
|
3907
|
+
const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
|
|
3908
|
+
let reach = 0;
|
|
3909
|
+
for (const grid of groupGrids.values()) {
|
|
3910
|
+
reach = Math.max(
|
|
3911
|
+
reach,
|
|
3912
|
+
faceCenterX - grid.points[0] + maxShift,
|
|
3913
|
+
grid.points[grid.cols * 2] - faceCenterX + maxShift
|
|
3914
|
+
);
|
|
3915
|
+
}
|
|
3916
|
+
const halfCells = Math.floor(reach / TURN_LATTICE_CELL_PX) + 1;
|
|
3917
|
+
const points = [];
|
|
3918
|
+
for (const y of [unionMaxY, unionMinY]) {
|
|
3919
|
+
for (let k = -halfCells; k <= halfCells; k++) {
|
|
3920
|
+
points.push(faceCenterX + k * TURN_LATTICE_CELL_PX, y);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
const lattice = { cols: 2 * halfCells, rows: 1, points };
|
|
3924
|
+
const hairFrontLayer = layers.find((l) => l.role === "hair_front");
|
|
3925
|
+
const hairFront = hairFrontLayer && (() => {
|
|
3926
|
+
const t = bboxToTransform(
|
|
3927
|
+
hairFrontLayer.bbox,
|
|
3928
|
+
hairFrontLayer.canvasW,
|
|
3929
|
+
hairFrontLayer.canvasH,
|
|
3930
|
+
"hair_front"
|
|
3931
|
+
);
|
|
3932
|
+
return {
|
|
3933
|
+
x: t.x,
|
|
3934
|
+
centerY: t.y,
|
|
3935
|
+
cropW: hairFrontLayer.cropW,
|
|
3936
|
+
cropH: hairFrontLayer.cropH
|
|
3937
|
+
};
|
|
3938
|
+
})();
|
|
3939
|
+
const eyeRowY = eyeRowOf(landmarks, faceCenterY);
|
|
3940
|
+
const profile = faceRowProfile(faceLayer, faceCenterY);
|
|
3941
|
+
return {
|
|
3942
|
+
landmarks: turnLandmarks(layers, carriers),
|
|
3943
|
+
lattice,
|
|
3944
|
+
faceCenterX,
|
|
3945
|
+
faceHalfWidth,
|
|
3946
|
+
faceCenterY,
|
|
3947
|
+
nodRadius,
|
|
3948
|
+
carriers,
|
|
3949
|
+
profile,
|
|
3950
|
+
hairFront,
|
|
3951
|
+
eyeRowY,
|
|
3952
|
+
...plateGuardsOf(carriers, faceHalfWidth, profile, hairFront),
|
|
3953
|
+
members,
|
|
3954
|
+
groupGrids,
|
|
3955
|
+
parallaxUnitY,
|
|
3956
|
+
hasNose,
|
|
3957
|
+
hasMouthOpen
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
function solveInputsOf(s) {
|
|
3961
|
+
return [
|
|
3962
|
+
s.landmarks,
|
|
3963
|
+
s.lattice,
|
|
3964
|
+
s.faceCenterX,
|
|
3965
|
+
s.faceHalfWidth,
|
|
3966
|
+
s.faceCenterY,
|
|
3967
|
+
s.nodRadius,
|
|
3968
|
+
s.carriers,
|
|
3969
|
+
s.profile,
|
|
3970
|
+
s.hairFront
|
|
3971
|
+
];
|
|
3972
|
+
}
|
|
2884
3973
|
function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
2885
3974
|
validateLayerInputs(layers, canvas);
|
|
3975
|
+
validateStrandEdges(layers, options.strandEdges);
|
|
2886
3976
|
const hasHair = layers.some((l) => l.role === "hair_front");
|
|
2887
|
-
const hasMouthOpen = layers.some((l) => l.role === "mouth_open");
|
|
2888
|
-
const hasNose = layers.some((l) => l.role === "nose");
|
|
2889
3977
|
const bodyLayer = layers.find((l) => l.role === "body");
|
|
2890
|
-
const parameters = [
|
|
2891
|
-
{
|
|
2892
|
-
id: StandardParameter.MouthOpen,
|
|
2893
|
-
name: "Mouth Open",
|
|
2894
|
-
min: 0,
|
|
2895
|
-
max: 1,
|
|
2896
|
-
default: 0
|
|
2897
|
-
},
|
|
2898
|
-
{
|
|
2899
|
-
id: StandardParameter.MouthForm,
|
|
2900
|
-
name: "Mouth Form",
|
|
2901
|
-
min: -1,
|
|
2902
|
-
max: 1,
|
|
2903
|
-
default: 0
|
|
2904
|
-
},
|
|
2905
|
-
{
|
|
2906
|
-
id: StandardParameter.EyeOpenLeft,
|
|
2907
|
-
name: "Eye L",
|
|
2908
|
-
min: 0,
|
|
2909
|
-
max: 1,
|
|
2910
|
-
default: 1
|
|
2911
|
-
},
|
|
2912
|
-
{
|
|
2913
|
-
id: StandardParameter.EyeOpenRight,
|
|
2914
|
-
name: "Eye R",
|
|
2915
|
-
min: 0,
|
|
2916
|
-
max: 1,
|
|
2917
|
-
default: 1
|
|
2918
|
-
},
|
|
2919
|
-
{
|
|
2920
|
-
id: StandardParameter.EyeballX,
|
|
2921
|
-
name: "Gaze X",
|
|
2922
|
-
min: -1,
|
|
2923
|
-
max: 1,
|
|
2924
|
-
default: 0
|
|
2925
|
-
},
|
|
2926
|
-
{
|
|
2927
|
-
id: StandardParameter.EyeballY,
|
|
2928
|
-
name: "Gaze Y",
|
|
2929
|
-
min: -1,
|
|
2930
|
-
max: 1,
|
|
2931
|
-
default: 0
|
|
2932
|
-
},
|
|
2933
|
-
{
|
|
2934
|
-
id: StandardParameter.AngleX,
|
|
2935
|
-
name: "Head Angle",
|
|
2936
|
-
min: -30,
|
|
2937
|
-
max: 30,
|
|
2938
|
-
default: 0
|
|
2939
|
-
},
|
|
2940
|
-
{
|
|
2941
|
-
id: StandardParameter.AngleY,
|
|
2942
|
-
name: "Head Angle Y",
|
|
2943
|
-
min: -30,
|
|
2944
|
-
max: 30,
|
|
2945
|
-
default: 0
|
|
2946
|
-
},
|
|
2947
|
-
{
|
|
2948
|
-
id: StandardParameter.AngleZ,
|
|
2949
|
-
name: "Head Angle Z",
|
|
2950
|
-
min: -30,
|
|
2951
|
-
max: 30,
|
|
2952
|
-
default: 0
|
|
2953
|
-
},
|
|
2954
|
-
{
|
|
2955
|
-
id: StandardParameter.Breath,
|
|
2956
|
-
name: "Breath",
|
|
2957
|
-
min: 0,
|
|
2958
|
-
max: 1,
|
|
2959
|
-
default: 0
|
|
2960
|
-
},
|
|
2961
|
-
{
|
|
2962
|
-
id: StandardParameter.BrowLeftY,
|
|
2963
|
-
name: "Brow L Y",
|
|
2964
|
-
min: -1,
|
|
2965
|
-
max: 1,
|
|
2966
|
-
default: 0
|
|
2967
|
-
},
|
|
2968
|
-
{
|
|
2969
|
-
id: StandardParameter.BrowRightY,
|
|
2970
|
-
name: "Brow R Y",
|
|
2971
|
-
min: -1,
|
|
2972
|
-
max: 1,
|
|
2973
|
-
default: 0
|
|
2974
|
-
},
|
|
2975
|
-
{
|
|
2976
|
-
id: StandardParameter.BrowLeftAngle,
|
|
2977
|
-
name: "Brow L Angle",
|
|
2978
|
-
min: -1,
|
|
2979
|
-
max: 1,
|
|
2980
|
-
default: 0
|
|
2981
|
-
},
|
|
2982
|
-
{
|
|
2983
|
-
id: StandardParameter.BrowRightAngle,
|
|
2984
|
-
name: "Brow R Angle",
|
|
2985
|
-
min: -1,
|
|
2986
|
-
max: 1,
|
|
2987
|
-
default: 0
|
|
2988
|
-
}
|
|
2989
|
-
];
|
|
3978
|
+
const parameters = [...STANDARD_PARAMETERS];
|
|
2990
3979
|
if (hasHair) {
|
|
2991
3980
|
parameters.push(
|
|
2992
3981
|
{
|
|
@@ -3005,87 +3994,31 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3005
3994
|
}
|
|
3006
3995
|
);
|
|
3007
3996
|
}
|
|
3008
|
-
const
|
|
3009
|
-
const
|
|
3010
|
-
|
|
3011
|
-
faceLayer.bbox,
|
|
3012
|
-
faceLayer.canvasW,
|
|
3013
|
-
faceLayer.canvasH,
|
|
3014
|
-
"face"
|
|
3015
|
-
);
|
|
3016
|
-
const faceCenterX = faceTransform.x;
|
|
3017
|
-
const faceCropH = faceLayer.cropH;
|
|
3018
|
-
const faceWarpLayers = layers.filter(
|
|
3019
|
-
(l) => ROLE_TABLE[l.role].deformer === "faceWarp"
|
|
3020
|
-
);
|
|
3021
|
-
let unionMinX = -canvas.width / 2;
|
|
3022
|
-
let unionMaxX = canvas.width / 2;
|
|
3023
|
-
let unionMinY = -canvas.height / 2;
|
|
3024
|
-
let unionMaxY = canvas.height / 2;
|
|
3025
|
-
if (faceWarpLayers.length > 0) {
|
|
3026
|
-
const transforms = faceWarpLayers.map(
|
|
3027
|
-
(l) => bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role)
|
|
3028
|
-
);
|
|
3029
|
-
unionMinX = Math.min(
|
|
3030
|
-
...transforms.map((t, i) => t.x - faceWarpLayers[i].cropW / 2)
|
|
3031
|
-
);
|
|
3032
|
-
unionMaxX = Math.max(
|
|
3033
|
-
...transforms.map((t, i) => t.x + faceWarpLayers[i].cropW / 2)
|
|
3034
|
-
);
|
|
3035
|
-
unionMinY = Math.min(
|
|
3036
|
-
...transforms.map((t, i) => t.y - faceWarpLayers[i].cropH / 2)
|
|
3037
|
-
);
|
|
3038
|
-
unionMaxY = Math.max(
|
|
3039
|
-
...transforms.map((t, i) => t.y + faceWarpLayers[i].cropH / 2)
|
|
3040
|
-
);
|
|
3041
|
-
const spanX = unionMaxX - unionMinX;
|
|
3042
|
-
const spanY = unionMaxY - unionMinY;
|
|
3043
|
-
const MARGIN = 0.12;
|
|
3044
|
-
unionMinX -= spanX * MARGIN;
|
|
3045
|
-
unionMaxX += spanX * MARGIN;
|
|
3046
|
-
unionMinY -= spanY * MARGIN;
|
|
3047
|
-
unionMaxY += spanY * MARGIN;
|
|
3048
|
-
}
|
|
3049
|
-
const halfW = Math.max(faceCenterX - unionMinX, unionMaxX - faceCenterX);
|
|
3050
|
-
const faceGridMinX = faceCenterX - halfW;
|
|
3051
|
-
const faceGridMaxX = faceCenterX + halfW;
|
|
3052
|
-
const faceGrid = {
|
|
3053
|
-
cols: FACE_GRID_CELLS,
|
|
3054
|
-
rows: FACE_GRID_CELLS,
|
|
3055
|
-
points: generateGridPoints(
|
|
3056
|
-
FACE_GRID_CELLS,
|
|
3057
|
-
FACE_GRID_CELLS,
|
|
3058
|
-
faceGridMinX,
|
|
3059
|
-
faceGridMaxX,
|
|
3060
|
-
unionMinY,
|
|
3061
|
-
unionMaxY
|
|
3062
|
-
)
|
|
3063
|
-
};
|
|
3064
|
-
const faceCenterY = faceTransform.y;
|
|
3065
|
-
const halfH = Math.max(faceCenterY - unionMinY, unionMaxY - faceCenterY);
|
|
3066
|
-
const faceHalfWidth = faceLayer.cropW / 2;
|
|
3067
|
-
const hairFrontLayer = layers.find((l) => l.role === "hair_front");
|
|
3068
|
-
const turn = hasNose ? solveTurnModel(
|
|
3069
|
-
resolveTurnTargets(options.turnTargets),
|
|
3070
|
-
turnLandmarks(layers),
|
|
3071
|
-
faceGrid,
|
|
3997
|
+
const setup = turnSetup(layers);
|
|
3998
|
+
const {
|
|
3999
|
+
lattice,
|
|
3072
4000
|
faceCenterX,
|
|
3073
4001
|
faceHalfWidth,
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
4002
|
+
faceCenterY,
|
|
4003
|
+
nodRadius,
|
|
4004
|
+
profile,
|
|
4005
|
+
eyeRowY,
|
|
4006
|
+
plate,
|
|
4007
|
+
edgeAt,
|
|
4008
|
+
plateGuardRows,
|
|
4009
|
+
members,
|
|
4010
|
+
groupGrids,
|
|
4011
|
+
carriers,
|
|
4012
|
+
parallaxUnitY,
|
|
4013
|
+
hasNose,
|
|
4014
|
+
hasMouthOpen
|
|
4015
|
+
} = setup;
|
|
4016
|
+
const faceCropH = plate.part.cropH;
|
|
4017
|
+
const turn = hasNose ? solveTurnModel(
|
|
4018
|
+
resolveTurnTargets(options.turnTargets),
|
|
4019
|
+
...solveInputsOf(setup),
|
|
4020
|
+
options.headEdges,
|
|
4021
|
+
options.strandEdges
|
|
3089
4022
|
) : void 0;
|
|
3090
4023
|
if (turn?.unreachable) {
|
|
3091
4024
|
throw new TurnTargetError(
|
|
@@ -3103,28 +4036,32 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3103
4036
|
holdBase: turn.holdBase,
|
|
3104
4037
|
depths: turn.depths,
|
|
3105
4038
|
achieved: turn.achieved,
|
|
3106
|
-
clamped: turn.clamped
|
|
4039
|
+
clamped: turn.clamped,
|
|
4040
|
+
...turn.strandOverlap === void 0 ? {} : { strandOverlap: turn.strandOverlap }
|
|
3107
4041
|
});
|
|
3108
4042
|
}
|
|
3109
|
-
const
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
const
|
|
4043
|
+
const latticeReach = Math.max(
|
|
4044
|
+
faceCenterX - lattice.points[0],
|
|
4045
|
+
lattice.points[lattice.cols * 2] - faceCenterX
|
|
4046
|
+
);
|
|
4047
|
+
const surface = turn?.surface ?? turnSurface({
|
|
4048
|
+
faceCenterX,
|
|
4049
|
+
faceCenterY,
|
|
4050
|
+
radius: latticeReach * HEAD_CYLINDER_RADIUS_FACTOR,
|
|
4051
|
+
travel: headTurnTravel(faceHalfWidth),
|
|
4052
|
+
nodRadius,
|
|
4053
|
+
lattice,
|
|
4054
|
+
profile,
|
|
4055
|
+
eyeRowY
|
|
4056
|
+
});
|
|
4057
|
+
const parallaxUnit = headTurnParallaxUnit(surface.radius);
|
|
4058
|
+
const plateLandingAt = plateLandingOn(plate, rowMapsOf(surface));
|
|
4059
|
+
const faceBottom = faceCenterY - faceCropH / 2;
|
|
3114
4060
|
const neckPivot = {
|
|
3115
4061
|
x: faceCenterX,
|
|
3116
4062
|
y: faceBottom - faceCropH * 0.15
|
|
3117
4063
|
// 15% below face bottom = neck
|
|
3118
4064
|
};
|
|
3119
|
-
const faceWarp2d = bakeHeadTurnGridWarp2DCentered(
|
|
3120
|
-
faceGrid,
|
|
3121
|
-
StandardParameter.AngleX,
|
|
3122
|
-
StandardParameter.AngleY,
|
|
3123
|
-
faceCenterX,
|
|
3124
|
-
faceCenterY,
|
|
3125
|
-
faceRadius,
|
|
3126
|
-
headTravel
|
|
3127
|
-
);
|
|
3128
4065
|
const deformers = [
|
|
3129
4066
|
// headDeformer: rigid matrix rotating/translating the whole head about the
|
|
3130
4067
|
// neck pivot. Same bindings as sample-model.ts apart from the turn, which
|
|
@@ -3134,14 +4071,14 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3134
4071
|
pivot: neckPivot,
|
|
3135
4072
|
bindings: [
|
|
3136
4073
|
// No AngleX binding: the turn moves NOTHING rigidly. Its sideways
|
|
3137
|
-
// travel is baked into
|
|
3138
|
-
//
|
|
3139
|
-
//
|
|
3140
|
-
//
|
|
3141
|
-
//
|
|
3142
|
-
//
|
|
3143
|
-
//
|
|
3144
|
-
//
|
|
4074
|
+
// travel is baked into every group grid (the surface's `travel`, see
|
|
4075
|
+
// bakeTurnGroupWarp2D) so the face slides inside a silhouette the
|
|
4076
|
+
// bangs hold; a translate here would take the hair shell with it. Nor
|
|
4077
|
+
// is the turn a roll — the sample model's ±6° "lean into the turn" was
|
|
4078
|
+
// tried here and dropped, since rotating about the neck pivot swings
|
|
4079
|
+
// the crown (~500px above it) far more than the chin, so the top of
|
|
4080
|
+
// the head appeared to lunge ahead of the face on every turn. Roll is
|
|
4081
|
+
// AngleZ's job, below.
|
|
3145
4082
|
// Nod: a vertical translate only. No rotate — a pitch expressed as a
|
|
3146
4083
|
// rigid rotation would sum with the AngleZ roll below at diagonal
|
|
3147
4084
|
// poses, collapsing pitch into roll.
|
|
@@ -3170,19 +4107,27 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3170
4107
|
to: HEAD_BREATH_BOB
|
|
3171
4108
|
}
|
|
3172
4109
|
]
|
|
3173
|
-
},
|
|
3174
|
-
// faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric
|
|
3175
|
-
// about faceCenterX so the bake's cylinder axis aligns with the face center.
|
|
3176
|
-
// One 2D warp carries both the turn and the nod (a deformer holds either
|
|
3177
|
-
// `warps` or `warp2d`, never both).
|
|
3178
|
-
{
|
|
3179
|
-
kind: "warp",
|
|
3180
|
-
id: "faceWarp",
|
|
3181
|
-
parent: "headDeformer",
|
|
3182
|
-
grid: faceGrid,
|
|
3183
|
-
warp2d: faceWarp2d
|
|
3184
4110
|
}
|
|
3185
4111
|
];
|
|
4112
|
+
for (const [group, grid] of groupGrids) {
|
|
4113
|
+
const { role } = members.find((m) => m.group === group);
|
|
4114
|
+
const family = turnFamily(role);
|
|
4115
|
+
const depth = group === "faceWarp" ? 0 : turn?.depths[family] ?? 0;
|
|
4116
|
+
deformers.push({
|
|
4117
|
+
kind: "warp",
|
|
4118
|
+
id: group,
|
|
4119
|
+
parent: "headDeformer",
|
|
4120
|
+
grid,
|
|
4121
|
+
warp2d: bakeTurnGroupWarp2D(
|
|
4122
|
+
grid,
|
|
4123
|
+
StandardParameter.AngleX,
|
|
4124
|
+
StandardParameter.AngleY,
|
|
4125
|
+
surface,
|
|
4126
|
+
(deg) => depth * parallaxUnit * deg / HEAD_TURN_MAX_DEG,
|
|
4127
|
+
carriers.get(role).anchor
|
|
4128
|
+
)
|
|
4129
|
+
});
|
|
4130
|
+
}
|
|
3186
4131
|
if (bodyLayer) {
|
|
3187
4132
|
const bt = bboxToTransform(
|
|
3188
4133
|
bodyLayer.bbox,
|
|
@@ -3199,8 +4144,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3199
4144
|
{
|
|
3200
4145
|
parameter: StandardParameter.AngleX,
|
|
3201
4146
|
channel: "translateX",
|
|
3202
|
-
from: -BODY_TURN_FOLLOW *
|
|
3203
|
-
to: BODY_TURN_FOLLOW *
|
|
4147
|
+
from: -BODY_TURN_FOLLOW * surface.travel,
|
|
4148
|
+
to: BODY_TURN_FOLLOW * surface.travel
|
|
3204
4149
|
},
|
|
3205
4150
|
// Breath: follow the head's bob, same direction, half amplitude.
|
|
3206
4151
|
{
|
|
@@ -3212,34 +4157,21 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3212
4157
|
]
|
|
3213
4158
|
});
|
|
3214
4159
|
}
|
|
3215
|
-
const
|
|
3216
|
-
for (const layer of layers) {
|
|
3217
|
-
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
3218
|
-
if ((layer.role === "eye_L" || layer.role === "eye_R") && side) {
|
|
3219
|
-
const ey = bboxToTransform(
|
|
3220
|
-
layer.bbox,
|
|
3221
|
-
layer.canvasW,
|
|
3222
|
-
layer.canvasH,
|
|
3223
|
-
layer.role
|
|
3224
|
-
).y;
|
|
3225
|
-
eyeCreaseBySide[side] = ey - EYELID_FOLD_CREASE * layer.cropH;
|
|
3226
|
-
}
|
|
3227
|
-
}
|
|
4160
|
+
const memberByRole = new Map(members.map((m) => [m.role, m]));
|
|
3228
4161
|
const parts = layers.map((layer) => {
|
|
3229
4162
|
const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
|
|
3230
4163
|
const spec = ROLE_TABLE[role];
|
|
3231
4164
|
const t = bboxToTransform(bbox, canvasW, canvasH, role);
|
|
3232
4165
|
const roleBindings = bindingsForRole(spec, role, cropW, cropH, {
|
|
3233
4166
|
hasMouthOpen,
|
|
3234
|
-
parallaxUnit,
|
|
3235
4167
|
parallaxUnitY,
|
|
3236
|
-
hasNose
|
|
3237
|
-
turnDepths: turn?.depths
|
|
4168
|
+
hasNose
|
|
3238
4169
|
});
|
|
3239
4170
|
const deformerId = spec.deformer === "none" ? void 0 : spec.deformer;
|
|
3240
4171
|
if (spec.mesh) {
|
|
4172
|
+
const member = memberByRole.get(role);
|
|
3241
4173
|
const { cols, rows } = meshCellsFor(cropW, cropH);
|
|
3242
|
-
const mesh = createPixelGridMesh(cols, rows, cropW, cropH);
|
|
4174
|
+
const mesh = member?.mesh ?? createPixelGridMesh(cols, rows, cropW, cropH);
|
|
3243
4175
|
const part = {
|
|
3244
4176
|
id: role,
|
|
3245
4177
|
color: [1, 1, 1, 1],
|
|
@@ -3254,35 +4186,38 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3254
4186
|
part.bindings = roleBindings;
|
|
3255
4187
|
}
|
|
3256
4188
|
if ((role === "hair_front" || role === "hair_back") && hasHair) {
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
let silhouetteWarp;
|
|
4189
|
+
const tipShift = HAIR_SWAY_TIP_FRACTION * cropH;
|
|
4190
|
+
const ownWarps = [];
|
|
3260
4191
|
if (role === "hair_front") {
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
Math.max(0, headroom) / HAIR_SWAY_PEAK_FRACTION
|
|
4192
|
+
ownWarps.push(
|
|
4193
|
+
bakeHairSwayWarp(
|
|
4194
|
+
mesh,
|
|
4195
|
+
StandardParameter.AngleX,
|
|
4196
|
+
HAIR_FRONT_DEPTH * parallaxUnit,
|
|
4197
|
+
HEAD_TURN_MAX_DEG
|
|
4198
|
+
)
|
|
3269
4199
|
);
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
4200
|
+
const holdBase = turn?.holdBase ?? plateReach(plateLandingAt, faceCenterX, edgeAt, plateGuardRows) + HOLD_CLEARANCE;
|
|
4201
|
+
ownWarps.push(
|
|
4202
|
+
bakeHairFrontSilhouetteWarp(
|
|
4203
|
+
mesh,
|
|
4204
|
+
t.x,
|
|
4205
|
+
t.y,
|
|
4206
|
+
faceCenterX,
|
|
4207
|
+
plateLandingAt,
|
|
4208
|
+
edgeAt,
|
|
4209
|
+
holdBase,
|
|
4210
|
+
turn?.holdEdgeAt ?? (() => holdBase),
|
|
4211
|
+
plateGuardRows
|
|
4212
|
+
)
|
|
3275
4213
|
);
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
holdBase,
|
|
3284
|
-
turn?.holdEdgeAt ?? (() => holdBase),
|
|
3285
|
-
columnMapAt
|
|
4214
|
+
ownWarps.push(
|
|
4215
|
+
bakeNodWarp(
|
|
4216
|
+
mesh,
|
|
4217
|
+
t.y,
|
|
4218
|
+
surface,
|
|
4219
|
+
hasNose ? HAIR_FRONT_NOD_DEPTH * parallaxUnitY : 0
|
|
4220
|
+
)
|
|
3286
4221
|
);
|
|
3287
4222
|
}
|
|
3288
4223
|
part.warps = [
|
|
@@ -3298,24 +4233,12 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
3298
4233
|
tipShift,
|
|
3299
4234
|
HAIR_SWAY_RANGE
|
|
3300
4235
|
),
|
|
3301
|
-
...
|
|
3302
|
-
...silhouetteWarp ? [silhouetteWarp] : []
|
|
4236
|
+
...ownWarps
|
|
3303
4237
|
];
|
|
3304
4238
|
}
|
|
3305
4239
|
if (spec.eyeSide !== void 0) {
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
const openParam = spec.eyeSide === "L" ? StandardParameter.EyeOpenLeft : StandardParameter.EyeOpenRight;
|
|
3309
|
-
const creaseWorldY = eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;
|
|
3310
|
-
const hasLash = layers.some((l) => l.role === `lash_${spec.eyeSide}`);
|
|
3311
|
-
part.warps = [
|
|
3312
|
-
bakeEyelidFoldWarp(
|
|
3313
|
-
mesh,
|
|
3314
|
-
openParam,
|
|
3315
|
-
creaseWorldY - t.y,
|
|
3316
|
-
isLash ? LASH_FOLD_K : hasLash ? 0 : EYELID_FOLD_K
|
|
3317
|
-
)
|
|
3318
|
-
];
|
|
4240
|
+
if (role.startsWith("eye_") || role.startsWith("lash_")) {
|
|
4241
|
+
part.warps = [...member.warps];
|
|
3319
4242
|
} else {
|
|
3320
4243
|
part.clip = { masks: [`eye_${spec.eyeSide}`] };
|
|
3321
4244
|
}
|