@ikijs/editor 0.23.0 → 0.24.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.d.mts CHANGED
@@ -891,9 +891,12 @@ interface GenerateOptions {
891
891
  * wedge carries the painted line, fading out in the antialias column as the
892
892
  * painted edge does; the fan inside it is the crop's own pixels.
893
893
  *
894
- * Draw order: hair_back < body < elbow_L, arm_L, forearm_L, elbow_R, arm_R,
895
- * forearm_R < face … < hair_front. The forearm draws over the upper arm,
896
- * which draws over the cap.
894
+ * Draw order: hair_back < body < elbow_L, forearm_L, arm_L, elbow_R,
895
+ * forearm_R, arm_R < face … < hair_front. The upper arm draws over the
896
+ * forearm, which draws over the cap. On the inside of a bend the bands
897
+ * overlap, and the forearm's seam-row cut stays under the upper arm there, so
898
+ * the overlap shows the upper arm's bottom edge at the elbow row instead of
899
+ * the cut rotated up the sleeve.
897
900
  *
898
901
  * Signs: ParamArmX + raises the arm outward on either side, in degrees 1:1.
899
902
  * A rotation is CCW-positive, so an arm whose shoulder lies right of the
@@ -1070,17 +1073,23 @@ declare function parseLayerRoles(fileNames: string[]): {
1070
1073
  * then stay inside the line's closed ink at every column, the tapered ends
1071
1074
  * included, and tucked skin never shows above the line.
1072
1075
  *
1073
- * The seam is the line's drawn bottom edge `Tu` plus one smooth closing
1074
- * travel `D`, a cubic in the column index pinned to zero at the opening's two
1075
- * end columns, so the closed line's ends are the drawn line's own ends. It is
1076
- * fitted by least squares to the raw per-column travel `(T - UPPER_SHARE * H)
1077
- * - Tu`, how far the line's bottom is from the share's point in the
1078
- * interior's span. The travel is fitted, not the seam, because `Tu` is an
1079
- * integer-row read: a smooth seam minus that staircase is a sawtooth the mesh
1080
- * would carry into the rendered line, while a smooth travel carries the
1081
- * drawn stroke (whose antialiased edge is smooth) as one piece. The share sets
1082
- * the travel's depth (the sag), the art's two arcs its shape; the cubic keeps
1083
- * an asymmetric mouth's tilt and cannot wobble.
1076
+ * Shut, the line is a smooth parabola across the opening. Its chord joins the
1077
+ * line's smoothed bottom (below) at the two end columns; its depth below the
1078
+ * chord, half way, is the sag at the centre column of the drawn arc plus a
1079
+ * fitted closing travel: a cubic in the column index, pinned to zero at the
1080
+ * end columns and fitted by least squares to the raw per-column travel
1081
+ * `(T - UPPER_SHARE * H) - Tu`, how far the line's bottom is from the share's
1082
+ * point in the interior's span. That sag is capped at `CLOSED_SAG_MAX` of the
1083
+ * opening's width, the drawn arc included, so a deep opening closes into a
1084
+ * gentle smile and a line drawn deeper than the cap closes flatter than
1085
+ * drawn. The seam is `Tu` plus the column's travel: the parabola minus the
1086
+ * line's bottom averaged over 3 columns each side (the smoothed bottom), not
1087
+ * minus `Tu` itself, so the end columns do not travel. `Tu` is an integer-row read, and a curve minus that staircase
1088
+ * is a sawtooth the mesh would carry into the rendered line; against the
1089
+ * smoothed bottom the stroke (whose antialiased edge is smooth) moves as one
1090
+ * piece, and a kink drawn in it (a V at the centre) keeps only its offset
1091
+ * from that average, which the cap counts in. Every closed shape and the
1092
+ * preview are read off the seam.
1084
1093
  *
1085
1094
  * The lower lip's columns outside the opening (the corner hooks) do not fold,
1086
1095
  * the line's are only thinned and shaped (no travel), and the interior's edge
@@ -1093,9 +1102,9 @@ declare function parseLayerRoles(fileNames: string[]): {
1093
1102
  *
1094
1103
  * Knots: the parts share column knots every `MOUTH_KNOT_PX` plus the
1095
1104
  * opening's boundaries `x0`, `x1` and `x1 + 1`, each an integer canvas
1096
- * boundary. The fit's x is the column index, which is the mesh's boundary
1105
+ * boundary. The travel's x is the column index, which is the mesh's boundary
1097
1106
  * position: a knot at boundary `b` samples column `b`, so the mesh renders the
1098
- * fit exactly at its knots and the pinned ends render as pinned (`x1` and
1107
+ * travel exactly at its knots and the pinned ends render as pinned (`x1` and
1099
1108
  * `x1 + 1` both sample the last column; the second sits at its right edge).
1100
1109
  * The preview resamples each column through the lip meshes' own landed rows
1101
1110
  * (`mouthRestShift`). A part's mesh rounds its local x, so one knot
@@ -1129,8 +1138,8 @@ interface OpeningColumn {
1129
1138
  /** How far the skin tucks up under the line shut: inside the thinned,
1130
1139
  * squashed line's ink. */
1131
1140
  overlap: number;
1132
- /** Where the lips meet when shut: the line's bottom edge after its smooth
1133
- * closing travel. */
1141
+ /** Where the lips meet when shut: the line's bottom edge after its closing
1142
+ * travel. */
1134
1143
  seam: number;
1135
1144
  }
1136
1145
  interface Opening {
@@ -1507,10 +1516,17 @@ interface AtlasLayout {
1507
1516
  * BOTTOM only. Page left/top edges need no gutter.
1508
1517
  *
1509
1518
  * pageWidth/pageHeight = tight bound (max x+width+padding, max y+height+padding).
1519
+ * When `maxSide` is given and the first layout is taller than it but not wider,
1520
+ * the pack is retried at every width from the widest padded source up to `maxSide`, and the first
1521
+ * layout that fits the height wins (shelf height is not monotonic in width);
1522
+ * when none does the first is returned for the caller to refuse. The attempts
1523
+ * are every width for this shelf order, not a proof that no packing exists.
1524
+ *
1510
1525
  * Empty sources → { pageWidth: 0, pageHeight: 0, placements: [], padding }.
1511
- * Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension.
1526
+ * Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension,
1527
+ * and on a supplied `maxSide` that is non-finite or <= 0.
1512
1528
  */
1513
- declare function packAtlas(sources: AtlasSource[], padding?: number): AtlasLayout;
1529
+ declare function packAtlas(sources: AtlasSource[], padding?: number, maxSide?: number): AtlasLayout;
1514
1530
  /**
1515
1531
  * Convert a pixel placement into a UV rect, inset by `insetPx` on all four
1516
1532
  * edges and clamped to [0, 1] so the validator's bounds check always passes.
package/dist/index.d.ts CHANGED
@@ -891,9 +891,12 @@ interface GenerateOptions {
891
891
  * wedge carries the painted line, fading out in the antialias column as the
892
892
  * painted edge does; the fan inside it is the crop's own pixels.
893
893
  *
894
- * Draw order: hair_back < body < elbow_L, arm_L, forearm_L, elbow_R, arm_R,
895
- * forearm_R < face … < hair_front. The forearm draws over the upper arm,
896
- * which draws over the cap.
894
+ * Draw order: hair_back < body < elbow_L, forearm_L, arm_L, elbow_R,
895
+ * forearm_R, arm_R < face … < hair_front. The upper arm draws over the
896
+ * forearm, which draws over the cap. On the inside of a bend the bands
897
+ * overlap, and the forearm's seam-row cut stays under the upper arm there, so
898
+ * the overlap shows the upper arm's bottom edge at the elbow row instead of
899
+ * the cut rotated up the sleeve.
897
900
  *
898
901
  * Signs: ParamArmX + raises the arm outward on either side, in degrees 1:1.
899
902
  * A rotation is CCW-positive, so an arm whose shoulder lies right of the
@@ -1070,17 +1073,23 @@ declare function parseLayerRoles(fileNames: string[]): {
1070
1073
  * then stay inside the line's closed ink at every column, the tapered ends
1071
1074
  * included, and tucked skin never shows above the line.
1072
1075
  *
1073
- * The seam is the line's drawn bottom edge `Tu` plus one smooth closing
1074
- * travel `D`, a cubic in the column index pinned to zero at the opening's two
1075
- * end columns, so the closed line's ends are the drawn line's own ends. It is
1076
- * fitted by least squares to the raw per-column travel `(T - UPPER_SHARE * H)
1077
- * - Tu`, how far the line's bottom is from the share's point in the
1078
- * interior's span. The travel is fitted, not the seam, because `Tu` is an
1079
- * integer-row read: a smooth seam minus that staircase is a sawtooth the mesh
1080
- * would carry into the rendered line, while a smooth travel carries the
1081
- * drawn stroke (whose antialiased edge is smooth) as one piece. The share sets
1082
- * the travel's depth (the sag), the art's two arcs its shape; the cubic keeps
1083
- * an asymmetric mouth's tilt and cannot wobble.
1076
+ * Shut, the line is a smooth parabola across the opening. Its chord joins the
1077
+ * line's smoothed bottom (below) at the two end columns; its depth below the
1078
+ * chord, half way, is the sag at the centre column of the drawn arc plus a
1079
+ * fitted closing travel: a cubic in the column index, pinned to zero at the
1080
+ * end columns and fitted by least squares to the raw per-column travel
1081
+ * `(T - UPPER_SHARE * H) - Tu`, how far the line's bottom is from the share's
1082
+ * point in the interior's span. That sag is capped at `CLOSED_SAG_MAX` of the
1083
+ * opening's width, the drawn arc included, so a deep opening closes into a
1084
+ * gentle smile and a line drawn deeper than the cap closes flatter than
1085
+ * drawn. The seam is `Tu` plus the column's travel: the parabola minus the
1086
+ * line's bottom averaged over 3 columns each side (the smoothed bottom), not
1087
+ * minus `Tu` itself, so the end columns do not travel. `Tu` is an integer-row read, and a curve minus that staircase
1088
+ * is a sawtooth the mesh would carry into the rendered line; against the
1089
+ * smoothed bottom the stroke (whose antialiased edge is smooth) moves as one
1090
+ * piece, and a kink drawn in it (a V at the centre) keeps only its offset
1091
+ * from that average, which the cap counts in. Every closed shape and the
1092
+ * preview are read off the seam.
1084
1093
  *
1085
1094
  * The lower lip's columns outside the opening (the corner hooks) do not fold,
1086
1095
  * the line's are only thinned and shaped (no travel), and the interior's edge
@@ -1093,9 +1102,9 @@ declare function parseLayerRoles(fileNames: string[]): {
1093
1102
  *
1094
1103
  * Knots: the parts share column knots every `MOUTH_KNOT_PX` plus the
1095
1104
  * opening's boundaries `x0`, `x1` and `x1 + 1`, each an integer canvas
1096
- * boundary. The fit's x is the column index, which is the mesh's boundary
1105
+ * boundary. The travel's x is the column index, which is the mesh's boundary
1097
1106
  * position: a knot at boundary `b` samples column `b`, so the mesh renders the
1098
- * fit exactly at its knots and the pinned ends render as pinned (`x1` and
1107
+ * travel exactly at its knots and the pinned ends render as pinned (`x1` and
1099
1108
  * `x1 + 1` both sample the last column; the second sits at its right edge).
1100
1109
  * The preview resamples each column through the lip meshes' own landed rows
1101
1110
  * (`mouthRestShift`). A part's mesh rounds its local x, so one knot
@@ -1129,8 +1138,8 @@ interface OpeningColumn {
1129
1138
  /** How far the skin tucks up under the line shut: inside the thinned,
1130
1139
  * squashed line's ink. */
1131
1140
  overlap: number;
1132
- /** Where the lips meet when shut: the line's bottom edge after its smooth
1133
- * closing travel. */
1141
+ /** Where the lips meet when shut: the line's bottom edge after its closing
1142
+ * travel. */
1134
1143
  seam: number;
1135
1144
  }
1136
1145
  interface Opening {
@@ -1507,10 +1516,17 @@ interface AtlasLayout {
1507
1516
  * BOTTOM only. Page left/top edges need no gutter.
1508
1517
  *
1509
1518
  * pageWidth/pageHeight = tight bound (max x+width+padding, max y+height+padding).
1519
+ * When `maxSide` is given and the first layout is taller than it but not wider,
1520
+ * the pack is retried at every width from the widest padded source up to `maxSide`, and the first
1521
+ * layout that fits the height wins (shelf height is not monotonic in width);
1522
+ * when none does the first is returned for the caller to refuse. The attempts
1523
+ * are every width for this shelf order, not a proof that no packing exists.
1524
+ *
1510
1525
  * Empty sources → { pageWidth: 0, pageHeight: 0, placements: [], padding }.
1511
- * Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension.
1526
+ * Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension,
1527
+ * and on a supplied `maxSide` that is non-finite or <= 0.
1512
1528
  */
1513
- declare function packAtlas(sources: AtlasSource[], padding?: number): AtlasLayout;
1529
+ declare function packAtlas(sources: AtlasSource[], padding?: number, maxSide?: number): AtlasLayout;
1514
1530
  /**
1515
1531
  * Convert a pixel placement into a UV rect, inset by `insetPx` on all four
1516
1532
  * edges and clamped to [0, 1] so the validator's bounds check always passes.
package/dist/index.js CHANGED
@@ -1845,7 +1845,10 @@ function captureBindingEndpoint(channel, restValue, posedValue) {
1845
1845
  // src/atlas.ts
1846
1846
  var ATLAS_PADDING = 2;
1847
1847
  var UV_INSET_PX = 0.5;
1848
- function packAtlas(sources, padding = ATLAS_PADDING) {
1848
+ function packAtlas(sources, padding = ATLAS_PADDING, maxSide) {
1849
+ if (maxSide !== void 0 && !(isFinite(maxSide) && maxSide > 0)) {
1850
+ throw new Error(`packAtlas: invalid maxSide ${maxSide}`);
1851
+ }
1849
1852
  for (const src of sources) {
1850
1853
  if (!isFinite(src.width) || src.width <= 0) {
1851
1854
  throw new Error(
@@ -1866,42 +1869,50 @@ function packAtlas(sources, padding = ATLAS_PADDING) {
1866
1869
  (sum, s) => sum + (s.width + padding) * (s.height + padding),
1867
1870
  0
1868
1871
  );
1869
- const targetWidth = Math.max(
1870
- Math.ceil(Math.sqrt(totalArea)),
1871
- // Ensure at least the widest single source fits.
1872
- Math.max(...sorted.map((s) => s.width + padding))
1873
- );
1874
- const placements = [];
1875
- let shelfX = 0;
1876
- let shelfY = 0;
1877
- let shelfHeight = 0;
1878
- for (const src of sorted) {
1879
- const paddedW = src.width + padding;
1880
- const paddedH = src.height + padding;
1881
- if (shelfX > 0 && shelfX + paddedW > targetWidth) {
1882
- shelfY += shelfHeight;
1883
- shelfX = 0;
1884
- shelfHeight = 0;
1885
- }
1886
- placements.push({
1887
- id: src.id,
1888
- x: shelfX,
1889
- y: shelfY,
1890
- width: src.width,
1891
- height: src.height
1892
- });
1893
- shelfX += paddedW;
1894
- if (paddedH > shelfHeight) shelfHeight = paddedH;
1872
+ const widest = Math.max(...sorted.map((s) => s.width + padding));
1873
+ const firstWidth = Math.max(Math.ceil(Math.sqrt(totalArea)), widest);
1874
+ const shelve = (targetWidth) => {
1875
+ const placements = [];
1876
+ let shelfX = 0;
1877
+ let shelfY = 0;
1878
+ let shelfHeight = 0;
1879
+ for (const src of sorted) {
1880
+ const paddedW = src.width + padding;
1881
+ const paddedH = src.height + padding;
1882
+ if (shelfX > 0 && shelfX + paddedW > targetWidth) {
1883
+ shelfY += shelfHeight;
1884
+ shelfX = 0;
1885
+ shelfHeight = 0;
1886
+ }
1887
+ placements.push({
1888
+ id: src.id,
1889
+ x: shelfX,
1890
+ y: shelfY,
1891
+ width: src.width,
1892
+ height: src.height
1893
+ });
1894
+ shelfX += paddedW;
1895
+ if (paddedH > shelfHeight) shelfHeight = paddedH;
1896
+ }
1897
+ let pageWidth = 0;
1898
+ let pageHeight = 0;
1899
+ for (const p of placements) {
1900
+ const right = p.x + p.width + padding;
1901
+ const bottom = p.y + p.height + padding;
1902
+ if (right > pageWidth) pageWidth = right;
1903
+ if (bottom > pageHeight) pageHeight = bottom;
1904
+ }
1905
+ return { pageWidth, pageHeight, placements };
1906
+ };
1907
+ const first = shelve(firstWidth);
1908
+ if (maxSide === void 0 || first.pageHeight <= maxSide || first.pageWidth > maxSide) {
1909
+ return { ...first, padding };
1895
1910
  }
1896
- let pageWidth = 0;
1897
- let pageHeight = 0;
1898
- for (const p of placements) {
1899
- const right = p.x + p.width + padding;
1900
- const bottom = p.y + p.height + padding;
1901
- if (right > pageWidth) pageWidth = right;
1902
- if (bottom > pageHeight) pageHeight = bottom;
1911
+ for (let w = widest; w <= maxSide; w++) {
1912
+ const layout = shelve(w);
1913
+ if (layout.pageHeight <= maxSide) return { ...layout, padding };
1903
1914
  }
1904
- return { pageWidth, pageHeight, placements, padding };
1915
+ return { ...first, padding };
1905
1916
  }
1906
1917
  function uvRectFor(placement, page, insetPx = UV_INSET_PX) {
1907
1918
  const px = placement.x + insetPx;
@@ -3004,14 +3015,14 @@ function armParts(role, layer, g, order, poseSwitch) {
3004
3015
  },
3005
3016
  true
3006
3017
  ),
3007
- part(role, ids.armDeformer, order + 1, meshOf(band(0.5, seam), quad)),
3008
3018
  part(
3009
3019
  ids.forearm,
3010
3020
  ids.forearmDeformer,
3011
- order + 2,
3021
+ order + 1,
3012
3022
  meshOf(band(seam, -0.5), quad),
3013
3023
  true
3014
- )
3024
+ ),
3025
+ part(role, ids.armDeformer, order + 2, meshOf(band(0.5, seam), quad))
3015
3026
  ];
3016
3027
  }
3017
3028
  var rotateBinding = (parameter, range, sign) => ({
@@ -3105,8 +3116,8 @@ var isLipInsideRole = (role) => LIP_INSIDE_ROLES.includes(role);
3105
3116
  var ROLE_TABLE = [
3106
3117
  { role: "hair_back", family: "hair_back" },
3107
3118
  { role: "body", family: "body" },
3108
- { role: "arm_L", family: "arm", parts: ["elbow_L", "arm_L", "forearm_L"] },
3109
- { role: "arm_R", family: "arm", parts: ["elbow_R", "arm_R", "forearm_R"] },
3119
+ { role: "arm_L", family: "arm", parts: ["elbow_L", "forearm_L", "arm_L"] },
3120
+ { role: "arm_R", family: "arm", parts: ["elbow_R", "forearm_R", "arm_R"] },
3110
3121
  { role: "face", family: "face", required: true },
3111
3122
  { role: "blush_L", family: "face" },
3112
3123
  { role: "blush_R", family: "face" },
@@ -3251,6 +3262,7 @@ function parseLayerRoles(fileNames) {
3251
3262
 
3252
3263
  // src/auto-rig/mouth.ts
3253
3264
  var UPPER_SHARE = 0.4;
3265
+ var CLOSED_SAG_MAX = 0.1;
3254
3266
  var UPPER_THIN = 0.7;
3255
3267
  var END_TAPER = 0.35;
3256
3268
  var END_TAPER_FROM = 0.7;
@@ -3317,10 +3329,15 @@ function mouthOpening(byRole) {
3317
3329
  for (let d = 0; (r = read(c - d) ?? read(c + d)) === void 0; d++) ;
3318
3330
  reads.push(r);
3319
3331
  }
3332
+ const n = reads.length;
3320
3333
  const m = (x0 + x1) / 2;
3321
- let p = 0;
3322
- let q = 0;
3323
- if (reads.length >= 4) {
3334
+ const centre = Math.round(m);
3335
+ const smooth = reads.map((_, i) => {
3336
+ const near = reads.slice(Math.max(0, i - 3), i + 4);
3337
+ return near.reduce((s, r) => s + r.Tu, 0) / near.length;
3338
+ });
3339
+ let sag = 0;
3340
+ if (n >= 4) {
3324
3341
  let s11 = 0;
3325
3342
  let s12 = 0;
3326
3343
  let s22 = 0;
@@ -3338,16 +3355,24 @@ function mouthOpening(byRole) {
3338
3355
  t2 += b2 * d;
3339
3356
  });
3340
3357
  const det = s11 * s22 - s12 * s12;
3341
- p = (t1 * s22 - t2 * s12) / det;
3342
- q = (s11 * t2 - s12 * t1) / det;
3358
+ const p = (t1 * s22 - t2 * s12) / det;
3359
+ const q = (s11 * t2 - s12 * t1) / det;
3360
+ const c = centre - x0;
3361
+ const D = c * (centre - x1) * (p + q * (centre - m));
3362
+ const arc = (reads[0].Tu + reads[n - 1].Tu) / 2 - reads[c].Tu;
3363
+ const t = c / (n - 1);
3364
+ const k = 4 * t * (1 - t);
3365
+ const offset = arc - (smooth[0] + (smooth[n - 1] - smooth[0]) * t - smooth[c]);
3366
+ sag = Math.min(arc - D, CLOSED_SAG_MAX * n - Math.max(0, offset) / k);
3343
3367
  }
3344
3368
  const columns = reads.map((r, i) => {
3345
- const x = x0 + i;
3346
- return { ...r, seam: r.Tu + (x - x0) * (x - x1) * (p + q * (x - m)) };
3369
+ if (n < 4) return { ...r, seam: r.Tu };
3370
+ const t = i / (x1 - x0);
3371
+ const parabola = smooth[0] + (smooth[n - 1] - smooth[0]) * t - sag * 4 * t * (1 - t);
3372
+ return { ...r, seam: r.Tu + parabola - smooth[i] };
3347
3373
  });
3348
3374
  const clamp2 = (col) => Math.min(x1, Math.max(x0, col));
3349
3375
  const base = (col) => columns[clamp2(col) - x0];
3350
- const centre = Math.round((x0 + x1) / 2);
3351
3376
  const w = base(centre).lineH;
3352
3377
  const from = END_TAPER_FROM * ((x1 - x0 + 1) / 2);
3353
3378
  const squash = (col) => {