@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.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") {
|
|
@@ -4458,6 +4702,7 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4458
4702
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4459
4703
|
0 && (module.exports = {
|
|
4460
4704
|
ALPHA_BBOX_THRESHOLD,
|
|
4705
|
+
ALPHA_OPAQUE,
|
|
4461
4706
|
ATLAS_PADDING,
|
|
4462
4707
|
AddDeformer,
|
|
4463
4708
|
AddPart,
|
|
@@ -4468,6 +4713,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4468
4713
|
DeletePart,
|
|
4469
4714
|
DeletePhysicsRig,
|
|
4470
4715
|
EditorDocument,
|
|
4716
|
+
HEAD_BAND,
|
|
4717
|
+
SPECK_CORE_FRACTION,
|
|
4471
4718
|
SetDeformerBindings,
|
|
4472
4719
|
SetDeformerParent,
|
|
4473
4720
|
SetDeformerPivot,
|
|
@@ -4491,9 +4738,14 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4491
4738
|
createDefaultPart,
|
|
4492
4739
|
createDefaultWarpDeformer,
|
|
4493
4740
|
createGridMesh,
|
|
4741
|
+
createLayerSetMeasurer,
|
|
4742
|
+
denseCoreOf,
|
|
4494
4743
|
detectAlphaBbox,
|
|
4744
|
+
foregroundSpan,
|
|
4495
4745
|
generateIkiFromLayerSet,
|
|
4746
|
+
headHalfOf,
|
|
4496
4747
|
interpolateGridOffsets,
|
|
4748
|
+
isSpeckCore,
|
|
4497
4749
|
packAtlas,
|
|
4498
4750
|
parseLayerRoles,
|
|
4499
4751
|
upsertGridKeyform,
|