@ikijs/editor 0.7.0 → 0.8.1

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.mjs CHANGED
@@ -1776,6 +1776,45 @@ function validateLayerInputs(layers, canvas) {
1776
1776
  }
1777
1777
  }
1778
1778
  }
1779
+ if (layer.denseCore !== void 0) {
1780
+ const where = `auto-rig: validateLayerInputs: role "${role}" denseCore`;
1781
+ const core = layer.denseCore;
1782
+ if (typeof core !== "object" || core === null || Array.isArray(core)) {
1783
+ throw new Error(`${where} must be a plain object { x, y, w, h }`);
1784
+ }
1785
+ for (const field of ["x", "y", "w", "h"]) {
1786
+ const v = core[field];
1787
+ const size = field === "w" || field === "h";
1788
+ if (typeof v !== "number" || !Number.isFinite(v) || size && v <= 0) {
1789
+ throw new Error(
1790
+ `${where}.${field} is ${String(v)}, not a finite number${size ? " > 0" : ""}`
1791
+ );
1792
+ }
1793
+ }
1794
+ const { x, y, w, h } = layer.denseCore;
1795
+ const right = bbox.x + bbox.w;
1796
+ const bottom = bbox.y + bbox.h;
1797
+ if (x < bbox.x) {
1798
+ throw new Error(
1799
+ `${where}.x (${x}) lies left of the bbox's left edge (${bbox.x})`
1800
+ );
1801
+ }
1802
+ if (y < bbox.y) {
1803
+ throw new Error(
1804
+ `${where}.y (${y}) lies above the bbox's top edge (${bbox.y})`
1805
+ );
1806
+ }
1807
+ if (x + w > right) {
1808
+ throw new Error(
1809
+ `${where}.w (${w}) puts its right edge at ${x + w}, past the bbox's right edge (${right})`
1810
+ );
1811
+ }
1812
+ if (y + h > bottom) {
1813
+ throw new Error(
1814
+ `${where}.h (${h}) puts its bottom edge at ${y + h}, past the bbox's bottom edge (${bottom})`
1815
+ );
1816
+ }
1817
+ }
1779
1818
  }
1780
1819
  }
1781
1820
  function validateStrandEdges(layers, strandEdges) {
@@ -1952,6 +1991,7 @@ function meshCellsFor(w, h) {
1952
1991
  }
1953
1992
  var FACE_PLATE_CELLS = 10;
1954
1993
  var FEATURE_GRID_CELLS = 4;
1994
+ var MOUTH_GRID_CELL_PX = 32;
1955
1995
  var TURN_LATTICE_CELL_PX = 16;
1956
1996
  var GRID_MARGIN = 0.12;
1957
1997
  var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
@@ -2102,31 +2142,33 @@ function turnColumnMap(grid, faceCenterX, radiusX, angleX, travel) {
2102
2142
  x + boundedCylinderBend(x - faceCenterX, radiusX, theta) + slide
2103
2143
  );
2104
2144
  }
2105
- const cellFor = (v, edges) => {
2145
+ const cellFor = (x) => {
2106
2146
  let lo = 0;
2107
2147
  let hi = grid.cols - 1;
2108
2148
  while (lo < hi) {
2109
2149
  const mid = lo + hi >> 1;
2110
- if (v < edges[mid + 1]) hi = mid;
2150
+ if (x < restX[mid + 1]) hi = mid;
2111
2151
  else lo = mid + 1;
2112
2152
  }
2113
2153
  return lo;
2114
2154
  };
2115
- const sample = (edges, values, v) => {
2116
- const c = cellFor(v, edges);
2117
- const s = Math.max(
2118
- 0,
2119
- Math.min(1, (v - edges[c]) / (edges[c + 1] - edges[c]))
2120
- );
2121
- if (s === 0) return values[c];
2122
- if (s === 1) return values[c + 1];
2123
- return values[c] + (values[c + 1] - values[c]) * s;
2124
- };
2125
2155
  return {
2126
2156
  restX,
2127
2157
  warpedX,
2128
- mapX: (x) => sample(restX, warpedX, x),
2129
- invertX: (X) => sample(warpedX, restX, X)
2158
+ // Piecewise-linear read of `warpedX` over `restX`, the within-cell
2159
+ // fraction clamped to [0,1] as sampleWarpGrid clamps (s, t). A node reads
2160
+ // its own warped x: `warpedX[c] + Δ·1` is `warpedX[c + 1]` only to an
2161
+ // ulp, and the columns a bake reads are nodes.
2162
+ mapX: (x) => {
2163
+ const c = cellFor(x);
2164
+ const s = Math.max(
2165
+ 0,
2166
+ Math.min(1, (x - restX[c]) / (restX[c + 1] - restX[c]))
2167
+ );
2168
+ if (s === 0) return warpedX[c];
2169
+ if (s === 1) return warpedX[c + 1];
2170
+ return warpedX[c] + (warpedX[c + 1] - warpedX[c]) * s;
2171
+ }
2130
2172
  };
2131
2173
  }
