@ikijs/editor 0.8.0 → 0.9.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 +37 -28
- package/dist/index.d.mts +405 -199
- package/dist/index.d.ts +405 -199
- package/dist/index.js +325 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +317 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ALPHA_BBOX_THRESHOLD: () => ALPHA_BBOX_THRESHOLD,
|
|
24
|
+
ALPHA_OPAQUE: () => ALPHA_OPAQUE,
|
|
24
25
|
ATLAS_PADDING: () => ATLAS_PADDING,
|
|
25
26
|
AddDeformer: () => AddDeformer,
|
|
26
27
|
AddPart: () => AddPart,
|
|
@@ -31,6 +32,8 @@ __export(index_exports, {
|
|
|
31
32
|
DeletePart: () => DeletePart,
|
|
32
33
|
DeletePhysicsRig: () => DeletePhysicsRig,
|
|
33
34
|
EditorDocument: () => EditorDocument,
|
|
35
|
+
HEAD_BAND: () => HEAD_BAND,
|
|
36
|
+
SPECK_CORE_FRACTION: () => SPECK_CORE_FRACTION,
|
|
34
37
|
SetDeformerBindings: () => SetDeformerBindings,
|
|
35
38
|
SetDeformerParent: () => SetDeformerParent,
|
|
36
39
|
SetDeformerPivot: () => SetDeformerPivot,
|
|
@@ -54,9 +57,14 @@ __export(index_exports, {
|
|
|
54
57
|
createDefaultPart: () => createDefaultPart,
|
|
55
58
|
createDefaultWarpDeformer: () => createDefaultWarpDeformer,
|
|
56
59
|
createGridMesh: () => createGridMesh,
|
|
60
|
+
createLayerSetMeasurer: () => createLayerSetMeasurer,
|
|
61
|
+
denseCoreOf: () => denseCoreOf,
|
|
57
62
|
detectAlphaBbox: () => detectAlphaBbox,
|
|
63
|
+
foregroundSpan: () => foregroundSpan,
|
|
58
64
|
generateIkiFromLayerSet: () => generateIkiFromLayerSet,
|
|
65
|
+
headHalfOf: () => headHalfOf,
|
|
59
66
|
interpolateGridOffsets: () => interpolateGridOffsets,
|
|
67
|
+
isSpeckCore: () => isSpeckCore,
|
|
60
68
|
packAtlas: () => packAtlas,
|
|
61
69
|
parseLayerRoles: () => parseLayerRoles,
|
|
62
70
|
upsertGridKeyform: () => upsertGridKeyform,
|
|
@@ -1485,6 +1493,242 @@ function detectAlphaBbox(rgba, width, height) {
|
|
|
1485
1493
|
return { x, y, w: x2 - x + 1, h: y2 - y + 1 };
|
|
1486
1494
|
}
|
|
1487
1495
|
|
|
1496
|
+
// src/layer-measure.ts
|
|
1497
|
+
var HEAD_BAND = 10;
|
|
1498
|
+
var ALPHA_OPAQUE = 128;
|
|
1499
|
+
function denseCoreOf(rgba, width, height) {
|
|
1500
|
+
let minX = width;
|
|
1501
|
+
let minY = height;
|
|
1502
|
+
let maxX = -1;
|
|
1503
|
+
let maxY = -1;
|
|
1504
|
+
for (let y = 0; y < height; y++) {
|
|
1505
|
+
for (let x = 0; x < width; x++) {
|
|
1506
|
+
if (rgba[(y * width + x) * 4 + 3] >= ALPHA_OPAQUE) {
|
|
1507
|
+
if (x < minX) minX = x;
|
|
1508
|
+
if (x > maxX) maxX = x;
|
|
1509
|
+
if (y < minY) minY = y;
|
|
1510
|
+
if (y > maxY) maxY = y;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (maxX < 0) return null;
|
|
1515
|
+
return { x: minX, y: minY, w: maxX - minX + 1, h: maxY - minY + 1 };
|
|
1516
|
+
}
|
|
1517
|
+
var SPECK_CORE_FRACTION = 0.25;
|
|
1518
|
+
function isSpeckCore(core, part) {
|
|
1519
|
+
return core.w < SPECK_CORE_FRACTION * part.w || core.h < SPECK_CORE_FRACTION * part.h;
|
|
1520
|
+
}
|
|
1521
|
+
function foregroundSpan(mask, width, height, rowLo, rowHi) {
|
|
1522
|
+
let left = width;
|
|
1523
|
+
let right = -1;
|
|
1524
|
+
for (let y = Math.max(0, rowLo); y <= Math.min(height - 1, rowHi); y++) {
|
|
1525
|
+
for (let x = 0; x < width; x++) {
|
|
1526
|
+
if (!mask[y * width + x]) continue;
|
|
1527
|
+
if (x < left) left = x;
|
|
1528
|
+
if (x > right) right = x;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return { left, right };
|
|
1532
|
+
}
|
|
1533
|
+
function headHalfOf(span) {
|
|
1534
|
+
return (span.right - span.left + 1) / 2;
|
|
1535
|
+
}
|
|
1536
|
+
function eyeBandRowOf(layers) {
|
|
1537
|
+
const centreY = (role) => {
|
|
1538
|
+
const layer = layers.find((l) => l.role === role);
|
|
1539
|
+
return layer && layer.bbox.y + layer.bbox.h / 2;
|
|
1540
|
+
};
|
|
1541
|
+
const pairRow = (left, right) => {
|
|
1542
|
+
const a = centreY(left);
|
|
1543
|
+
const b = centreY(right);
|
|
1544
|
+
return a === void 0 || b === void 0 ? void 0 : (a + b) / 2;
|
|
1545
|
+
};
|
|
1546
|
+
const row = pairRow("iris_L", "iris_R") ?? pairRow("eye_L", "eye_R");
|
|
1547
|
+
if (row === void 0) {
|
|
1548
|
+
throw new Error("auto-rig: no eye pair to measure the head width at");
|
|
1549
|
+
}
|
|
1550
|
+
return Math.round(row);
|
|
1551
|
+
}
|
|
1552
|
+
function foregroundSpanOfRows(rowLeft, rowRight, rowLo, rowHi) {
|
|
1553
|
+
let left = Infinity;
|
|
1554
|
+
let right = -Infinity;
|
|
1555
|
+
for (let y = Math.max(0, rowLo); y <= Math.min(rowLeft.length - 1, rowHi); y++) {
|
|
1556
|
+
if (rowLeft[y] < left) left = rowLeft[y];
|
|
1557
|
+
if (rowRight[y] > right) right = rowRight[y];
|
|
1558
|
+
}
|
|
1559
|
+
return { left, right };
|
|
1560
|
+
}
|
|
1561
|
+
function facePlateHalfOf(layers) {
|
|
1562
|
+
const face = layers.find((l) => l.role === "face");
|
|
1563
|
+
if (face === void 0) {
|
|
1564
|
+
throw new Error("auto-rig: no face layer to measure the plate against");
|
|
1565
|
+
}
|
|
1566
|
+
return face.cropW / 2;
|
|
1567
|
+
}
|
|
1568
|
+
var STRAND_MIN_RUN_FRACTION = 0.5;
|
|
1569
|
+
function createLayerSetMeasurer(canvas) {
|
|
1570
|
+
for (const field of ["width", "height"]) {
|
|
1571
|
+
const value = canvas[field];
|
|
1572
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1573
|
+
throw new Error(
|
|
1574
|
+
`auto-rig: createLayerSetMeasurer: canvas.${field} must be a positive safe integer, got ${value}`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
const { width: canvasW, height: canvasH } = canvas;
|
|
1579
|
+
const rgbaLength = canvasW * canvasH * 4;
|
|
1580
|
+
if (!Number.isSafeInteger(rgbaLength)) {
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
`auto-rig: createLayerSetMeasurer: canvas ${canvasW}x${canvasH} is too large: width \xD7 height \xD7 4 = ${rgbaLength} is not a safe integer`
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
const opaque = new Uint8Array(canvasW * canvasH);
|
|
1586
|
+
const layerInputs = [];
|
|
1587
|
+
const rowSpansByRole = [];
|
|
1588
|
+
let hairFrontMask;
|
|
1589
|
+
function add({ role, fileName, rgba }) {
|
|
1590
|
+
if (rgba.length !== rgbaLength) {
|
|
1591
|
+
throw new Error(
|
|
1592
|
+
`auto-rig: layer "${fileName}": rgba length ${rgba.length} is not ${canvasW}x${canvasH}x4 = ${rgbaLength}`
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
const bbox = detectAlphaBbox(rgba, canvasW, canvasH);
|
|
1596
|
+
if (bbox === null) return null;
|
|
1597
|
+
const rowLeft = new Int32Array(canvasH).fill(canvasW);
|
|
1598
|
+
const rowRight = new Int32Array(canvasH).fill(-1);
|
|
1599
|
+
const ownMask = role === "hair_front" ? new Uint8Array(canvasW * canvasH) : void 0;
|
|
1600
|
+
for (let y = 0; y < canvasH; y++) {
|
|
1601
|
+
for (let x = 0; x < canvasW; x++) {
|
|
1602
|
+
const p = y * canvasW + x;
|
|
1603
|
+
if (rgba[p * 4 + 3] < ALPHA_OPAQUE) continue;
|
|
1604
|
+
opaque[p] = 1;
|
|
1605
|
+
if (ownMask !== void 0) ownMask[p] = 1;
|
|
1606
|
+
if (x < rowLeft[y]) rowLeft[y] = x;
|
|
1607
|
+
if (x > rowRight[y]) rowRight[y] = x;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
rowSpansByRole.push({ role, rowLeft, rowRight });
|
|
1611
|
+
if (ownMask !== void 0) hairFrontMask = ownMask;
|
|
1612
|
+
const layer = {
|
|
1613
|
+
role,
|
|
1614
|
+
fileName,
|
|
1615
|
+
canvasW,
|
|
1616
|
+
canvasH,
|
|
1617
|
+
bbox,
|
|
1618
|
+
cropW: bbox.w,
|
|
1619
|
+
cropH: bbox.h
|
|
1620
|
+
};
|
|
1621
|
+
if (role === "face") {
|
|
1622
|
+
const rowHalfWidths = [];
|
|
1623
|
+
for (let y = bbox.y; y < bbox.y + bbox.h; y++) {
|
|
1624
|
+
rowHalfWidths.push(
|
|
1625
|
+
rowRight[y] >= rowLeft[y] ? headHalfOf({ left: rowLeft[y], right: rowRight[y] }) : 0
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
layer.rowHalfWidths = rowHalfWidths;
|
|
1629
|
+
} else if (role === "nose") {
|
|
1630
|
+
const core = denseCoreOf(rgba, canvasW, canvasH);
|
|
1631
|
+
if (core !== null && !isSpeckCore(core, bbox)) layer.denseCore = core;
|
|
1632
|
+
}
|
|
1633
|
+
layerInputs.push(layer);
|
|
1634
|
+
return layer;
|
|
1635
|
+
}
|
|
1636
|
+
function finish() {
|
|
1637
|
+
const eyeRow = eyeBandRowOf(layerInputs);
|
|
1638
|
+
const span = foregroundSpan(
|
|
1639
|
+
opaque,
|
|
1640
|
+
canvasW,
|
|
1641
|
+
canvasH,
|
|
1642
|
+
eyeRow - HEAD_BAND,
|
|
1643
|
+
eyeRow + HEAD_BAND
|
|
1644
|
+
);
|
|
1645
|
+
const headHalfWidth = span.right > span.left ? headHalfOf(span) : void 0;
|
|
1646
|
+
const headHalfWidthApplied = headHalfWidth !== void 0 && headHalfWidth > facePlateHalfOf(layerInputs);
|
|
1647
|
+
let headEdges;
|
|
1648
|
+
if (headHalfWidthApplied) {
|
|
1649
|
+
const bandLo = eyeRow - HEAD_BAND;
|
|
1650
|
+
const bandHi = eyeRow + HEAD_BAND;
|
|
1651
|
+
const left = [];
|
|
1652
|
+
const right = [];
|
|
1653
|
+
for (const { role, rowLeft, rowRight } of rowSpansByRole) {
|
|
1654
|
+
const own = foregroundSpanOfRows(rowLeft, rowRight, bandLo, bandHi);
|
|
1655
|
+
if (own.right <= own.left) continue;
|
|
1656
|
+
left.push({ role, x: own.left - canvasW / 2 });
|
|
1657
|
+
right.push({ role, x: own.right - canvasW / 2 });
|
|
1658
|
+
}
|
|
1659
|
+
headEdges = { left, right };
|
|
1660
|
+
}
|
|
1661
|
+
let strandEdges;
|
|
1662
|
+
const irisLayers = layerInputs.filter((l) => l.role === "iris_L" || l.role === "iris_R").sort((a, b) => a.bbox.x + a.bbox.w / 2 - (b.bbox.x + b.bbox.w / 2));
|
|
1663
|
+
if (hairFrontMask !== void 0 && irisLayers.length === 2) {
|
|
1664
|
+
const mask = hairFrontMask;
|
|
1665
|
+
const half = canvasW / 2;
|
|
1666
|
+
const strandOn = (iris, other, side) => {
|
|
1667
|
+
const r = Math.floor(iris.bbox.y + iris.bbox.h / 2);
|
|
1668
|
+
const centre = iris.bbox.x + iris.bbox.w / 2;
|
|
1669
|
+
const otherX = other.bbox.x + other.bbox.w / 2 - half;
|
|
1670
|
+
const c = side < 0 ? Math.ceil(centre) - 1 : Math.floor(centre);
|
|
1671
|
+
const span2 = rowSpansByRole.find((s) => s.role === iris.role);
|
|
1672
|
+
if (span2.rowRight[r] < span2.rowLeft[r]) return void 0;
|
|
1673
|
+
const minRun = STRAND_MIN_RUN_FRACTION * (span2.rowRight[r] + 1 - span2.rowLeft[r]);
|
|
1674
|
+
const strand = new Uint8Array(canvasW);
|
|
1675
|
+
for (let a = 0; a < canvasW; ) {
|
|
1676
|
+
if (mask[r * canvasW + a] !== 1) {
|
|
1677
|
+
a++;
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
let b = a;
|
|
1681
|
+
while (b < canvasW && mask[r * canvasW + b] === 1) b++;
|
|
1682
|
+
if (b - a >= minRun) strand.fill(1, a, b);
|
|
1683
|
+
a = b;
|
|
1684
|
+
}
|
|
1685
|
+
const strandAt = (x2) => x2 >= 0 && x2 < canvasW && strand[x2] === 1;
|
|
1686
|
+
const outerBoundary = (x2) => (side < 0 ? x2 : x2 + 1) - half;
|
|
1687
|
+
let x = c;
|
|
1688
|
+
let runFace;
|
|
1689
|
+
if (!strandAt(c)) {
|
|
1690
|
+
while (x >= 0 && x < canvasW && !strandAt(x)) x += side;
|
|
1691
|
+
if (x < 0 || x >= canvasW) return void 0;
|
|
1692
|
+
runFace = outerBoundary(x - side);
|
|
1693
|
+
} else {
|
|
1694
|
+
let f = c - side;
|
|
1695
|
+
while (strandAt(f)) f -= side;
|
|
1696
|
+
const face = outerBoundary(f);
|
|
1697
|
+
runFace = side * (face - otherX) > 0 ? face : null;
|
|
1698
|
+
}
|
|
1699
|
+
while (strandAt(x)) x += side;
|
|
1700
|
+
return {
|
|
1701
|
+
y: canvasH / 2 - (r + 0.5),
|
|
1702
|
+
irisOuter: (side < 0 ? span2.rowLeft[r] : span2.rowRight[r] + 1) - half,
|
|
1703
|
+
irisInner: (side < 0 ? span2.rowRight[r] + 1 : span2.rowLeft[r]) - half,
|
|
1704
|
+
runOuter: outerBoundary(x - side),
|
|
1705
|
+
runFace
|
|
1706
|
+
};
|
|
1707
|
+
};
|
|
1708
|
+
const [low, high] = irisLayers;
|
|
1709
|
+
const left = strandOn(low, high, -1);
|
|
1710
|
+
const right = strandOn(high, low, 1);
|
|
1711
|
+
if (left !== void 0 || right !== void 0) {
|
|
1712
|
+
strandEdges = {
|
|
1713
|
+
...left === void 0 ? {} : { left },
|
|
1714
|
+
...right === void 0 ? {} : { right }
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return {
|
|
1719
|
+
layers: layerInputs,
|
|
1720
|
+
...headHalfWidth === void 0 ? {} : { headHalfWidth },
|
|
1721
|
+
headHalfWidthApplied,
|
|
1722
|
+
turnOptions: {
|
|
1723
|
+
...headHalfWidthApplied ? { turnTargets: { headHalfWidth } } : {},
|
|
1724
|
+
...headEdges === void 0 ? {} : { headEdges },
|
|
1725
|
+
...strandEdges === void 0 ? {} : { strandEdges }
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
return { add, finish };
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1488
1732
|
// src/binding-capture.ts
|
|
1489
1733
|
function captureBindingEndpoint(channel, restValue, posedValue) {
|
|
1490
1734
|
if (channel === "opacity") {
|
|
@@ -2052,6 +2296,7 @@ function meshCellsFor(w, h) {
|
|
|
2052
2296
|
}
|
|
2053
2297
|
var FACE_PLATE_CELLS = 10;
|
|
2054
2298
|
var FEATURE_GRID_CELLS = 4;
|
|
2299
|
+
var MOUTH_GRID_CELL_PX = 32;
|
|
2055
2300
|
var TURN_LATTICE_CELL_PX = 16;
|
|
2056
2301
|
var GRID_MARGIN = 0.12;
|
|
2057
2302
|
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
@@ -2202,31 +2447,33 @@ function turnColumnMap(grid, faceCenterX, radiusX, angleX, travel) {
|
|
|
2202
2447
|
x + boundedCylinderBend(x - faceCenterX, radiusX, theta) + slide
|
|
2203
2448
|
);
|
|
2204
2449
|
}
|
|
2205
|
-
const cellFor = (
|
|
2450
|
+
const cellFor = (x) => {
|
|
2206
2451
|
let lo = 0;
|
|
2207
2452
|
let hi = grid.cols - 1;
|
|
2208
2453
|
while (lo < hi) {
|
|
2209
2454
|
const mid = lo + hi >> 1;
|
|
2210
|
-
if (
|
|
2455
|
+
if (x < restX[mid + 1]) hi = mid;
|
|
2211
2456
|
else lo = mid + 1;
|
|
2212
2457
|
}
|
|
2213
2458
|
return lo;
|
|
2214
2459
|
};
|
|
2215
|
-
const sample = (edges, values, v) => {
|
|
2216
|
-
const c = cellFor(v, edges);
|
|
2217
|
-
const s = Math.max(
|
|
2218
|
-
0,
|
|
2219
|
-
Math.min(1, (v - edges[c]) / (edges[c + 1] - edges[c]))
|
|
2220
|
-
);
|
|
2221
|
-
if (s === 0) return values[c];
|
|
2222
|
-
if (s === 1) return values[c + 1];
|
|
2223
|
-
return values[c] + (values[c + 1] - values[c]) * s;
|
|
2224
|
-
};
|
|
2225
2460
|
return {
|
|
2226
2461
|
restX,
|
|
2227
2462
|
warpedX,
|
|
2228
|
-
|
|
2229
|
-
|
|
2463
|
+
// Piecewise-linear read of `warpedX` over `restX`, the within-cell
|
|
2464
|
+
// fraction clamped to [0,1] as sampleWarpGrid clamps (s, t). A node reads
|
|
2465
|
+
// its own warped x: `warpedX[c] + Δ·1` is `warpedX[c + 1]` only to an
|
|
2466
|
+
// ulp, and the columns a bake reads are nodes.
|
|
2467
|
+
mapX: (x) => {
|
|
2468
|
+
const c = cellFor(x);
|
|
2469
|
+
const s = Math.max(
|
|
2470
|
+
0,
|
|
2471
|
+
Math.min(1, (x - restX[c]) / (restX[c + 1] - restX[c]))
|
|
2472
|
+
);
|
|
2473
|
+
if (s === 0) return warpedX[c];
|
|
2474
|
+
if (s === 1) return warpedX[c + 1];
|
|
2475
|
+
return warpedX[c] + (warpedX[c + 1] - warpedX[c]) * s;
|
|
2476
|
+
}
|
|
2230
2477
|
};
|
|
2231
2478
|
}
|
|
2232
2479
|
function renderedLandingX(part, vertexLanding, px, py) {
|
|
@@ -2332,7 +2579,7 @@ function plateLandingOn(plate, mapAt) {
|
|
|
2332
2579
|
y
|
|
2333
2580
|
);
|
|
2334
2581
|
}
|
|
2335
|
-
function plateGuardRowsFor(face, hairFront) {
|
|
2582
|
+
function plateGuardRowsFor(face, hairFront, widestY = -Infinity) {
|
|
2336
2583
|
const rowsOf = (part, y) => {
|
|
2337
2584
|
const { rows } = meshCellsFor(part.cropW, part.cropH);
|
|
2338
2585
|
return Array.from(
|
|
@@ -2347,7 +2594,7 @@ function plateGuardRowsFor(face, hairFront) {
|
|
|
2347
2594
|
return [
|
|
2348
2595
|
...faceRows,
|
|
2349
2596
|
...rowsOf(hairFront, hairFront.centerY).map(
|
|
2350
|
-
(y) => Math.max(bottom, Math.min(top, y))
|
|
2597
|
+
(y) => Math.max(bottom, Math.min(top, Math.max(y, widestY)))
|
|
2351
2598
|
)
|
|
2352
2599
|
];
|
|
2353
2600
|
}
|
|
@@ -2359,7 +2606,7 @@ function plateGuardsOf(carriers, faceHalfWidth, profile, hairFront) {
|
|
|
2359
2606
|
return {
|
|
2360
2607
|
plate,
|
|
2361
2608
|
edgeAt: profile === void 0 ? () => faceHalfWidth : profile.at,
|
|
2362
|
-
plateGuardRows: plateGuardRowsFor(plate.part, hairFront)
|
|
2609
|
+
plateGuardRows: plateGuardRowsFor(plate.part, hairFront, profile?.widestY)
|
|
2363
2610
|
};
|
|
2364
2611
|
}
|
|
2365
2612
|
var TurnTargetError = class extends Error {
|
|
@@ -2537,14 +2784,15 @@ var TURN_SWEEP_SAMPLES = 32;
|
|
|
2537
2784
|
var HOLD_CLEARANCE = 1;
|
|
2538
2785
|
var TURN_HOLD_SAMPLES = 16;
|
|
2539
2786
|
var TURN_SETTLE_PASSES = 24;
|
|
2540
|
-
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge) {
|
|
2787
|
+
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge, widestY = -Infinity) {
|
|
2541
2788
|
const local = x - faceCenterX;
|
|
2542
2789
|
const dist = Math.abs(local);
|
|
2543
2790
|
const side = Math.sign(local);
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2791
|
+
const row = Math.max(y, widestY);
|
|
2792
|
+
const painted = edgeAt(row);
|
|
2793
|
+
if (dist <= painted) return plateLandingAt(deg, x, row);
|
|
2546
2794
|
if (dist <= holdBase) {
|
|
2547
|
-
const inner = plateLandingAt(deg, faceCenterX + side * painted,
|
|
2795
|
+
const inner = plateLandingAt(deg, faceCenterX + side * painted, row);
|
|
2548
2796
|
const u = (dist - painted) / (holdBase - painted);
|
|
2549
2797
|
return inner + (faceCenterX + side * holdEdge - inner) * u;
|
|
2550
2798
|
}
|
|
@@ -2649,7 +2897,8 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2649
2897
|
plateLandingAt,
|
|
2650
2898
|
ctx.edgeAt,
|
|
2651
2899
|
hold.holdBase,
|
|
2652
|
-
hold.holdEdgeAt(deg)
|
|
2900
|
+
hold.holdEdgeAt(deg),
|
|
2901
|
+
ctx.profile?.widestY
|
|
2653
2902
|
),
|
|
2654
2903
|
leadShift
|
|
2655
2904
|
);
|
|
@@ -3624,7 +3873,7 @@ function plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg)
|
|
|
3624
3873
|
)
|
|
3625
3874
|
);
|
|
3626
3875
|
}
|
|
3627
|
-
function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows) {
|
|
3876
|
+
function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows, widestY = -Infinity) {
|
|
3628
3877
|
if (holdEdgeAt(0) !== holdBase) {
|
|
3629
3878
|
throw new Error(
|
|
3630
3879
|
`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`
|
|
@@ -3662,7 +3911,8 @@ function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandi
|
|
|
3662
3911
|
plateLandingAt,
|
|
3663
3912
|
edgeAt,
|
|
3664
3913
|
holdBase,
|
|
3665
|
-
holdEdge
|
|
3914
|
+
holdEdge,
|
|
3915
|
+
widestY
|
|
3666
3916
|
) - x,
|
|
3667
3917
|
0
|
|
3668
3918
|
);
|
|
@@ -3841,7 +4091,7 @@ function defaultPoseOf(bindings) {
|
|
|
3841
4091
|
return binding.from + (binding.to - binding.from) * t;
|
|
3842
4092
|
});
|
|
3843
4093
|
}
|
|
3844
|
-
function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
|
|
4094
|
+
function groupGridFor(members, cells, turnShiftPx, nodShiftPx, cellPx) {
|
|
3845
4095
|
let minX = Infinity;
|
|
3846
4096
|
let maxX = -Infinity;
|
|
3847
4097
|
let minY = Infinity;
|
|
@@ -3876,24 +4126,50 @@ function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
|
|
|
3876
4126
|
}
|
|
3877
4127
|
}
|
|
3878
4128
|
}
|
|
4129
|
+
const extentMinX = minX;
|
|
4130
|
+
const extentMaxX = maxX;
|
|
3879
4131
|
minX -= turnShiftPx;
|
|
3880
4132
|
maxX += turnShiftPx;
|
|
3881
4133
|
minY -= nodShiftPx;
|
|
3882
4134
|
maxY += nodShiftPx;
|
|
3883
4135
|
const marginX = (maxX - minX) * GRID_MARGIN + 1;
|
|
3884
4136
|
const marginY = (maxY - minY) * GRID_MARGIN + 1;
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
4137
|
+
const points = generateGridPoints(
|
|
4138
|
+
cells,
|
|
4139
|
+
cells,
|
|
4140
|
+
minX - marginX,
|
|
4141
|
+
maxX + marginX,
|
|
4142
|
+
minY - marginY,
|
|
4143
|
+
maxY + marginY
|
|
4144
|
+
);
|
|
4145
|
+
if (cellPx === void 0) return { cols: cells, rows: cells, points };
|
|
4146
|
+
const stride = cells + 1;
|
|
4147
|
+
const uniformXs = Array.from({ length: stride }, (_, c) => points[c * 2]);
|
|
4148
|
+
const ys = Array.from(
|
|
4149
|
+
{ length: stride },
|
|
4150
|
+
(_, r) => points[r * stride * 2 + 1]
|
|
4151
|
+
);
|
|
4152
|
+
const xs = [uniformXs[0]];
|
|
4153
|
+
for (let c = 0; c < cells; c++) {
|
|
4154
|
+
const left = uniformXs[c];
|
|
4155
|
+
const right = uniformXs[c + 1];
|
|
4156
|
+
const from = Math.max(left, extentMinX);
|
|
4157
|
+
const to = Math.min(right, extentMaxX);
|
|
4158
|
+
if (right - left > cellPx && from < to) {
|
|
4159
|
+
const steps = Math.ceil((to - from) / cellPx);
|
|
4160
|
+
if (from > left) xs.push(from);
|
|
4161
|
+
for (let k = 1; k < steps; k++) {
|
|
4162
|
+
xs.push(from + k * (to - from) / steps);
|
|
4163
|
+
}
|
|
4164
|
+
if (to < right) xs.push(to);
|
|
4165
|
+
}
|
|
4166
|
+
xs.push(right);
|
|
4167
|
+
}
|
|
4168
|
+
const merged = [];
|
|
4169
|
+
for (const y of ys) {
|
|
4170
|
+
for (const x of xs) merged.push(x, y);
|
|
4171
|
+
}
|
|
4172
|
+
return { cols: xs.length - 1, rows: cells, points: merged };
|
|
3897
4173
|
}
|
|
3898
4174
|
function eyelidFoldFor(layers, layer, mesh, t) {
|
|
3899
4175
|
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
@@ -3994,7 +4270,8 @@ function turnSetup(layers) {
|
|
|
3994
4270
|
own,
|
|
3995
4271
|
isPlate ? FACE_PLATE_CELLS : FEATURE_GRID_CELLS,
|
|
3996
4272
|
isPlate ? 0 : turnShift[turnFamily(role)],
|
|
3997
|
-
nodShiftPx
|
|
4273
|
+
nodShiftPx,
|
|
4274
|
+
group === "mouthWarp" ? MOUTH_GRID_CELL_PX : void 0
|
|
3998
4275
|
)
|
|
3999
4276
|
);
|
|
4000
4277
|
}
|
|
@@ -4334,7 +4611,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4334
4611
|
edgeAt,
|
|
4335
4612
|
holdBase,
|
|
4336
4613
|
turn?.holdEdgeAt ?? (() => holdBase),
|
|
4337
|
-
plateGuardRows
|
|
4614
|
+
plateGuardRows,
|
|
4615
|
+
profile?.widestY
|
|
4338
4616
|
)
|
|
4339
4617
|
);
|
|
4340
4618
|
ownWarps.push(
|
|
@@ -4424,6 +4702,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4424
4702
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4425
4703
|
0 && (module.exports = {
|
|
4426
4704
|
ALPHA_BBOX_THRESHOLD,
|
|
4705
|
+
ALPHA_OPAQUE,
|
|
4427
4706
|
ATLAS_PADDING,
|
|
4428
4707
|
AddDeformer,
|
|
4429
4708
|
AddPart,
|
|
@@ -4434,6 +4713,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4434
4713
|
DeletePart,
|
|
4435
4714
|
DeletePhysicsRig,
|
|
4436
4715
|
EditorDocument,
|
|
4716
|
+
HEAD_BAND,
|
|
4717
|
+
SPECK_CORE_FRACTION,
|
|
4437
4718
|
SetDeformerBindings,
|
|
4438
4719
|
SetDeformerParent,
|
|
4439
4720
|
SetDeformerPivot,
|
|
@@ -4457,9 +4738,14 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4457
4738
|
createDefaultPart,
|
|
4458
4739
|
createDefaultWarpDeformer,
|
|
4459
4740
|
createGridMesh,
|
|
4741
|
+
createLayerSetMeasurer,
|
|
4742
|
+
denseCoreOf,
|
|
4460
4743
|
detectAlphaBbox,
|
|
4744
|
+
foregroundSpan,
|
|
4461
4745
|
generateIkiFromLayerSet,
|
|
4746
|
+
headHalfOf,
|
|
4462
4747
|
interpolateGridOffsets,
|
|
4748
|
+
isSpeckCore,
|
|
4463
4749
|
packAtlas,
|
|
4464
4750
|
parseLayerRoles,
|
|
4465
4751
|
upsertGridKeyform,
|