@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.mjs
CHANGED
|
@@ -1420,6 +1420,242 @@ function detectAlphaBbox(rgba, width, height) {
|
|
|
1420
1420
|
return { x, y, w: x2 - x + 1, h: y2 - y + 1 };
|
|
1421
1421
|
}
|
|
1422
1422
|
|
|
1423
|
+
// src/layer-measure.ts
|
|
1424
|
+
var HEAD_BAND = 10;
|
|
1425
|
+
var ALPHA_OPAQUE = 128;
|
|
1426
|
+
function denseCoreOf(rgba, width, height) {
|
|
1427
|
+
let minX = width;
|
|
1428
|
+
let minY = height;
|
|
1429
|
+
let maxX = -1;
|
|
1430
|
+
let maxY = -1;
|
|
1431
|
+
for (let y = 0; y < height; y++) {
|
|
1432
|
+
for (let x = 0; x < width; x++) {
|
|
1433
|
+
if (rgba[(y * width + x) * 4 + 3] >= ALPHA_OPAQUE) {
|
|
1434
|
+
if (x < minX) minX = x;
|
|
1435
|
+
if (x > maxX) maxX = x;
|
|
1436
|
+
if (y < minY) minY = y;
|
|
1437
|
+
if (y > maxY) maxY = y;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (maxX < 0) return null;
|
|
1442
|
+
return { x: minX, y: minY, w: maxX - minX + 1, h: maxY - minY + 1 };
|
|
1443
|
+
}
|
|
1444
|
+
var SPECK_CORE_FRACTION = 0.25;
|
|
1445
|
+
function isSpeckCore(core, part) {
|
|
1446
|
+
return core.w < SPECK_CORE_FRACTION * part.w || core.h < SPECK_CORE_FRACTION * part.h;
|
|
1447
|
+
}
|
|
1448
|
+
function foregroundSpan(mask, width, height, rowLo, rowHi) {
|
|
1449
|
+
let left = width;
|
|
1450
|
+
let right = -1;
|
|
1451
|
+
for (let y = Math.max(0, rowLo); y <= Math.min(height - 1, rowHi); y++) {
|
|
1452
|
+
for (let x = 0; x < width; x++) {
|
|
1453
|
+
if (!mask[y * width + x]) continue;
|
|
1454
|
+
if (x < left) left = x;
|
|
1455
|
+
if (x > right) right = x;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return { left, right };
|
|
1459
|
+
}
|
|
1460
|
+
function headHalfOf(span) {
|
|
1461
|
+
return (span.right - span.left + 1) / 2;
|
|
1462
|
+
}
|
|
1463
|
+
function eyeBandRowOf(layers) {
|
|
1464
|
+
const centreY = (role) => {
|
|
1465
|
+
const layer = layers.find((l) => l.role === role);
|
|
1466
|
+
return layer && layer.bbox.y + layer.bbox.h / 2;
|
|
1467
|
+
};
|
|
1468
|
+
const pairRow = (left, right) => {
|
|
1469
|
+
const a = centreY(left);
|
|
1470
|
+
const b = centreY(right);
|
|
1471
|
+
return a === void 0 || b === void 0 ? void 0 : (a + b) / 2;
|
|
1472
|
+
};
|
|
1473
|
+
const row = pairRow("iris_L", "iris_R") ?? pairRow("eye_L", "eye_R");
|
|
1474
|
+
if (row === void 0) {
|
|
1475
|
+
throw new Error("auto-rig: no eye pair to measure the head width at");
|
|
1476
|
+
}
|
|
1477
|
+
return Math.round(row);
|
|
1478
|
+
}
|
|
1479
|
+
function foregroundSpanOfRows(rowLeft, rowRight, rowLo, rowHi) {
|
|
1480
|
+
let left = Infinity;
|
|
1481
|
+
let right = -Infinity;
|
|
1482
|
+
for (let y = Math.max(0, rowLo); y <= Math.min(rowLeft.length - 1, rowHi); y++) {
|
|
1483
|
+
if (rowLeft[y] < left) left = rowLeft[y];
|
|
1484
|
+
if (rowRight[y] > right) right = rowRight[y];
|
|
1485
|
+
}
|
|
1486
|
+
return { left, right };
|
|
1487
|
+
}
|
|
1488
|
+
function facePlateHalfOf(layers) {
|
|
1489
|
+
const face = layers.find((l) => l.role === "face");
|
|
1490
|
+
if (face === void 0) {
|
|
1491
|
+
throw new Error("auto-rig: no face layer to measure the plate against");
|
|
1492
|
+
}
|
|
1493
|
+
return face.cropW / 2;
|
|
1494
|
+
}
|
|
1495
|
+
var STRAND_MIN_RUN_FRACTION = 0.5;
|
|
1496
|
+
function createLayerSetMeasurer(canvas) {
|
|
1497
|
+
for (const field of ["width", "height"]) {
|
|
1498
|
+
const value = canvas[field];
|
|
1499
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1500
|
+
throw new Error(
|
|
1501
|
+
`auto-rig: createLayerSetMeasurer: canvas.${field} must be a positive safe integer, got ${value}`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
const { width: canvasW, height: canvasH } = canvas;
|
|
1506
|
+
const rgbaLength = canvasW * canvasH * 4;
|
|
1507
|
+
if (!Number.isSafeInteger(rgbaLength)) {
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
`auto-rig: createLayerSetMeasurer: canvas ${canvasW}x${canvasH} is too large: width \xD7 height \xD7 4 = ${rgbaLength} is not a safe integer`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
const opaque = new Uint8Array(canvasW * canvasH);
|
|
1513
|
+
const layerInputs = [];
|
|
1514
|
+
const rowSpansByRole = [];
|
|
1515
|
+
let hairFrontMask;
|
|
1516
|
+
function add({ role, fileName, rgba }) {
|
|
1517
|
+
if (rgba.length !== rgbaLength) {
|
|
1518
|
+
throw new Error(
|
|
1519
|
+
`auto-rig: layer "${fileName}": rgba length ${rgba.length} is not ${canvasW}x${canvasH}x4 = ${rgbaLength}`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
const bbox = detectAlphaBbox(rgba, canvasW, canvasH);
|
|
1523
|
+
if (bbox === null) return null;
|
|
1524
|
+
const rowLeft = new Int32Array(canvasH).fill(canvasW);
|
|
1525
|
+
const rowRight = new Int32Array(canvasH).fill(-1);
|
|
1526
|
+
const ownMask = role === "hair_front" ? new Uint8Array(canvasW * canvasH) : void 0;
|
|
1527
|
+
for (let y = 0; y < canvasH; y++) {
|
|
1528
|
+
for (let x = 0; x < canvasW; x++) {
|
|
1529
|
+
const p = y * canvasW + x;
|
|
1530
|
+
if (rgba[p * 4 + 3] < ALPHA_OPAQUE) continue;
|
|
1531
|
+
opaque[p] = 1;
|
|
1532
|
+
if (ownMask !== void 0) ownMask[p] = 1;
|
|
1533
|
+
if (x < rowLeft[y]) rowLeft[y] = x;
|
|
1534
|
+
if (x > rowRight[y]) rowRight[y] = x;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
rowSpansByRole.push({ role, rowLeft, rowRight });
|
|
1538
|
+
if (ownMask !== void 0) hairFrontMask = ownMask;
|
|
1539
|
+
const layer = {
|
|
1540
|
+
role,
|
|
1541
|
+
fileName,
|
|
1542
|
+
canvasW,
|
|
1543
|
+
canvasH,
|
|
1544
|
+
bbox,
|
|
1545
|
+
cropW: bbox.w,
|
|
1546
|
+
cropH: bbox.h
|
|
1547
|
+
};
|
|
1548
|
+
if (role === "face") {
|
|
1549
|
+
const rowHalfWidths = [];
|
|
1550
|
+
for (let y = bbox.y; y < bbox.y + bbox.h; y++) {
|
|
1551
|
+
rowHalfWidths.push(
|
|
1552
|
+
rowRight[y] >= rowLeft[y] ? headHalfOf({ left: rowLeft[y], right: rowRight[y] }) : 0
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
layer.rowHalfWidths = rowHalfWidths;
|
|
1556
|
+
} else if (role === "nose") {
|
|
1557
|
+
const core = denseCoreOf(rgba, canvasW, canvasH);
|
|
1558
|
+
if (core !== null && !isSpeckCore(core, bbox)) layer.denseCore = core;
|
|
1559
|
+
}
|
|
1560
|
+
layerInputs.push(layer);
|
|
1561
|
+
return layer;
|
|
1562
|
+
}
|
|
1563
|
+
function finish() {
|
|
1564
|
+
const eyeRow = eyeBandRowOf(layerInputs);
|
|
1565
|
+
const span = foregroundSpan(
|
|
1566
|
+
opaque,
|
|
1567
|
+
canvasW,
|
|
1568
|
+
canvasH,
|
|
1569
|
+
eyeRow - HEAD_BAND,
|
|
1570
|
+
eyeRow + HEAD_BAND
|
|
1571
|
+
);
|
|
1572
|
+
const headHalfWidth = span.right > span.left ? headHalfOf(span) : void 0;
|
|
1573
|
+
const headHalfWidthApplied = headHalfWidth !== void 0 && headHalfWidth > facePlateHalfOf(layerInputs);
|
|
1574
|
+
let headEdges;
|
|
1575
|
+
if (headHalfWidthApplied) {
|
|
1576
|
+
const bandLo = eyeRow - HEAD_BAND;
|
|
1577
|
+
const bandHi = eyeRow + HEAD_BAND;
|
|
1578
|
+
const left = [];
|
|
1579
|
+
const right = [];
|
|
1580
|
+
for (const { role, rowLeft, rowRight } of rowSpansByRole) {
|
|
1581
|
+
const own = foregroundSpanOfRows(rowLeft, rowRight, bandLo, bandHi);
|
|
1582
|
+
if (own.right <= own.left) continue;
|
|
1583
|
+
left.push({ role, x: own.left - canvasW / 2 });
|
|
1584
|
+
right.push({ role, x: own.right - canvasW / 2 });
|
|
1585
|
+
}
|
|
1586
|
+
headEdges = { left, right };
|
|
1587
|
+
}
|
|
1588
|
+
let strandEdges;
|
|
1589
|
+
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));
|
|
1590
|
+
if (hairFrontMask !== void 0 && irisLayers.length === 2) {
|
|
1591
|
+
const mask = hairFrontMask;
|
|
1592
|
+
const half = canvasW / 2;
|
|
1593
|
+
const strandOn = (iris, other, side) => {
|
|
1594
|
+
const r = Math.floor(iris.bbox.y + iris.bbox.h / 2);
|
|
1595
|
+
const centre = iris.bbox.x + iris.bbox.w / 2;
|
|
1596
|
+
const otherX = other.bbox.x + other.bbox.w / 2 - half;
|
|
1597
|
+
const c = side < 0 ? Math.ceil(centre) - 1 : Math.floor(centre);
|
|
1598
|
+
const span2 = rowSpansByRole.find((s) => s.role === iris.role);
|
|
1599
|
+
if (span2.rowRight[r] < span2.rowLeft[r]) return void 0;
|
|
1600
|
+
const minRun = STRAND_MIN_RUN_FRACTION * (span2.rowRight[r] + 1 - span2.rowLeft[r]);
|
|
1601
|
+
const strand = new Uint8Array(canvasW);
|
|
1602
|
+
for (let a = 0; a < canvasW; ) {
|
|
1603
|
+
if (mask[r * canvasW + a] !== 1) {
|
|
1604
|
+
a++;
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
let b = a;
|
|
1608
|
+
while (b < canvasW && mask[r * canvasW + b] === 1) b++;
|
|
1609
|
+
if (b - a >= minRun) strand.fill(1, a, b);
|
|
1610
|
+
a = b;
|
|
1611
|
+
}
|
|
1612
|
+
const strandAt = (x2) => x2 >= 0 && x2 < canvasW && strand[x2] === 1;
|
|
1613
|
+
const outerBoundary = (x2) => (side < 0 ? x2 : x2 + 1) - half;
|
|
1614
|
+
let x = c;
|
|
1615
|
+
let runFace;
|
|
1616
|
+
if (!strandAt(c)) {
|
|
1617
|
+
while (x >= 0 && x < canvasW && !strandAt(x)) x += side;
|
|
1618
|
+
if (x < 0 || x >= canvasW) return void 0;
|
|
1619
|
+
runFace = outerBoundary(x - side);
|
|
1620
|
+
} else {
|
|
1621
|
+
let f = c - side;
|
|
1622
|
+
while (strandAt(f)) f -= side;
|
|
1623
|
+
const face = outerBoundary(f);
|
|
1624
|
+
runFace = side * (face - otherX) > 0 ? face : null;
|
|
1625
|
+
}
|
|
1626
|
+
while (strandAt(x)) x += side;
|
|
1627
|
+
return {
|
|
1628
|
+
y: canvasH / 2 - (r + 0.5),
|
|
1629
|
+
irisOuter: (side < 0 ? span2.rowLeft[r] : span2.rowRight[r] + 1) - half,
|
|
1630
|
+
irisInner: (side < 0 ? span2.rowRight[r] + 1 : span2.rowLeft[r]) - half,
|
|
1631
|
+
runOuter: outerBoundary(x - side),
|
|
1632
|
+
runFace
|
|
1633
|
+
};
|
|
1634
|
+
};
|
|
1635
|
+
const [low, high] = irisLayers;
|
|
1636
|
+
const left = strandOn(low, high, -1);
|
|
1637
|
+
const right = strandOn(high, low, 1);
|
|
1638
|
+
if (left !== void 0 || right !== void 0) {
|
|
1639
|
+
strandEdges = {
|
|
1640
|
+
...left === void 0 ? {} : { left },
|
|
1641
|
+
...right === void 0 ? {} : { right }
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
layers: layerInputs,
|
|
1647
|
+
...headHalfWidth === void 0 ? {} : { headHalfWidth },
|
|
1648
|
+
headHalfWidthApplied,
|
|
1649
|
+
turnOptions: {
|
|
1650
|
+
...headHalfWidthApplied ? { turnTargets: { headHalfWidth } } : {},
|
|
1651
|
+
...headEdges === void 0 ? {} : { headEdges },
|
|
1652
|
+
...strandEdges === void 0 ? {} : { strandEdges }
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
return { add, finish };
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1423
1659
|
// src/binding-capture.ts
|
|
1424
1660
|
function captureBindingEndpoint(channel, restValue, posedValue) {
|
|
1425
1661
|
if (channel === "opacity") {
|
|
@@ -1991,6 +2227,7 @@ function meshCellsFor(w, h) {
|
|
|
1991
2227
|
}
|
|
1992
2228
|
var FACE_PLATE_CELLS = 10;
|
|
1993
2229
|
var FEATURE_GRID_CELLS = 4;
|
|
2230
|
+
var MOUTH_GRID_CELL_PX = 32;
|
|
1994
2231
|
var TURN_LATTICE_CELL_PX = 16;
|
|
1995
2232
|
var GRID_MARGIN = 0.12;
|
|
1996
2233
|
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
@@ -2141,31 +2378,33 @@ function turnColumnMap(grid, faceCenterX, radiusX, angleX, travel) {
|
|
|
2141
2378
|
x + boundedCylinderBend(x - faceCenterX, radiusX, theta) + slide
|
|
2142
2379
|
);
|
|
2143
2380
|
}
|
|
2144
|
-
const cellFor = (
|
|
2381
|
+
const cellFor = (x) => {
|
|
2145
2382
|
let lo = 0;
|
|
2146
2383
|
let hi = grid.cols - 1;
|
|
2147
2384
|
while (lo < hi) {
|
|
2148
2385
|
const mid = lo + hi >> 1;
|
|
2149
|
-
if (
|
|
2386
|
+
if (x < restX[mid + 1]) hi = mid;
|
|
2150
2387
|
else lo = mid + 1;
|
|
2151
2388
|
}
|
|
2152
2389
|
return lo;
|
|
2153
2390
|
};
|
|
2154
|
-
const sample = (edges, values, v) => {
|
|
2155
|
-
const c = cellFor(v, edges);
|
|
2156
|
-
const s = Math.max(
|
|
2157
|
-
0,
|
|
2158
|
-
Math.min(1, (v - edges[c]) / (edges[c + 1] - edges[c]))
|
|
2159
|
-
);
|
|
2160
|
-
if (s === 0) return values[c];
|
|
2161
|
-
if (s === 1) return values[c + 1];
|
|
2162
|
-
return values[c] + (values[c + 1] - values[c]) * s;
|
|
2163
|
-
};
|
|
2164
2391
|
return {
|
|
2165
2392
|
restX,
|
|
2166
2393
|
warpedX,
|
|
2167
|
-
|
|
2168
|
-
|
|
2394
|
+
// Piecewise-linear read of `warpedX` over `restX`, the within-cell
|
|
2395
|
+
// fraction clamped to [0,1] as sampleWarpGrid clamps (s, t). A node reads
|
|
2396
|
+
// its own warped x: `warpedX[c] + Δ·1` is `warpedX[c + 1]` only to an
|
|
2397
|
+
// ulp, and the columns a bake reads are nodes.
|
|
2398
|
+
mapX: (x) => {
|
|
2399
|
+
const c = cellFor(x);
|
|
2400
|
+
const s = Math.max(
|
|
2401
|
+
0,
|
|
2402
|
+
Math.min(1, (x - restX[c]) / (restX[c + 1] - restX[c]))
|
|
2403
|
+
);
|
|
2404
|
+
if (s === 0) return warpedX[c];
|
|
2405
|
+
if (s === 1) return warpedX[c + 1];
|
|
2406
|
+
return warpedX[c] + (warpedX[c + 1] - warpedX[c]) * s;
|
|
2407
|
+
}
|
|
2169
2408
|
};
|
|
2170
2409
|
}
|
|
2171
2410
|
function renderedLandingX(part, vertexLanding, px, py) {
|
|
@@ -2271,7 +2510,7 @@ function plateLandingOn(plate, mapAt) {
|
|
|
2271
2510
|
y
|
|
2272
2511
|
);
|
|
2273
2512
|
}
|
|
2274
|
-
function plateGuardRowsFor(face, hairFront) {
|
|
2513
|
+
function plateGuardRowsFor(face, hairFront, widestY = -Infinity) {
|
|
2275
2514
|
const rowsOf = (part, y) => {
|
|
2276
2515
|
const { rows } = meshCellsFor(part.cropW, part.cropH);
|
|
2277
2516
|
return Array.from(
|
|
@@ -2286,7 +2525,7 @@ function plateGuardRowsFor(face, hairFront) {
|
|
|
2286
2525
|
return [
|
|
2287
2526
|
...faceRows,
|
|
2288
2527
|
...rowsOf(hairFront, hairFront.centerY).map(
|
|
2289
|
-
(y) => Math.max(bottom, Math.min(top, y))
|
|
2528
|
+
(y) => Math.max(bottom, Math.min(top, Math.max(y, widestY)))
|
|
2290
2529
|
)
|
|
2291
2530
|
];
|
|
2292
2531
|
}
|
|
@@ -2298,7 +2537,7 @@ function plateGuardsOf(carriers, faceHalfWidth, profile, hairFront) {
|
|
|
2298
2537
|
return {
|
|
2299
2538
|
plate,
|
|
2300
2539
|
edgeAt: profile === void 0 ? () => faceHalfWidth : profile.at,
|
|
2301
|
-
plateGuardRows: plateGuardRowsFor(plate.part, hairFront)
|
|
2540
|
+
plateGuardRows: plateGuardRowsFor(plate.part, hairFront, profile?.widestY)
|
|
2302
2541
|
};
|
|
2303
2542
|
}
|
|
2304
2543
|
var TurnTargetError = class extends Error {
|
|
@@ -2476,14 +2715,15 @@ var TURN_SWEEP_SAMPLES = 32;
|
|
|
2476
2715
|
var HOLD_CLEARANCE = 1;
|
|
2477
2716
|
var TURN_HOLD_SAMPLES = 16;
|
|
2478
2717
|
var TURN_SETTLE_PASSES = 24;
|
|
2479
|
-
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge) {
|
|
2718
|
+
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge, widestY = -Infinity) {
|
|
2480
2719
|
const local = x - faceCenterX;
|
|
2481
2720
|
const dist = Math.abs(local);
|
|
2482
2721
|
const side = Math.sign(local);
|
|
2483
|
-
const
|
|
2484
|
-
|
|
2722
|
+
const row = Math.max(y, widestY);
|
|
2723
|
+
const painted = edgeAt(row);
|
|
2724
|
+
if (dist <= painted) return plateLandingAt(deg, x, row);
|
|
2485
2725
|
if (dist <= holdBase) {
|
|
2486
|
-
const inner = plateLandingAt(deg, faceCenterX + side * painted,
|
|
2726
|
+
const inner = plateLandingAt(deg, faceCenterX + side * painted, row);
|
|
2487
2727
|
const u = (dist - painted) / (holdBase - painted);
|
|
2488
2728
|
return inner + (faceCenterX + side * holdEdge - inner) * u;
|
|
2489
2729
|
}
|
|
@@ -2588,7 +2828,8 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2588
2828
|
plateLandingAt,
|
|
2589
2829
|
ctx.edgeAt,
|
|
2590
2830
|
hold.holdBase,
|
|
2591
|
-
hold.holdEdgeAt(deg)
|
|
2831
|
+
hold.holdEdgeAt(deg),
|
|
2832
|
+
ctx.profile?.widestY
|
|
2592
2833
|
),
|
|
2593
2834
|
leadShift
|
|
2594
2835
|
);
|
|
@@ -3563,7 +3804,7 @@ function plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg)
|
|
|
3563
3804
|
)
|
|
3564
3805
|
);
|
|
3565
3806
|
}
|
|
3566
|
-
function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows) {
|
|
3807
|
+
function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows, widestY = -Infinity) {
|
|
3567
3808
|
if (holdEdgeAt(0) !== holdBase) {
|
|
3568
3809
|
throw new Error(
|
|
3569
3810
|
`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`
|
|
@@ -3601,7 +3842,8 @@ function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandi
|
|
|
3601
3842
|
plateLandingAt,
|
|
3602
3843
|
edgeAt,
|
|
3603
3844
|
holdBase,
|
|
3604
|
-
holdEdge
|
|
3845
|
+
holdEdge,
|
|
3846
|
+
widestY
|
|
3605
3847
|
) - x,
|
|
3606
3848
|
0
|
|
3607
3849
|
);
|
|
@@ -3780,7 +4022,7 @@ function defaultPoseOf(bindings) {
|
|
|
3780
4022
|
return binding.from + (binding.to - binding.from) * t;
|
|
3781
4023
|
});
|
|
3782
4024
|
}
|
|
3783
|
-
function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
|
|
4025
|
+
function groupGridFor(members, cells, turnShiftPx, nodShiftPx, cellPx) {
|
|
3784
4026
|
let minX = Infinity;
|
|
3785
4027
|
let maxX = -Infinity;
|
|
3786
4028
|
let minY = Infinity;
|
|
@@ -3815,24 +4057,50 @@ function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
|
|
|
3815
4057
|
}
|
|
3816
4058
|
}
|
|
3817
4059
|
}
|
|
4060
|
+
const extentMinX = minX;
|
|
4061
|
+
const extentMaxX = maxX;
|
|
3818
4062
|
minX -= turnShiftPx;
|
|
3819
4063
|
maxX += turnShiftPx;
|
|
3820
4064
|
minY -= nodShiftPx;
|
|
3821
4065
|
maxY += nodShiftPx;
|
|
3822
4066
|
const marginX = (maxX - minX) * GRID_MARGIN + 1;
|
|
3823
4067
|
const marginY = (maxY - minY) * GRID_MARGIN + 1;
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
4068
|
+
const points = generateGridPoints(
|
|
4069
|
+
cells,
|
|
4070
|
+
cells,
|
|
4071
|
+
minX - marginX,
|
|
4072
|
+
maxX + marginX,
|
|
4073
|
+
minY - marginY,
|
|
4074
|
+
maxY + marginY
|
|
4075
|
+
);
|
|
4076
|
+
if (cellPx === void 0) return { cols: cells, rows: cells, points };
|
|
4077
|
+
const stride = cells + 1;
|
|
4078
|
+
const uniformXs = Array.from({ length: stride }, (_, c) => points[c * 2]);
|
|
4079
|
+
const ys = Array.from(
|
|
4080
|
+
{ length: stride },
|
|
4081
|
+
(_, r) => points[r * stride * 2 + 1]
|
|
4082
|
+
);
|
|
4083
|
+
const xs = [uniformXs[0]];
|
|
4084
|
+
for (let c = 0; c < cells; c++) {
|
|
4085
|
+
const left = uniformXs[c];
|
|
4086
|
+
const right = uniformXs[c + 1];
|
|
4087
|
+
const from = Math.max(left, extentMinX);
|
|
4088
|
+
const to = Math.min(right, extentMaxX);
|
|
4089
|
+
if (right - left > cellPx && from < to) {
|
|
4090
|
+
const steps = Math.ceil((to - from) / cellPx);
|
|
4091
|
+
if (from > left) xs.push(from);
|
|
4092
|
+
for (let k = 1; k < steps; k++) {
|
|
4093
|
+
xs.push(from + k * (to - from) / steps);
|
|
4094
|
+
}
|
|
4095
|
+
if (to < right) xs.push(to);
|
|
4096
|
+
}
|
|
4097
|
+
xs.push(right);
|
|
4098
|
+
}
|
|
4099
|
+
const merged = [];
|
|
4100
|
+
for (const y of ys) {
|
|
4101
|
+
for (const x of xs) merged.push(x, y);
|
|
4102
|
+
}
|
|
4103
|
+
return { cols: xs.length - 1, rows: cells, points: merged };
|
|
3836
4104
|
}
|
|
3837
4105
|
function eyelidFoldFor(layers, layer, mesh, t) {
|
|
3838
4106
|
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
@@ -3933,7 +4201,8 @@ function turnSetup(layers) {
|
|
|
3933
4201
|
own,
|
|
3934
4202
|
isPlate ? FACE_PLATE_CELLS : FEATURE_GRID_CELLS,
|
|
3935
4203
|
isPlate ? 0 : turnShift[turnFamily(role)],
|
|
3936
|
-
nodShiftPx
|
|
4204
|
+
nodShiftPx,
|
|
4205
|
+
group === "mouthWarp" ? MOUTH_GRID_CELL_PX : void 0
|
|
3937
4206
|
)
|
|
3938
4207
|
);
|
|
3939
4208
|
}
|
|
@@ -4273,7 +4542,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4273
4542
|
edgeAt,
|
|
4274
4543
|
holdBase,
|
|
4275
4544
|
turn?.holdEdgeAt ?? (() => holdBase),
|
|
4276
|
-
plateGuardRows
|
|
4545
|
+
plateGuardRows,
|
|
4546
|
+
profile?.widestY
|
|
4277
4547
|
)
|
|
4278
4548
|
);
|
|
4279
4549
|
ownWarps.push(
|
|
@@ -4362,6 +4632,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4362
4632
|
}
|
|
4363
4633
|
export {
|
|
4364
4634
|
ALPHA_BBOX_THRESHOLD,
|
|
4635
|
+
ALPHA_OPAQUE,
|
|
4365
4636
|
ATLAS_PADDING,
|
|
4366
4637
|
AddDeformer,
|
|
4367
4638
|
AddPart,
|
|
@@ -4372,6 +4643,8 @@ export {
|
|
|
4372
4643
|
DeletePart,
|
|
4373
4644
|
DeletePhysicsRig,
|
|
4374
4645
|
EditorDocument,
|
|
4646
|
+
HEAD_BAND,
|
|
4647
|
+
SPECK_CORE_FRACTION,
|
|
4375
4648
|
SetDeformerBindings,
|
|
4376
4649
|
SetDeformerParent,
|
|
4377
4650
|
SetDeformerPivot,
|
|
@@ -4395,9 +4668,14 @@ export {
|
|
|
4395
4668
|
createDefaultPart,
|
|
4396
4669
|
createDefaultWarpDeformer,
|
|
4397
4670
|
createGridMesh,
|
|
4671
|
+
createLayerSetMeasurer,
|
|
4672
|
+
denseCoreOf,
|
|
4398
4673
|
detectAlphaBbox,
|
|
4674
|
+
foregroundSpan,
|
|
4399
4675
|
generateIkiFromLayerSet,
|
|
4676
|
+
headHalfOf,
|
|
4400
4677
|
interpolateGridOffsets,
|
|
4678
|
+
isSpeckCore,
|
|
4401
4679
|
packAtlas,
|
|
4402
4680
|
parseLayerRoles,
|
|
4403
4681
|
upsertGridKeyform,
|