2132
2174
  function renderedLandingX(part, vertexLanding, px, py) {
@@ -2232,7 +2274,7 @@ function plateLandingOn(plate, mapAt) {
2232
2274
  y
2233
2275
  );
2234
2276
  }
2235
- function plateGuardRowsFor(face, hairFront) {
2277
+ function plateGuardRowsFor(face, hairFront, widestY = -Infinity) {
2236
2278
  const rowsOf = (part, y) => {
2237
2279
  const { rows } = meshCellsFor(part.cropW, part.cropH);
2238
2280
  return Array.from(
@@ -2247,7 +2289,7 @@ function plateGuardRowsFor(face, hairFront) {
2247
2289
  return [
2248
2290
  ...faceRows,
2249
2291
  ...rowsOf(hairFront, hairFront.centerY).map(
2250
- (y) => Math.max(bottom, Math.min(top, y))
2292
+ (y) => Math.max(bottom, Math.min(top, Math.max(y, widestY)))
2251
2293
  )
2252
2294
  ];
2253
2295
  }
@@ -2259,7 +2301,7 @@ function plateGuardsOf(carriers, faceHalfWidth, profile, hairFront) {
2259
2301
  return {
2260
2302
  plate,
2261
2303
  edgeAt: profile === void 0 ? () => faceHalfWidth : profile.at,
2262
- plateGuardRows: plateGuardRowsFor(plate.part, hairFront)
2304
+ plateGuardRows: plateGuardRowsFor(plate.part, hairFront, profile?.widestY)
2263
2305
  };
2264
2306
  }
2265
2307
  var TurnTargetError = class extends Error {
@@ -2330,12 +2372,17 @@ function resolveTurnTargets(targets = {}) {
2330
2372
  defaulted
2331
2373
  };
2332
2374
  }
2375
+ function noseDrawnBox(layer) {
2376
+ return layer.denseCore ?? layer.bbox;
2377
+ }
2333
2378
  function turnLandmarks(layers, carriers) {
2334
2379
  const markOf = (role) => {
2335
2380
  const layer = layers.find((l) => l.role === role);
2336
2381
  if (!layer) return void 0;
2337
- const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
2338
- const mark = { x: t.x, y: t.y, w: layer.cropW };
2382
+ const drawn = role === "nose" ? noseDrawnBox(layer) : layer.bbox;
2383
+ const t = bboxToTransform(drawn, layer.canvasW, layer.canvasH, role);
2384
+ const w = drawn === layer.bbox ? layer.cropW : drawn.w;
2385
+ const mark = { x: t.x, y: t.y, w };
2339
2386
  const carrier = carriers?.get(role);
2340
2387
  if (carrier !== void 0) mark.carrier = carrier;
2341
2388
  return mark;
@@ -2432,14 +2479,15 @@ var TURN_SWEEP_SAMPLES = 32;
2432
2479
  var HOLD_CLEARANCE = 1;
2433
2480
  var TURN_HOLD_SAMPLES = 16;
2434
2481
  var TURN_SETTLE_PASSES = 24;
2435
- function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge) {
2482
+ function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge, widestY = -Infinity) {
2436
2483
  const local = x - faceCenterX;
2437
2484
  const dist = Math.abs(local);
2438
2485
  const side = Math.sign(local);
2439
- const painted = edgeAt(y);
2440
- if (dist <= painted) return plateLandingAt(deg, x, y);
2486
+ const row = Math.max(y, widestY);
2487
+ const painted = edgeAt(row);
2488
+ if (dist <= painted) return plateLandingAt(deg, x, row);
2441
2489
  if (dist <= holdBase) {
2442
- const inner = plateLandingAt(deg, faceCenterX + side * painted, y);
2490
+ const inner = plateLandingAt(deg, faceCenterX + side * painted, row);
2443
2491
  const u = (dist - painted) / (holdBase - painted);
2444
2492
  return inner + (faceCenterX + side * holdEdge - inner) * u;
2445
2493
  }
@@ -2544,7 +2592,8 @@ function evaluateTurnCandidate(ctx, radius) {
2544
2592
  plateLandingAt,
2545
2593
  ctx.edgeAt,
2546
2594
  hold.holdBase,
2547
- hold.holdEdgeAt(deg)
2595
+ hold.holdEdgeAt(deg),
2596
+ ctx.profile?.widestY
2548
2597
  ),
2549
2598
  leadShift
2550
2599
  );
@@ -3286,6 +3335,7 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
3286
3335
  var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
3287
3336
  var HAIR_FRONT_DEPTH = 0.1;
3288
3337
  var MOUTH_TURN_TILT_DEG = 5;
3338
+ var NOSE_TURN_TILT_DEG = 6;
3289
3339
  var NOD_TRAVEL = 30;
3290
3340
  var NOD_BEND = 0.5;
3291
3341
  var HAIR_BACK_NOD_DEPTH = -0.135;
@@ -3518,7 +3568,7 @@ function plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg)
3518
3568
  )
3519
3569
  );
3520
3570
  }
3521
- function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows) {
3571
+ function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows, widestY = -Infinity) {
3522
3572
  if (holdEdgeAt(0) !== holdBase) {
3523
3573
  throw new Error(
3524
3574
  `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`
@@ -3556,7 +3606,8 @@ function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandi
3556
3606
  plateLandingAt,
3557
3607
  edgeAt,
3558
3608
  holdBase,
3559
- holdEdge
3609
+ holdEdge,
3610
+ widestY
3560
3611
  ) - x,
3561
3612
  0
3562
3613
  );
@@ -3735,7 +3786,7 @@ function defaultPoseOf(bindings) {
3735
3786
  return binding.from + (binding.to - binding.from) * t;
3736
3787
  });
