@ikijs/editor 0.8.1 → 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 +393 -193
- package/dist/index.d.ts +393 -193
- package/dist/index.js +252 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +244 -0
- 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") {
|
|
@@ -4396,6 +4632,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4396
4632
|
}
|
|
4397
4633
|
export {
|
|
4398
4634
|
ALPHA_BBOX_THRESHOLD,
|
|
4635
|
+
ALPHA_OPAQUE,
|
|
4399
4636
|
ATLAS_PADDING,
|
|
4400
4637
|
AddDeformer,
|
|
4401
4638
|
AddPart,
|
|
@@ -4406,6 +4643,8 @@ export {
|
|
|
4406
4643
|
DeletePart,
|
|
4407
4644
|
DeletePhysicsRig,
|
|
4408
4645
|
EditorDocument,
|
|
4646
|
+
HEAD_BAND,
|
|
4647
|
+
SPECK_CORE_FRACTION,
|
|
4409
4648
|
SetDeformerBindings,
|
|
4410
4649
|
SetDeformerParent,
|
|
4411
4650
|
SetDeformerPivot,
|
|
@@ -4429,9 +4668,14 @@ export {
|
|
|
4429
4668
|
createDefaultPart,
|
|
4430
4669
|
createDefaultWarpDeformer,
|
|
4431
4670
|
createGridMesh,
|
|
4671
|
+
createLayerSetMeasurer,
|
|
4672
|
+
denseCoreOf,
|
|
4432
4673
|
detectAlphaBbox,
|
|
4674
|
+
foregroundSpan,
|
|
4433
4675
|
generateIkiFromLayerSet,
|
|
4676
|
+
headHalfOf,
|
|
4434
4677
|
interpolateGridOffsets,
|
|
4678
|
+
isSpeckCore,
|
|
4435
4679
|
packAtlas,
|
|
4436
4680
|
parseLayerRoles,
|
|
4437
4681
|
upsertGridKeyform,
|