@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/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,