3737
3788
  }
3738
- function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
3789
+ function groupGridFor(members, cells, turnShiftPx, nodShiftPx, cellPx) {
3739
3790
  let minX = Infinity;
3740
3791
  let maxX = -Infinity;
3741
3792
  let minY = Infinity;
@@ -3770,24 +3821,50 @@ function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
3770
3821
  }
3771
3822
  }
3772
3823
  }
3824
+ const extentMinX = minX;
3825
+ const extentMaxX = maxX;
3773
3826
  minX -= turnShiftPx;
3774
3827
  maxX += turnShiftPx;
3775
3828
  minY -= nodShiftPx;
3776
3829
  maxY += nodShiftPx;
3777
3830
  const marginX = (maxX - minX) * GRID_MARGIN + 1;
3778
3831
  const marginY = (maxY - minY) * GRID_MARGIN + 1;
3779
- return {
3780
- cols: cells,
3781
- rows: cells,
3782
- points: generateGridPoints(
3783
- cells,
3784
- cells,
3785
- minX - marginX,
3786
- maxX + marginX,
3787
- minY - marginY,
3788
- maxY + marginY
3789
- )
3790
- };
3832
+ const points = generateGridPoints(
3833
+ cells,
3834
+ cells,
3835
+ minX - marginX,
3836
+ maxX + marginX,
3837
+ minY - marginY,
3838
+ maxY + marginY
3839
+ );
3840
+ if (cellPx === void 0) return { cols: cells, rows: cells, points };
3841
+ const stride = cells + 1;
3842
+ const uniformXs = Array.from({ length: stride }, (_, c) => points[c * 2]);
3843
+ const ys = Array.from(
3844
+ { length: stride },
3845
+ (_, r) => points[r * stride * 2 + 1]
3846
+ );
3847
+ const xs = [uniformXs[0]];
3848
+ for (let c = 0; c < cells; c++) {
3849
+ const left = uniformXs[c];
3850
+ const right = uniformXs[c + 1];
3851
+ const from = Math.max(left, extentMinX);
3852
+ const to = Math.min(right, extentMaxX);
3853
+ if (right - left > cellPx && from < to) {
3854
+ const steps = Math.ceil((to - from) / cellPx);
3855
+ if (from > left) xs.push(from);
3856
+ for (let k = 1; k < steps; k++) {
3857
+ xs.push(from + k * (to - from) / steps);
3858
+ }
3859
+ if (to < right) xs.push(to);
3860
+ }
3861
+ xs.push(right);
3862
+ }
3863
+ const merged = [];
3864
+ for (const y of ys) {
3865
+ for (const x of xs) merged.push(x, y);
3866
+ }
3867
+ return { cols: xs.length - 1, rows: cells, points: merged };
3791
3868
  }
