@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/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 = (v, edges) => {
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 (v < edges[mid + 1]) hi = mid;
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
- mapX: (x) => sample(restX, warpedX, x),
2229
- invertX: (X) => sample(warpedX, restX, X)
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 painted = edgeAt(y);
2545
- if (dist <= painted) return plateLandingAt(deg, x, y);
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, y);
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
- return {
3886
- cols: cells,
3887
- rows: cells,
3888
- points: generateGridPoints(
3889
- cells,
3890
- cells,
3891
- minX - marginX,
3892
- maxX + marginX,
3893
- minY - marginY,
3894
- maxY + marginY
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,