3792
3869
  function eyelidFoldFor(layers, layer, mesh, t) {
3793
3870
  const side = ROLE_TABLE[layer.role].eyeSide;
@@ -3888,7 +3965,8 @@ function turnSetup(layers) {
3888
3965
  own,
3889
3966
  isPlate ? FACE_PLATE_CELLS : FEATURE_GRID_CELLS,
3890
3967
  isPlate ? 0 : turnShift[turnFamily(role)],
3891
- nodShiftPx
3968
+ nodShiftPx,
3969
+ group === "mouthWarp" ? MOUTH_GRID_CELL_PX : void 0
3892
3970
  )
3893
3971
  );
3894
3972
  }
@@ -3898,11 +3976,31 @@ function turnSetup(layers) {
3898
3976
  y: mouthAt.y,
3899
3977
  tiltDeg: MOUTH_TURN_TILT_DEG
3900
3978
  };
3979
+ const noseLayer = layers.find((l) => l.role === "nose");
3980
+ let noseAnchor;
3981
+ if (noseLayer) {
3982
+ const drawn = noseDrawnBox(noseLayer);
3983
+ const at = bboxToTransform(
3984
+ drawn,
3985
+ noseLayer.canvasW,
3986
+ noseLayer.canvasH,
3987
+ "nose"
3988
+ );
3989
+ noseAnchor = {
3990
+ x: at.x,
3991
+ y: at.y + drawn.h / 2,
3992
+ tiltDeg: NOSE_TURN_TILT_DEG
3993
+ };
3994
+ }
3901
3995
  const carriers = new Map(
3902
- members.map((m) => [
3903
- m.role,
3904
- m.group === "mouthWarp" ? { grid: groupGrids.get(m.group), part: m.part, anchor: mouthAnchor } : { grid: groupGrids.get(m.group), part: m.part }
3905
- ])
3996
+ members.map((m) => {
3997
+ const grid = groupGrids.get(m.group);
3998
+ const anchor = m.group === "mouthWarp" ? mouthAnchor : m.group === "noseWarp" ? noseAnchor : void 0;
3999
+ return [
4000
+ m.role,
4001
+ anchor === void 0 ? { grid, part: m.part } : { grid, part: m.part, anchor }
4002
+ ];
4003
+ })
3906
4004
  );
3907
4005
  const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
3908
4006
  let reach = 0;
@@ -4208,7 +4306,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
4208
4306
  edgeAt,
4209
4307
  holdBase,
4210
4308
  turn?.holdEdgeAt ?? (() => holdBase),
4211
- plateGuardRows
4309
+ plateGuardRows,
4310
+ profile?.widestY
4212
4311
  )
4213
4312
  );
4214
4313
  ownWarps.push(