@ikijs/editor 0.10.0 → 0.11.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
@@ -1632,7 +1632,6 @@ function createLayerSetMeasurer(canvas) {
1632
1632
  const opaque = new Uint8Array(canvasW * canvasH);
1633
1633
  const layerInputs = [];
1634
1634
  const rowSpansByRole = [];
1635
- let hairFrontMask;
1636
1635
  function add({ role, fileName, rgba }) {
1637
1636
  if (rgba.length !== rgbaLength) {
1638
1637
  throw new Error(
@@ -1643,19 +1642,29 @@ function createLayerSetMeasurer(canvas) {
1643
1642
  if (bbox === null) return null;
1644
1643
  const rowLeft = new Int32Array(canvasH).fill(canvasW);
1645
1644
  const rowRight = new Int32Array(canvasH).fill(-1);
1646
- const ownMask = role === "hair_front" ? new Uint8Array(canvasW * canvasH) : void 0;
1645
+ const rowRuns = role === "hair_front" || role === "hair_back" ? [] : void 0;
1647
1646
  for (let y = 0; y < canvasH; y++) {
1647
+ let runs;
1648
+ if (rowRuns !== void 0 && y >= bbox.y && y < bbox.y + bbox.h) {
1649
+ runs = [];
1650
+ rowRuns.push(runs);
1651
+ }
1652
+ let inRun = false;
1648
1653
  for (let x = 0; x < canvasW; x++) {
1649
1654
  const p = y * canvasW + x;
1650
- if (rgba[p * 4 + 3] < ALPHA_OPAQUE) continue;
1655
+ const on = rgba[p * 4 + 3] >= ALPHA_OPAQUE;
1656
+ if (runs !== void 0 && on !== inRun) {
1657
+ runs.push(x);
1658
+ inRun = on;
1659
+ }
1660
+ if (!on) continue;
1651
1661
  opaque[p] = 1;
1652
- if (ownMask !== void 0) ownMask[p] = 1;
1653
1662
  if (x < rowLeft[y]) rowLeft[y] = x;
1654
1663
  if (x > rowRight[y]) rowRight[y] = x;
1655
1664
  }
1665
+ if (runs !== void 0 && inRun) runs.push(canvasW);
1656
1666
  }
1657
1667
  rowSpansByRole.push({ role, rowLeft, rowRight });
1658
- if (ownMask !== void 0) hairFrontMask = ownMask;
1659
1668
  const layer = {
1660
1669
  role,
1661
1670
  fileName,
@@ -1665,6 +1674,7 @@ function createLayerSetMeasurer(canvas) {
1665
1674
  cropW: bbox.w,
1666
1675
  cropH: bbox.h
1667
1676
  };
1677
+ if (rowRuns !== void 0) layer.rowRuns = rowRuns;
1668
1678
  if (role === "face") {
1669
1679
  const rowHalfWidths = [];
1670
1680
  for (let y = bbox.y; y < bbox.y + bbox.h; y++) {
@@ -1709,8 +1719,10 @@ function createLayerSetMeasurer(canvas) {
1709
1719
  }
1710
1720
  let strandEdges;
1711
1721
  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));
1712
- if (hairFrontMask !== void 0 && irisLayers.length === 2) {
1713
- const mask = hairFrontMask;
1722
+ const hairFront = layerInputs.find((l) => l.role === "hair_front");
1723
+ const hairRuns = hairFront?.rowRuns;
1724
+ if (hairFront !== void 0 && hairRuns !== void 0 && irisLayers.length === 2) {
1725
+ const hairTop = hairFront.bbox.y;
1714
1726
  const half = canvasW / 2;
1715
1727
  const strandOn = (iris, other, side) => {
1716
1728
  const r = Math.floor(iris.bbox.y + iris.bbox.h / 2);
@@ -1721,15 +1733,11 @@ function createLayerSetMeasurer(canvas) {
1721
1733
  if (span2.rowRight[r] < span2.rowLeft[r]) return void 0;
1722
1734
  const minRun = STRAND_MIN_RUN_FRACTION * (span2.rowRight[r] + 1 - span2.rowLeft[r]);
1723
1735
  const strand = new Uint8Array(canvasW);
1724
- for (let a = 0; a < canvasW; ) {
1725
- if (mask[r * canvasW + a] !== 1) {
1726
- a++;
1727
- continue;
1736
+ const runs = r >= hairTop && r < hairTop + hairRuns.length ? hairRuns[r - hairTop] : [];
1737
+ for (let k = 0; k < runs.length; k += 2) {
1738
+ if (runs[k + 1] - runs[k] >= minRun) {
1739
+ strand.fill(1, runs[k], runs[k + 1]);
1728
1740
  }
1729
- let b = a;
1730
- while (b < canvasW && mask[r * canvasW + b] === 1) b++;
1731
- if (b - a >= minRun) strand.fill(1, a, b);
1732
- a = b;
1733
1741
  }
1734
1742
  const strandAt = (x2) => x2 >= 0 && x2 < canvasW && strand[x2] === 1;
1735
1743
  const outerBoundary = (x2) => (side < 0 ? x2 : x2 + 1) - half;
@@ -2171,6 +2179,23 @@ function checkLayers(layers, canvas) {
2171
2179
  }
2172
2180
  });
2173
2181
  }
2182
+ if (l.rowRuns !== void 0) {
2183
+ const rows = l.rowRuns;
2184
+ if (!Array.isArray(rows) || rows.length !== h) {
2185
+ throw new Error(
2186
+ `${at}: rowRuns must have one entry per crop row (${h})`
2187
+ );
2188
+ }
2189
+ rows.forEach((runs, i) => {
2190
+ if (!Array.isArray(runs) || runs.length % 2 !== 0 || !runs.every(
2191
+ (c, k) => Number.isInteger(c) && c >= x && c <= x + w && (k === 0 || c > runs[k - 1])
2192
+ )) {
2193
+ throw new Error(
2194
+ `${at}: rowRuns[${i}] ${JSON.stringify(runs)} is not an even-length, strictly increasing list of columns inside its crop (${x}..${x + w})`
2195
+ );
2196
+ }
2197
+ });
2198
+ }
2174
2199
  if (l.jawRows !== void 0) {
2175
2200
  const cols = l.jawRows;
2176
2201
  if (!Array.isArray(cols) || cols.length !== w) {
@@ -2428,12 +2453,17 @@ var TURN = {
2428
2453
  * its translation (bangs 1.18×, side locks at the eye row ≈ 1.0×). */
2429
2454
  hairFollow: 1.1,
2430
2455
  /** The head's outline at the eye row holds (−0.014 / −0.004 hh): where the
2431
- * front hair draws it, its outer edge follows the face this much. */
2456
+ * front hair draws it, its outer edge follows the face this much on a row
2457
+ * where no back hair paints behind that edge. Where back hair does, the
2458
+ * edge rides further, as far as that back hair stays behind it through
2459
+ * the turn and the nod, up to the face's follow there (`hairFollow`;
2460
+ * over the far eye's rows, as far as that eye's outer corner goes). */
2432
2461
  outlineFollow: 0,
2433
2462
  /** The back hair's slight counter-motion. */
2434
2463
  hairBack: -0.027,
2435
2464
  /** The ears lag the plate: the far one moves this share of its slide, the
2436
- * near one this (the samples' parallax ratios, 0.44 and 0.87). */
2465
+ * near one this at its widest reach, its root riding the head (the
2466
+ * samples' parallax ratios, 0.44 and 0.87). */
2437
2467
  earFar: 0.44,
2438
2468
  earNear: 0.87
2439
2469
  };
@@ -2447,6 +2477,13 @@ var NOD = {
2447
2477
  nose: { up: -0.207, down: 0.193 },
2448
2478
  mouth: { up: -0.172, down: 0.177 },
2449
2479
  hairFront: { up: -0.11, down: 0.189 },
2480
+ /** The front hair's cap top. The samples' cap top moves 1.215× their face's
2481
+ * centroid looking down (Mao 1.236, Haru 1.626, Hiyori 1.194, Natori
2482
+ * 0.730) and 0.415× looking up (0.385, 0.430, 0.400, 0.487); each ratio
2483
+ * times the plate's own nod halfway between its top and the chin (0.1435
2484
+ * down, 0.084 up). Looking down, the rig slides it only as far as the
2485
+ * back hair covers the crown behind it. */
2486
+ hairFrontTop: { up: -0.035, down: 0.174 },
2450
2487
  hairBack: { up: -0.011, down: 0.025 }
2451
2488
  };
2452
2489
  var ROLL_DEG = 10;
@@ -2494,418 +2531,82 @@ function resolveStyle(style, check) {
2494
2531
  };
2495
2532
  }
2496
2533
 
2497
- // src/auto-rig/fields.ts
2498
- var NOSE_TILT_DEG = 6;
2499
- function nodDy(v, ay, hh) {
2500
- const up = Math.max(0, ay) / 30;
2501
- const down = Math.max(0, -ay) / 30;
2502
- return -(up * v.up + down * v.down) * hh;
2503
- }
2504
- function spin(x, y, ox, oy, a) {
2505
- if (a === 0) return [0, 0];
2506
- const px = x - ox;
2507
- const py = y - oy;
2508
- return [
2509
- px * Math.cos(a) - py * Math.sin(a) - px,
2510
- px * Math.sin(a) + py * Math.cos(a) - py
2511
- ];
2512
- }
2513
- function faceField(m) {
2514
- const f = m.frame;
2515
- return (x, y, ax, ay) => {
2516
- const s = Math.sign(ax);
2517
- const a = Math.abs(ax) / 30;
2518
- const chin = smoothstep(f.mouthY, f.chinY, y);
2519
- const width = m.widthUpper + (m.widthJaw - m.widthUpper) * smoothstep(f.cheekY, f.jawY, y);
2520
- const shell = f.hairShell ? 0 : m.shellScale - 1;
2521
- const dx = s * a * (m.face + m.chinLead * chin) + a * (width - 1 + shell) * (x - f.axisX);
2522
- const t = Math.min(
2523
- 1,
2524
- Math.max(0, (f.face.y1 - y) / Math.max(1, f.face.y1 - f.chinY))
2525
- );
2526
- const v = {
2527
- up: NOD.faceTop.up + (NOD.chin.up - NOD.faceTop.up) * t,
2528
- down: NOD.faceTop.down + (NOD.chin.down - NOD.faceTop.down) * t
2529
- };
2530
- return [dx, nodDy(v, ay, f.hh)];
2531
- };
2532
- }
2533
- function earField(m) {
2534
- const face = faceField(m);
2535
- return (x, y, ax, ay) => {
2536
- const s = Math.sign(ax);
2537
- const a = Math.abs(ax) / 30;
2538
- const share = sideOf(m, x, s) === "far" ? TURN.earFar : TURN.earNear;
2539
- return [s * a * share * m.face, face(x, y, 0, ay)[1]];
2540
- };
2541
- }
2542
- var NECK_SHADE_MAX = 0.45;
2543
- var NECK_SHADE_FULL = 0.2;
2544
- function neckField(m) {
2545
- const f = m.frame;
2546
- const nk = f.neck;
2547
- if (nk === void 0) return () => [0, 0];
2548
- const face = faceField(m);
2549
- const full = Math.abs(face(f.axisX, f.chinY, 30, 0)[0]);
2550
- const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
2551
- const bottom = f.face.y0;
2552
- const tip = f.cutAt(f.axisX);
2553
- const top = tip - NECK_SHADE_FULL * (tip - bottom);
2554
- return (x, y, ax) => {
2555
- const wy = Math.min(
2556
- 1,
2557
- Math.max(0, (y - bottom) / Math.max(1, top - bottom))
2558
- );
2559
- const wx = Math.max(0, 1 - Math.abs(x - f.axisX) / nk.waist);
2560
- return [share * face(f.axisX, f.chinY, ax, 0)[0] * wx * wy, 0];
2561
- };
2562
- }
2563
- function sideOf(m, x, s) {
2564
- return s * (x - m.frame.axisX) > 0 ? "far" : "near";
2534
+ // src/auto-rig/head.ts
2535
+ function outerEdge(runs, axisX, side) {
2536
+ if (runs.length === 0) return void 0;
2537
+ const x = side > 0 ? runs[runs.length - 1] : runs[0];
2538
+ return side * (x - axisX) > 0 ? x : void 0;
2565
2539
  }
2566
- function eyeField(m, box) {
2567
- const xe = cx(box);
2568
- const ye = cy(box);
2569
- const hh = m.frame.hh;
2570
- return (x, y, ax, ay) => {
2571
- const s = Math.sign(ax);
2572
- const a = Math.abs(ax) / 30;
2573
- const side = sideOf(m, xe, s);
2574
- const dx = s * a * m.eye[side] + a * (m.eyeScale[side] - 1) * (x - xe);
2575
- const up = Math.max(0, ay) / 30;
2576
- const down = Math.max(0, -ay) / 30;
2577
- const squash = up * (NOD.eyeHeight.up - 1) + down * (NOD.eyeHeight.down - 1);
2578
- return [dx, nodDy(NOD.eye, ay, hh) + squash * (y - ye)];
2579
- };
2540
+ function runReach(runs, x, side) {
2541
+ const px = side > 0 ? x - 1 : x;
2542
+ for (let k = 0; k < runs.length; k += 2) {
2543
+ const [a, b] = [runs[k], runs[k + 1]];
2544
+ if (a <= px && px + 1 <= b) {
2545
+ return side > 0 ? { inward: x - a, outward: b - x } : { inward: b - x, outward: x - a };
2546
+ }
2547
+ }
2548
+ return { inward: 0, outward: 0 };
2580
2549
  }
2581
- function browField(m, box) {
2582
- const xb = cx(box);
2583
- return (_x, _y, ax, ay) => {
2584
- const s = Math.sign(ax);
2585
- const a = Math.abs(ax) / 30;
2586
- return [s * a * m.brow[sideOf(m, xb, s)], nodDy(NOD.brow, ay, m.frame.hh)];
2587
- };
2550
+ function filledProfile(rows) {
2551
+ if (rows === void 0 || !rows.some((v) => v > 0)) return null;
2552
+ const out = rows.slice();
2553
+ const first = out.findIndex((v) => v > 0);
2554
+ for (let i = 0; i < first; i++) out[i] = out[first];
2555
+ for (let i = first + 1; i < out.length; i++)
2556
+ if (!(out[i] > 0)) out[i] = out[i - 1];
2557
+ return out;
2588
2558
  }
2589
- function noseField(m) {
2590
- const at = m.noseAt;
2591
- const top = m.noseTopY ?? at.y;
2592
- return (x, y, ax, ay) => {
2593
- const s = Math.sign(ax);
2594
- const a = Math.abs(ax) / 30;
2595
- const [tx, ty] = spin(
2596
- x,
2597
- y,
2598
- at.x,
2599
- top,
2600
- s * a * m.tilt * NOSE_TILT_DEG * DEG
2601
- );
2602
- return [s * a * m.nose + tx, nodDy(NOD.nose, ay, m.frame.hh) + ty];
2603
- };
2559
+ var smoothProfile = (rows) => boxSmooth(rows, Math.max(1, Math.round(rows.length * 0.01)));
2560
+ var EAR_STEP = 0.03;
2561
+ var EAR_ROWS = 3;
2562
+ function findEars(raw, wMax, firstRow, lastRow, minRows, maxRows) {
2563
+ const step = Math.max(4, EAR_STEP * wMax);
2564
+ for (let t = Math.max(firstRow, EAR_ROWS); t < lastRow; t++) {
2565
+ if (raw[t] - raw[t - EAR_ROWS] < step) continue;
2566
+ for (let b = t + minRows; b <= Math.min(lastRow, t + maxRows); b++) {
2567
+ if (raw[b - EAR_ROWS] - raw[b] >= step) {
2568
+ return { top: t - EAR_ROWS, bottom: b };
2569
+ }
2570
+ }
2571
+ return void 0;
2572
+ }
2573
+ return void 0;
2604
2574
  }
2605
- function mouthField(m) {
2606
- const c = m.mouthAt;
2607
- return (x, y, ax, ay) => {
2608
- const s = Math.sign(ax);
2609
- const a = Math.abs(ax) / 30;
2610
- const wx = c.x + (x - c.x) * (1 + a * (m.mouthWidth - 1));
2611
- const [tx, ty] = spin(
2612
- wx,
2613
- y,
2614
- c.x,
2615
- c.y,
2616
- s * a * m.tilt * TURN.mouthTiltDeg * DEG
2617
- );
2618
- return [
2619
- s * a * m.mouth + wx - x + tx,
2620
- nodDy(NOD.mouth, ay, m.frame.hh) + ty
2621
- ];
2622
- };
2575
+ function boxSmooth(v, k) {
2576
+ return v.map((_, i) => {
2577
+ let s = 0;
2578
+ let n = 0;
2579
+ for (let j = i - k; j <= i + k; j++) {
2580
+ if (j < 0 || j >= v.length) continue;
2581
+ s += v[j];
2582
+ n++;
2583
+ }
2584
+ return s / n;
2585
+ });
2623
2586
  }
2624
- function hairField(m, shift, nod) {
2625
- const f = m.frame;
2626
- return (x, _y, ax, ay) => {
2627
- const s = Math.sign(ax);
2628
- const a = Math.abs(ax) / 30;
2629
- return [
2630
- s * a * shift + a * (m.shellScale - 1) * (x - f.axisX),
2631
- nodDy(nod, ay, f.hh)
2632
- ];
2633
- };
2587
+ var CUT_MARGIN = 2;
2588
+ var CUT_LOW = 3;
2589
+ function fadeToOutline(d, nk) {
2590
+ return Math.max(0, 1 - d / nk.waist);
2634
2591
  }
2635
- function hairFrontBends(frame, boxes) {
2636
- const f = frame;
2637
- const eyes = [boxes.eye_L, boxes.eye_R].filter(
2638
- (e) => e !== void 0
2639
- );
2640
- const inner = Math.max(
2641
- 1,
2642
- Math.min(
2643
- f.paintedHalfAt(f.eyeY),
2644
- ...eyes.map((e) => Math.max(f.axisX - e.x0, e.x1 - f.axisX))
2645
- )
2646
- );
2647
- return {
2648
- inner,
2649
- left: Math.max(inner + 1, f.axisX - f.shellLeft),
2650
- right: Math.max(inner + 1, f.shellRight - f.axisX)
2592
+ var STROKE_RUN = 4;
2593
+ var STROKE_SLACK = 6;
2594
+ var JAW_SHADE = 0.05;
2595
+ function buildHeadFrame(layers, opts) {
2596
+ const byRole = new Map(layers.map((l) => [l.role, l]));
2597
+ const faceLayer = byRole.get("face");
2598
+ const { canvasW, canvasH } = faceLayer;
2599
+ const face = boxOfLayer(faceLayer);
2600
+ const axisX = cx(face);
2601
+ const plateHalf = faceLayer.cropW / 2;
2602
+ const centreY = (role) => {
2603
+ const l = byRole.get(role);
2604
+ return l === void 0 ? void 0 : cy(boxOfLayer(l));
2651
2605
  };
2652
- }
2653
- function hairFrontField(m) {
2654
- const f = m.frame;
2655
- const back = hairField(m, m.hairBack, NOD.hairBack);
2656
- const top = f.face.y1;
2657
- const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
2658
- (e) => e !== void 0
2659
- );
2660
- const { inner, left, right } = hairFrontBends(f, m.boxes);
2661
- const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
2662
- const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
2663
- return (x, y, ax, ay) => {
2664
- const s = Math.sign(ax);
2665
- const a = Math.abs(ax) / 30;
2666
- const u = s * (x - f.axisX);
2667
- const across = Math.min(1, Math.max(0, (u + inner) / (2 * inner)));
2668
- const band = 1 - smoothstep(eyeH / 2, 1.5 * eyeH, Math.abs(y - eyeY));
2669
- const core = m.hairFront + (m.hairFar - m.hairFront) * across * band;
2670
- const out = Math.min(
2671
- 1,
2672
- Math.max(
2673
- 0,
2674
- (Math.abs(x - f.axisX) - inner) / ((x < f.axisX ? left : right) - inner)
2675
- )
2676
- );
2677
- const follow = core + (m.hairOuter - core) * out;
2678
- const rx = s * a * follow + a * (m.shellScale - 1) * (x - f.axisX);
2679
- const ry = nodDy(NOD.hairFront, ay, f.hh);
2680
- const [bx, by] = back(x, y, ax, ay);
2681
- const w = 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
2682
- return [bx + w * (rx - bx), by + w * (ry - by)];
2683
- };
2684
- }
2685
- function familyField(m, family) {
2686
- switch (family) {
2687
- case "face":
2688
- return faceField(m);
2689
- case "eye_L":
2690
- case "eye_R":
2691
- return eyeField(m, m.boxes[family]);
2692
- case "brow_L":
2693
- case "brow_R":
2694
- return browField(m, m.boxes[family]);
2695
- case "nose":
2696
- return noseField(m);
2697
- case "mouth":
2698
- return mouthField(m);
2699
- case "hair_front":
2700
- return hairFrontField(m);
2701
- case "hair_back":
2702
- return hairField(m, m.hairBack, NOD.hairBack);
2703
- case "body":
2704
- return () => [0, 0];
2705
- }
2706
- }
2707
-
2708
- // src/auto-rig/context.ts
2709
- function sidePair(byRole, a, b) {
2710
- const la = byRole.get(a);
2711
- const lb = byRole.get(b);
2712
- if (la === void 0 || lb === void 0) return void 0;
2713
- const [lo, hi] = [
2714
- { role: a, box: boxOfLayer(la) },
2715
- { role: b, box: boxOfLayer(lb) }
2716
- ].sort((p, q) => cx(p.box) - cx(q.box));
2717
- return { left: lo, right: hi };
2718
- }
2719
- function sideIrises(byRole) {
2720
- const pair = sidePair(byRole, "iris_L", "iris_R");
2721
- return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
2722
- }
2723
- function solveContext(frame, byRole, options, hasNose) {
2724
- const box = (role) => boxOfLayer(byRole.get(role));
2725
- const row = (s, strand2, sideSign) => {
2726
- if (strand2 !== void 0) {
2727
- const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
2728
- const hi = sideSign < 0 ? strand2.irisInner : strand2.irisOuter;
2729
- return { role: s.role, lo, hi, y: strand2.y };
2730
- }
2731
- const inset = bw(s.box) > 4 ? 1 : 0;
2732
- return {
2733
- role: s.role,
2734
- lo: s.box.x0 + inset,
2735
- hi: s.box.x1 - inset,
2736
- y: cy(s.box)
2737
- };
2738
- };
2739
- const eyes = sidePair(byRole, "eye_L", "eye_R");
2740
- const irises = sidePair(byRole, "iris_L", "iris_R");
2741
- const pair = irises ?? eyes;
2742
- const strand = irises ? options.strandEdges : void 0;
2743
- const left = row(pair.left, strand?.left, -1);
2744
- const right = row(pair.right, strand?.right, 1);
2745
- let candidates;
2746
- const edges = options.headEdges;
2747
- if (frame.hairShell && edges && edges.left.length > 0 && edges.right.length > 0) {
2748
- candidates = {
2749
- left: edges.left.map((e) => ({ role: e.role, x: e.x })),
2750
- // A right edge names its outermost pixel column; the boundary is one on.
2751
- right: edges.right.map((e) => ({ role: e.role, x: e.x + 1 }))
2752
- };
2753
- } else if (frame.hairShell) {
2754
- candidates = {
2755
- left: [{ role: "hair_back", x: frame.shellLeft }],
2756
- right: [{ role: "hair_back", x: frame.shellRight }]
2757
- };
2758
- } else {
2759
- const w = frame.paintedHalfAt(frame.eyeY);
2760
- candidates = {
2761
- left: [{ role: "face", x: frame.axisX - w }],
2762
- right: [{ role: "face", x: frame.axisX + w }]
2763
- };
2764
- }
2765
- const restHalf = (Math.max(...candidates.right.map((c) => c.x)) - Math.min(...candidates.left.map((c) => c.x))) / 2;
2766
- const nose = byRole.get("nose");
2767
- let noseAt;
2768
- let noseBox;
2769
- if (hasNose && nose !== void 0) {
2770
- noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
2771
- noseAt = { x: cx(noseBox), y: cy(noseBox) };
2772
- }
2773
- const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
2774
- const m = box("mouth");
2775
- return {
2776
- frame,
2777
- left,
2778
- right,
2779
- candidates,
2780
- restHalf,
2781
- strand,
2782
- hasHairFront: byRole.has("hair_front"),
2783
- eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
2784
- noseAt,
2785
- noseBox,
2786
- mouthAt: { x: cx(m), y: cy(m) },
2787
- mouthBox
2788
- };
2789
- }
2790
- function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
2791
- const analytic = /* @__PURE__ */ new Map();
2792
- const landed = /* @__PURE__ */ new Map();
2793
- const field = (role) => {
2794
- const family = roleSpec(role).family;
2795
- let f = analytic.get(family);
2796
- if (f === void 0) analytic.set(family, f = familyField(turn, family));
2797
- return f;
2798
- };
2799
- return (role, x, y, ax) => {
2800
- const family = roleSpec(role).family;
2801
- const part = parts.find((p) => p.id === role);
2802
- const g = grids.get(family);
2803
- const warp = part?.warps?.find((w) => w.parameter === import_format3.StandardParameter.AngleX);
2804
- if (part === void 0 || part.mesh === void 0 || !g && !warp) {
2805
- return x + field(role)(x, y, ax, 0)[0];
2806
- }
2807
- const box = {
2808
- x0: part.transform.x - part.width / 2,
2809
- x1: part.transform.x + part.width / 2,
2810
- y0: part.transform.y - part.height / 2,
2811
- y1: part.transform.y + part.height / 2
2812
- };
2813
- const rest = meshPoints(part.mesh, box);
2814
- const key = `${role}@${ax}`;
2815
- let xs = landed.get(key);
2816
- if (xs === void 0) {
2817
- if (g) {
2818
- const offsets = keyformAt(g.keyforms, ax);
2819
- xs = rest.map(([px, py]) => land(g.lattice, offsets, px, py)[0]);
2820
- } else {
2821
- const k = warp.keyforms;
2822
- const offsets = blendStops(
2823
- k.map((kf) => kf.value),
2824
- k.map((kf) => kf.offsets),
2825
- ax
2826
- );
2827
- xs = rest.map(([px], i) => px + offsets[2 * i] * bw(box));
2828
- }
2829
- landed.set(key, xs);
2830
- }
2831
- const idx = part.mesh.indices;
2832
- for (let t = from.get(role) ?? 0; t < idx.length; t += 3) {
2833
- const [a, b, c] = [idx[t], idx[t + 1], idx[t + 2]];
2834
- const [ax0, ay0] = rest[a];
2835
- const [bx0, by0] = rest[b];
2836
- const [cx0, cy0] = rest[c];
2837
- const d = (by0 - cy0) * (ax0 - cx0) + (cx0 - bx0) * (ay0 - cy0);
2838
- if (Math.abs(d) < 1e-9) continue;
2839
- const u = ((by0 - cy0) * (x - cx0) + (cx0 - bx0) * (y - cy0)) / d;
2840
- const v = ((cy0 - ay0) * (x - cx0) + (ax0 - cx0) * (y - cy0)) / d;
2841
- const w = 1 - u - v;
2842
- if (u < -1e-6 || v < -1e-6 || w < -1e-6) continue;
2843
- return u * xs[a] + v * xs[b] + w * xs[c];
2844
- }
2845
- return x + field(role)(x, y, ax, 0)[0];
2846
- };
2847
- }
2848
-
2849
- // src/auto-rig/head.ts
2850
- function filledProfile(rows) {
2851
- if (rows === void 0 || !rows.some((v) => v > 0)) return null;
2852
- const out = rows.slice();
2853
- const first = out.findIndex((v) => v > 0);
2854
- for (let i = 0; i < first; i++) out[i] = out[first];
2855
- for (let i = first + 1; i < out.length; i++)
2856
- if (!(out[i] > 0)) out[i] = out[i - 1];
2857
- return out;
2858
- }
2859
- var smoothProfile = (rows) => boxSmooth(rows, Math.max(1, Math.round(rows.length * 0.01)));
2860
- var EAR_STEP = 0.03;
2861
- var EAR_ROWS = 3;
2862
- function findEars(raw, wMax, firstRow, lastRow, minRows, maxRows) {
2863
- const step = Math.max(4, EAR_STEP * wMax);
2864
- for (let t = Math.max(firstRow, EAR_ROWS); t < lastRow; t++) {
2865
- if (raw[t] - raw[t - EAR_ROWS] < step) continue;
2866
- for (let b = t + minRows; b <= Math.min(lastRow, t + maxRows); b++) {
2867
- if (raw[b - EAR_ROWS] - raw[b] >= step) {
2868
- return { top: t - EAR_ROWS, bottom: b };
2869
- }
2870
- }
2871
- return void 0;
2872
- }
2873
- return void 0;
2874
- }
2875
- function boxSmooth(v, k) {
2876
- return v.map((_, i) => {
2877
- let s = 0;
2878
- let n = 0;
2879
- for (let j = i - k; j <= i + k; j++) {
2880
- if (j < 0 || j >= v.length) continue;
2881
- s += v[j];
2882
- n++;
2883
- }
2884
- return s / n;
2885
- });
2886
- }
2887
- var CUT_MARGIN = 2;
2888
- function shadeAcross(d, nk) {
2889
- return Math.max(0, 1 - d / nk.waist);
2890
- }
2891
- var STROKE_RUN = 4;
2892
- var STROKE_SLACK = 6;
2893
- var JAW_SHADE = 0.05;
2894
- function buildHeadFrame(layers, opts) {
2895
- const byRole = new Map(layers.map((l) => [l.role, l]));
2896
- const faceLayer = byRole.get("face");
2897
- const { canvasW, canvasH } = faceLayer;
2898
- const face = boxOfLayer(faceLayer);
2899
- const axisX = cx(face);
2900
- const plateHalf = faceLayer.cropW / 2;
2901
- const centreY = (role) => {
2902
- const l = byRole.get(role);
2903
- return l === void 0 ? void 0 : cy(boxOfLayer(l));
2904
- };
2905
- const pair = (a, b) => {
2906
- const ya = centreY(a);
2907
- const yb = centreY(b);
2908
- return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
2606
+ const pair = (a, b) => {
2607
+ const ya = centreY(a);
2608
+ const yb = centreY(b);
2609
+ return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
2909
2610
  };
2910
2611
  const eyeY = pair("iris_L", "iris_R") ?? pair("eye_L", "eye_R");
2911
2612
  const mouthY = centreY("mouth");
@@ -3043,20 +2744,30 @@ function buildHeadFrame(layers, opts) {
3043
2744
  };
3044
2745
  const chin = chinY;
3045
2746
  const shade = JAW_SHADE * HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chin);
2747
+ const reach = opts.chinSlide === void 0 ? nk.waist : clamp(
2748
+ nk.waist - opts.chinSlide - CUT_MARGIN - (CUT_LOW + 1),
2749
+ 0,
2750
+ nk.waist
2751
+ );
2752
+ let banded = -1;
3046
2753
  const cut = new Float64Array(faceLayer.cropW);
3047
2754
  for (let i = 0; i < faceLayer.cropW; i++) {
3048
2755
  const d = Math.abs(colX(i) - axisX);
3049
2756
  const stroke = d <= nk.half ? strokeAt(i) : void 0;
3050
- const y = stroke !== void 0 ? stroke - (shade + CUT_MARGIN) * shadeAcross(d, nk) : (d <= nk.half ? chin + (nk.topY - chin) * (d / Math.max(1, nk.half)) : outlineAt(d)) - CUT_MARGIN;
2757
+ const band = reach > 0 ? Math.max(0, 1 - d / reach) : 0;
2758
+ if (stroke !== void 0 && band > 0) banded = Math.max(banded, d);
2759
+ const y = stroke !== void 0 ? stroke - shade * band - CUT_MARGIN * fadeToOutline(d, nk) : (d <= nk.half ? chin + (nk.topY - chin) * (d / Math.max(1, nk.half)) : outlineAt(d)) - CUT_MARGIN;
3051
2760
  cut[i] = y >= face.y1 ? face.y1 : y;
3052
2761
  }
3053
2762
  const low = cut.map((_, i) => {
3054
2763
  let m = Infinity;
3055
- for (let j = i - 3; j <= i + 3; j++) {
2764
+ for (let j = i - CUT_LOW; j <= i + CUT_LOW; j++) {
3056
2765
  if (j >= 0 && j < cut.length) m = Math.min(m, cut[j]);
3057
2766
  }
3058
2767
  return m;
3059
2768
  });
2769
+ if (opts.chinSlide !== void 0 && banded >= 0)
2770
+ nk.bandEnd = banded + CUT_LOW + 1;
3060
2771
  cutAt = (x) => {
3061
2772
  const f = x - (faceLayer.bbox.x - canvasW / 2) - 0.5;
3062
2773
  const i = clamp(Math.floor(f), 0, low.length - 1);
@@ -3081,42 +2792,580 @@ function buildHeadFrame(layers, opts) {
3081
2792
  }
3082
2793
  }
3083
2794
  }
3084
- const hh = HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chinY);
3085
- const headHalf = opts.headHalfWidth;
3086
- const hairShell = headHalf !== void 0;
3087
- let shellLeft = axisX - plateHalf;
3088
- let shellRight = axisX + plateHalf;
3089
- if (hairShell) {
3090
- if (opts.headEdges && opts.headEdges.left.length > 0 && opts.headEdges.right.length > 0) {
3091
- shellLeft = Math.min(...opts.headEdges.left.map((e) => e.x));
3092
- shellRight = Math.max(...opts.headEdges.right.map((e) => e.x)) + 1;
3093
- } else {
3094
- shellLeft = axisX - headHalf;
3095
- shellRight = axisX + headHalf;
2795
+ const hh = HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chinY);
2796
+ const headHalf = opts.headHalfWidth;
2797
+ const hairShell = headHalf !== void 0;
2798
+ let shellLeft = axisX - plateHalf;
2799
+ let shellRight = axisX + plateHalf;
2800
+ if (hairShell) {
2801
+ if (opts.headEdges && opts.headEdges.left.length > 0 && opts.headEdges.right.length > 0) {
2802
+ shellLeft = Math.min(...opts.headEdges.left.map((e) => e.x));
2803
+ shellRight = Math.max(...opts.headEdges.right.map((e) => e.x)) + 1;
2804
+ } else {
2805
+ shellLeft = axisX - headHalf;
2806
+ shellRight = axisX + headHalf;
2807
+ }
2808
+ }
2809
+ const hairRuns = (role, y) => {
2810
+ const l = byRole.get(role);
2811
+ if (l?.rowRuns === void 0) return void 0;
2812
+ const runs = l.rowRuns[Math.floor(canvasH / 2 - y) - l.bbox.y];
2813
+ return runs === void 0 ? [] : runs.map((c) => c - canvasW / 2);
2814
+ };
2815
+ return {
2816
+ canvasW,
2817
+ canvasH,
2818
+ face,
2819
+ axisX,
2820
+ eyeY,
2821
+ mouthY,
2822
+ chinY,
2823
+ hh,
2824
+ paintedHalfAt,
2825
+ wMax,
2826
+ headMax,
2827
+ hasProfile: profile !== null,
2828
+ neck,
2829
+ ears,
2830
+ cutAt,
2831
+ cheekY: eyeY - 0.5 * (eyeY - chinY),
2832
+ jawY: eyeY - 0.8 * (eyeY - chinY),
2833
+ shellLeft,
2834
+ shellRight,
2835
+ hairShell,
2836
+ holdBase: headHalf ?? plateHalf,
2837
+ hairRuns
2838
+ };
2839
+ }
2840
+
2841
+ // src/auto-rig/fields.ts
2842
+ var NOSE_TILT_DEG = 6;
2843
+ function nodDy(v, ay, hh) {
2844
+ const up = Math.max(0, ay) / 30;
2845
+ const down = Math.max(0, -ay) / 30;
2846
+ return -(up * v.up + down * v.down) * hh;
2847
+ }
2848
+ function spin(x, y, ox, oy, a) {
2849
+ if (a === 0) return [0, 0];
2850
+ const px = x - ox;
2851
+ const py = y - oy;
2852
+ return [
2853
+ px * Math.cos(a) - py * Math.sin(a) - px,
2854
+ px * Math.sin(a) + py * Math.cos(a) - py
2855
+ ];
2856
+ }
2857
+ function faceField(m) {
2858
+ const f = m.frame;
2859
+ return (x, y, ax, ay) => {
2860
+ const s = Math.sign(ax);
2861
+ const a = Math.abs(ax) / 30;
2862
+ const chin = smoothstep(f.mouthY, f.chinY, y);
2863
+ const width = m.widthUpper + (m.widthJaw - m.widthUpper) * smoothstep(f.cheekY, f.jawY, y);
2864
+ const shell = f.hairShell ? 0 : m.shellScale - 1;
2865
+ const dx = s * a * (m.face + m.chinLead * chin) + a * (width - 1 + shell) * (x - f.axisX);
2866
+ const t = Math.min(
2867
+ 1,
2868
+ Math.max(0, (f.face.y1 - y) / Math.max(1, f.face.y1 - f.chinY))
2869
+ );
2870
+ const v = {
2871
+ up: NOD.faceTop.up + (NOD.chin.up - NOD.faceTop.up) * t,
2872
+ down: NOD.faceTop.down + (NOD.chin.down - NOD.faceTop.down) * t
2873
+ };
2874
+ return [dx, nodDy(v, ay, f.hh)];
2875
+ };
2876
+ }
2877
+ function earField(m) {
2878
+ const f = m.frame;
2879
+ const ears = f.ears;
2880
+ if (ears === void 0) return () => [0, 0];
2881
+ const face = faceField(m);
2882
+ return (x, y, ax, ay) => {
2883
+ const s = Math.sign(ax);
2884
+ const a = Math.abs(ax) / 30;
2885
+ const dy = face(x, y, 0, ay)[1];
2886
+ if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
2887
+ const d = Math.abs(x - f.axisX);
2888
+ const attach = ears.attachAt(y);
2889
+ if (d <= attach) return [face(x, y, ax, 0)[0], dy];
2890
+ const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
2891
+ const rim = s * a * TURN.earNear * m.face;
2892
+ const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
2893
+ return [root + (rim - root) * t, dy];
2894
+ };
2895
+ }
2896
+ function chinSlide(m) {
2897
+ const f = m.frame;
2898
+ return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
2899
+ }
2900
+ var NECK_SHADE_MAX = 0.45;
2901
+ var NECK_SHADE_FULL = 0.2;
2902
+ function neckField(m) {
2903
+ const f = m.frame;
2904
+ const nk = f.neck;
2905
+ if (nk === void 0) return () => [0, 0];
2906
+ const face = faceField(m);
2907
+ const full = chinSlide(m);
2908
+ const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
2909
+ const bottom = f.face.y0;
2910
+ const tip = f.cutAt(f.axisX);
2911
+ const top = tip - NECK_SHADE_FULL * (tip - bottom);
2912
+ return (x, y, ax) => {
2913
+ const wy = Math.min(
2914
+ 1,
2915
+ Math.max(0, (y - bottom) / Math.max(1, top - bottom))
2916
+ );
2917
+ const wx = Math.max(0, 1 - Math.abs(x - f.axisX) / nk.waist);
2918
+ return [share * face(f.axisX, f.chinY, ax, 0)[0] * wx * wy, 0];
2919
+ };
2920
+ }
2921
+ function sideOf(m, x, s) {
2922
+ return s * (x - m.frame.axisX) > 0 ? "far" : "near";
2923
+ }
2924
+ function eyeField(m, box) {
2925
+ const xe = cx(box);
2926
+ const ye = cy(box);
2927
+ const hh = m.frame.hh;
2928
+ return (x, y, ax, ay) => {
2929
+ const s = Math.sign(ax);
2930
+ const a = Math.abs(ax) / 30;
2931
+ const side = sideOf(m, xe, s);
2932
+ const dx = s * a * m.eye[side] + a * (m.eyeScale[side] - 1) * (x - xe);
2933
+ const up = Math.max(0, ay) / 30;
2934
+ const down = Math.max(0, -ay) / 30;
2935
+ const squash = up * (NOD.eyeHeight.up - 1) + down * (NOD.eyeHeight.down - 1);
2936
+ return [dx, nodDy(NOD.eye, ay, hh) + squash * (y - ye)];
2937
+ };
2938
+ }
2939
+ function browField(m, box) {
2940
+ const xb = cx(box);
2941
+ return (_x, _y, ax, ay) => {
2942
+ const s = Math.sign(ax);
2943
+ const a = Math.abs(ax) / 30;
2944
+ return [s * a * m.brow[sideOf(m, xb, s)], nodDy(NOD.brow, ay, m.frame.hh)];
2945
+ };
2946
+ }
2947
+ function noseField(m) {
2948
+ const at = m.noseAt;
2949
+ const top = m.noseTopY ?? at.y;
2950
+ return (x, y, ax, ay) => {
2951
+ const s = Math.sign(ax);
2952
+ const a = Math.abs(ax) / 30;
2953
+ const [tx, ty] = spin(
2954
+ x,
2955
+ y,
2956
+ at.x,
2957
+ top,
2958
+ s * a * m.tilt * NOSE_TILT_DEG * DEG
2959
+ );
2960
+ return [s * a * m.nose + tx, nodDy(NOD.nose, ay, m.frame.hh) + ty];
2961
+ };
2962
+ }
2963
+ function mouthField(m) {
2964
+ const c = m.mouthAt;
2965
+ return (x, y, ax, ay) => {
2966
+ const s = Math.sign(ax);
2967
+ const a = Math.abs(ax) / 30;
2968
+ const wx = c.x + (x - c.x) * (1 + a * (m.mouthWidth - 1));
2969
+ const [tx, ty] = spin(
2970
+ wx,
2971
+ y,
2972
+ c.x,
2973
+ c.y,
2974
+ s * a * m.tilt * TURN.mouthTiltDeg * DEG
2975
+ );
2976
+ return [
2977
+ s * a * m.mouth + wx - x + tx,
2978
+ nodDy(NOD.mouth, ay, m.frame.hh) + ty
2979
+ ];
2980
+ };
2981
+ }
2982
+ function hairField(m, shift, nod) {
2983
+ const f = m.frame;
2984
+ return (x, _y, ax, ay) => {
2985
+ const s = Math.sign(ax);
2986
+ const a = Math.abs(ax) / 30;
2987
+ return [
2988
+ s * a * shift + a * (m.shellScale - 1) * (x - f.axisX),
2989
+ nodDy(nod, ay, f.hh)
2990
+ ];
2991
+ };
2992
+ }
2993
+ function hairFrontBends(frame, boxes) {
2994
+ const f = frame;
2995
+ const eyes = [boxes.eye_L, boxes.eye_R].filter(
2996
+ (e) => e !== void 0
2997
+ );
2998
+ const inner = Math.max(
2999
+ 1,
3000
+ Math.min(
3001
+ f.paintedHalfAt(f.eyeY),
3002
+ ...eyes.map((e) => Math.max(f.axisX - e.x0, e.x1 - f.axisX))
3003
+ )
3004
+ );
3005
+ return {
3006
+ inner,
3007
+ left: Math.max(inner + 1, f.axisX - f.shellLeft),
3008
+ right: Math.max(inner + 1, f.shellRight - f.axisX)
3009
+ };
3010
+ }
3011
+ function hairFrontField(m) {
3012
+ const f = m.frame;
3013
+ const back = hairField(m, m.hairBack, NOD.hairBack);
3014
+ const cap = { up: NOD.hairFrontTop.up, down: capSlide(m) };
3015
+ const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
3016
+ (e) => e !== void 0
3017
+ );
3018
+ const { inner, left, right } = hairFrontBends(f, m.boxes);
3019
+ const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
3020
+ const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
3021
+ const coreAt = (x, y, s) => {
3022
+ const u = s * (x - f.axisX);
3023
+ const across = Math.min(1, Math.max(0, (u + inner) / (2 * inner)));
3024
+ const band = 1 - smoothstep(eyeH / 2, 1.5 * eyeH, Math.abs(y - eyeY));
3025
+ return m.hairFront + (m.hairFar - m.hairFront) * across * band;
3026
+ };
3027
+ const followAt = (x, y, s) => {
3028
+ const core = coreAt(x, y, s);
3029
+ const out = Math.min(
3030
+ 1,
3031
+ Math.max(
3032
+ 0,
3033
+ (Math.abs(x - f.axisX) - inner) / ((x < f.axisX ? left : right) - inner)
3034
+ )
3035
+ );
3036
+ return core + (m.hairOuter - core) * out;
3037
+ };
3038
+ const ride = outerRide(m, inner, coreAt, followAt);
3039
+ return (x, y, ax, ay) => {
3040
+ const s = Math.sign(ax);
3041
+ const a = Math.abs(ax) / 30;
3042
+ const follow = followAt(x, y, s) + (ride === void 0 ? 0 : ride(x, y, s));
3043
+ const rx = s * a * follow + a * (m.shellScale - 1) * (x - f.axisX);
3044
+ const ry = nodDy(NOD.hairFront, ay, f.hh);
3045
+ const bx = back(x, y, ax, ay)[0];
3046
+ const by = nodDy(cap, ay, f.hh);
3047
+ const w = crownBlend(m, y);
3048
+ return [bx + w * (rx - bx), by + w * (ry - by)];
3049
+ };
3050
+ }
3051
+ function crownBlend(m, y) {
3052
+ const top = m.frame.face.y1;
3053
+ return 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
3054
+ }
3055
+ var CAP_COVER_LOSS = 0.01;
3056
+ function capSlide(m) {
3057
+ const f = m.frame;
3058
+ const least = NOD.hairBack.down;
3059
+ const grid = m.hairFrontGrid;
3060
+ if (grid === void 0 || f.hairRuns("hair_front", grid.ys[0]) === void 0 || f.hairRuns("hair_back", grid.ys[0]) === void 0) {
3061
+ return least;
3062
+ }
3063
+ const { xs, ys } = grid;
3064
+ const top = ys[0];
3065
+ const n = ys.length;
3066
+ const bangs = NOD.hairFront.down;
3067
+ const wr = ys.map((y) => crownBlend(m, y));
3068
+ const blendAt = (y) => {
3069
+ const v = clamp((top - y) / (top - ys[n - 1]) * (n - 1), 0, n - 1);
3070
+ const r = Math.min(n - 2, Math.floor(v));
3071
+ return wr[r] + (wr[r + 1] - wr[r]) * (v - r);
3072
+ };
3073
+ const flat = ys[wr.findIndex((w) => w >= 1)] ?? ys[n - 1];
3074
+ const x0 = xs[0];
3075
+ const tops = new Float64Array(Math.round(xs[xs.length - 1] - x0)).fill(NaN);
3076
+ for (let i = 0; i < top - flat; i++) {
3077
+ const runs = f.hairRuns("hair_front", top - i - 0.5);
3078
+ for (let k = 0; k < runs.length; k += 2) {
3079
+ for (let px = runs[k]; px < runs[k + 1]; px++) {
3080
+ const j = Math.round(px - x0);
3081
+ if (Number.isNaN(tops[j])) tops[j] = i;
3082
+ }
3083
+ }
3084
+ }
3085
+ const hh = f.hh;
3086
+ const most = Math.ceil(Math.max(bangs, NOD.hairFrontTop.down) * hh);
3087
+ const bared = (s) => clamp(Math.ceil(s - 0.5), 0, most);
3088
+ const dropAt = (w, slide) => (w * bangs + (1 - w) * slide) * hh;
3089
+ const backNod = NOD.hairBack.down * hh;
3090
+ const backRows = /* @__PURE__ */ new Map();
3091
+ const columns = [];
3092
+ tops.forEach((i, j) => {
3093
+ if (Number.isNaN(i)) return;
3094
+ const w = blendAt(top - i);
3095
+ const empty = new Int32Array(most + 1);
3096
+ for (let r = 0; r < most; r++) {
3097
+ const at = Math.floor(i + r + 0.5 - backNod);
3098
+ let runs = backRows.get(at);
3099
+ if (runs === void 0) {
3100
+ runs = f.hairRuns("hair_back", top - at - 0.5);
3101
+ backRows.set(at, runs);
3102
+ }
3103
+ empty[r + 1] = empty[r] + (paints(runs, x0 + j) ? 0 : 1);
3104
+ }
3105
+ columns.push({ w, empty, base: empty[bared(dropAt(w, least))] });
3106
+ });
3107
+ const budget = CAP_COVER_LOSS * hh;
3108
+ const fits = (slide) => columns.every((c) => c.empty[bared(dropAt(c.w, slide))] - c.base <= budget);
3109
+ let lo = least;
3110
+ let hi = NOD.hairFrontTop.down;
3111
+ if (fits(hi)) return hi;
3112
+ for (let k = 0; k < 30; k++) {
3113
+ const mid = (lo + hi) / 2;
3114
+ if (fits(mid)) lo = mid;
3115
+ else hi = mid;
3116
+ }
3117
+ return lo;
3118
+ }
3119
+ function paints(runs, px) {
3120
+ for (let k = 0; k < runs.length; k += 2) {
3121
+ if (runs[k] <= px && px + 1 <= runs[k + 1]) return true;
3122
+ }
3123
+ return false;
3124
+ }
3125
+ function outerRide(m, inner, coreAt, followAt) {
3126
+ const f = m.frame;
3127
+ const ys = m.hairFrontGrid?.ys;
3128
+ if (ys === void 0) return void 0;
3129
+ const top = ys[0];
3130
+ if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
3131
+ return void 0;
3132
+ }
3133
+ const drift = Math.abs(m.hairBack);
3134
+ const sides = [-1, 1];
3135
+ const n = ys.length;
3136
+ const count = Math.round(top - ys[n - 1]);
3137
+ const below = (NOD.hairFront.down - NOD.hairBack.down) * f.hh;
3138
+ const above = (NOD.hairBack.up - NOD.hairFront.up) * f.hh;
3139
+ const j0 = Math.floor(-above);
3140
+ const backRows = Array.from(
3141
+ { length: Math.ceil(count + below) - j0 },
3142
+ (_, j) => f.hairRuns("hair_back", top - (j + j0) - 0.5)
3143
+ );
3144
+ const offset = sides.map(() => new Float64Array(count).fill(NaN));
3145
+ const far = sides.map(() => new Float64Array(count));
3146
+ const near = sides.map(() => new Float64Array(count));
3147
+ for (let i = 0; i < count; i++) {
3148
+ const y = top - i - 0.5;
3149
+ const front = f.hairRuns("hair_front", y);
3150
+ sides.forEach((side, k) => {
3151
+ const edge = outerEdge(front, f.axisX, side);
3152
+ if (edge === void 0) return;
3153
+ let outward = Infinity;
3154
+ let inward = Infinity;
3155
+ for (let j = Math.floor(i - above); j < Math.ceil(i + 1 + below); j++) {
3156
+ const c = runReach(backRows[j - j0], edge, side);
3157
+ outward = Math.min(outward, c.outward);
3158
+ inward = Math.min(inward, c.inward);
3159
+ }
3160
+ offset[k][i] = side * (edge - f.axisX);
3161
+ far[k][i] = m.shellScale * outward - drift;
3162
+ near[k][i] = m.shellScale * inward - drift;
3163
+ });
3164
+ }
3165
+ const end = new Float64Array(n * 2);
3166
+ const lift = new Float64Array(n * 4);
3167
+ for (let r = 0; r < n; r++) {
3168
+ const i0 = Math.max(0, Math.floor(top - ys[Math.max(0, r - 1)]));
3169
+ const i1 = Math.min(count, Math.ceil(top - ys[Math.min(n - 1, r + 1)]));
3170
+ sides.forEach((side, k) => {
3171
+ let d = Infinity;
3172
+ let allowFar = Infinity;
3173
+ let allowNear = Infinity;
3174
+ for (let i = i0; i < i1; i++) {
3175
+ if (Number.isNaN(offset[k][i])) continue;
3176
+ d = Math.min(d, offset[k][i]);
3177
+ allowFar = Math.min(allowFar, far[k][i]);
3178
+ allowNear = Math.min(allowNear, near[k][i]);
3179
+ }
3180
+ end[r * 2 + k] = d;
3181
+ if (d === Infinity || d <= inner) return;
3182
+ const x = f.axisX + side * d;
3183
+ sides.forEach((s, j) => {
3184
+ const allow = Math.min(
3185
+ coreAt(x, ys[r], s),
3186
+ s === side ? allowFar : allowNear
3187
+ );
3188
+ lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
3189
+ });
3190
+ });
3191
+ }
3192
+ const rowRide = (r, k, j, d) => {
3193
+ const v = lift[(r * 2 + k) * 2 + j];
3194
+ return v === 0 ? 0 : v * Math.min(1, (d - inner) / (end[r * 2 + k] - inner));
3195
+ };
3196
+ return (x, y, s) => {
3197
+ const d = Math.abs(x - f.axisX);
3198
+ if (s === 0 || d <= inner) return 0;
3199
+ const k = x < f.axisX ? 0 : 1;
3200
+ const j = s < 0 ? 0 : 1;
3201
+ let v = clamp((top - y) / (top - ys[n - 1]) * (n - 1), 0, n - 1);
3202
+ if (Math.abs(v - Math.round(v)) < 1e-3) v = Math.round(v);
3203
+ const r = Math.min(n - 2, Math.floor(v));
3204
+ const t = v - r;
3205
+ return rowRide(r, k, j, d) * (1 - t) + rowRide(r + 1, k, j, d) * t;
3206
+ };
3207
+ }
3208
+ function familyField(m, family) {
3209
+ switch (family) {
3210
+ case "face":
3211
+ return faceField(m);
3212
+ case "eye_L":
3213
+ case "eye_R":
3214
+ return eyeField(m, m.boxes[family]);
3215
+ case "brow_L":
3216
+ case "brow_R":
3217
+ return browField(m, m.boxes[family]);
3218
+ case "nose":
3219
+ return noseField(m);
3220
+ case "mouth":
3221
+ return mouthField(m);
3222
+ case "hair_front":
3223
+ return hairFrontField(m);
3224
+ case "hair_back":
3225
+ return hairField(m, m.hairBack, NOD.hairBack);
3226
+ case "body":
3227
+ return () => [0, 0];
3228
+ }
3229
+ }
3230
+
3231
+ // src/auto-rig/context.ts
3232
+ function sidePair(byRole, a, b) {
3233
+ const la = byRole.get(a);
3234
+ const lb = byRole.get(b);
3235
+ if (la === void 0 || lb === void 0) return void 0;
3236
+ const [lo, hi] = [
3237
+ { role: a, box: boxOfLayer(la) },
3238
+ { role: b, box: boxOfLayer(lb) }
3239
+ ].sort((p, q) => cx(p.box) - cx(q.box));
3240
+ return { left: lo, right: hi };
3241
+ }
3242
+ function sideIrises(byRole) {
3243
+ const pair = sidePair(byRole, "iris_L", "iris_R");
3244
+ return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
3245
+ }
3246
+ function solveContext(frame, byRole, options, hasNose) {
3247
+ const box = (role) => boxOfLayer(byRole.get(role));
3248
+ const row = (s, strand2, sideSign) => {
3249
+ if (strand2 !== void 0) {
3250
+ const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
3251
+ const hi = sideSign < 0 ? strand2.irisInner : strand2.irisOuter;
3252
+ return { role: s.role, lo, hi, y: strand2.y };
3096
3253
  }
3254
+ const inset = bw(s.box) > 4 ? 1 : 0;
3255
+ return {
3256
+ role: s.role,
3257
+ lo: s.box.x0 + inset,
3258
+ hi: s.box.x1 - inset,
3259
+ y: cy(s.box)
3260
+ };
3261
+ };
3262
+ const eyes = sidePair(byRole, "eye_L", "eye_R");
3263
+ const irises = sidePair(byRole, "iris_L", "iris_R");
3264
+ const pair = irises ?? eyes;
3265
+ const strand = irises ? options.strandEdges : void 0;
3266
+ const left = row(pair.left, strand?.left, -1);
3267
+ const right = row(pair.right, strand?.right, 1);
3268
+ let candidates;
3269
+ const edges = options.headEdges;
3270
+ if (frame.hairShell && edges && edges.left.length > 0 && edges.right.length > 0) {
3271
+ candidates = {
3272
+ left: edges.left.map((e) => ({ role: e.role, x: e.x })),
3273
+ // A right edge names its outermost pixel column; the boundary is one on.
3274
+ right: edges.right.map((e) => ({ role: e.role, x: e.x + 1 }))
3275
+ };
3276
+ } else if (frame.hairShell) {
3277
+ candidates = {
3278
+ left: [{ role: "hair_back", x: frame.shellLeft }],
3279
+ right: [{ role: "hair_back", x: frame.shellRight }]
3280
+ };
3281
+ } else {
3282
+ const w = frame.paintedHalfAt(frame.eyeY);
3283
+ candidates = {
3284
+ left: [{ role: "face", x: frame.axisX - w }],
3285
+ right: [{ role: "face", x: frame.axisX + w }]
3286
+ };
3287
+ }
3288
+ const restHalf = (Math.max(...candidates.right.map((c) => c.x)) - Math.min(...candidates.left.map((c) => c.x))) / 2;
3289
+ const nose = byRole.get("nose");
3290
+ let noseAt;
3291
+ let noseBox;
3292
+ if (hasNose && nose !== void 0) {
3293
+ noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
3294
+ noseAt = { x: cx(noseBox), y: cy(noseBox) };
3097
3295
  }
3296
+ const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
3297
+ const m = box("mouth");
3098
3298
  return {
3099
- canvasW,
3100
- canvasH,
3101
- face,
3102
- axisX,
3103
- eyeY,
3104
- mouthY,
3105
- chinY,
3106
- hh,
3107
- paintedHalfAt,
3108
- wMax,
3109
- headMax,
3110
- hasProfile: profile !== null,
3111
- neck,
3112
- ears,
3113
- cutAt,
3114
- cheekY: eyeY - 0.5 * (eyeY - chinY),
3115
- jawY: eyeY - 0.8 * (eyeY - chinY),
3116
- shellLeft,
3117
- shellRight,
3118
- hairShell,
3119
- holdBase: headHalf ?? plateHalf
3299
+ frame,
3300
+ left,
3301
+ right,
3302
+ candidates,
3303
+ restHalf,
3304
+ strand,
3305
+ hasHairFront: byRole.has("hair_front"),
3306
+ eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
3307
+ noseAt,
3308
+ noseBox,
3309
+ mouthAt: { x: cx(m), y: cy(m) },
3310
+ mouthBox
3311
+ };
3312
+ }
3313
+ function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
3314
+ const analytic = /* @__PURE__ */ new Map();
3315
+ const landed = /* @__PURE__ */ new Map();
3316
+ const field = (role) => {
3317
+ const family = roleSpec(role).family;
3318
+ let f = analytic.get(family);
3319
+ if (f === void 0) analytic.set(family, f = familyField(turn, family));
3320
+ return f;
3321
+ };
3322
+ return (role, x, y, ax) => {
3323
+ const family = roleSpec(role).family;
3324
+ const part = parts.find((p) => p.id === role);
3325
+ const g = grids.get(family);
3326
+ const warp = part?.warps?.find((w) => w.parameter === import_format3.StandardParameter.AngleX);
3327
+ if (part === void 0 || part.mesh === void 0 || !g && !warp) {
3328
+ return x + field(role)(x, y, ax, 0)[0];
3329
+ }
3330
+ const box = {
3331
+ x0: part.transform.x - part.width / 2,
3332
+ x1: part.transform.x + part.width / 2,
3333
+ y0: part.transform.y - part.height / 2,
3334
+ y1: part.transform.y + part.height / 2
3335
+ };
3336
+ const rest = meshPoints(part.mesh, box);
3337
+ const key = `${role}@${ax}`;
3338
+ let xs = landed.get(key);
3339
+ if (xs === void 0) {
3340
+ if (g) {
3341
+ const offsets = keyformAt(g.keyforms, ax);
3342
+ xs = rest.map(([px, py]) => land(g.lattice, offsets, px, py)[0]);
3343
+ } else {
3344
+ const k = warp.keyforms;
3345
+ const offsets = blendStops(
3346
+ k.map((kf) => kf.value),
3347
+ k.map((kf) => kf.offsets),
3348
+ ax
3349
+ );
3350
+ xs = rest.map(([px], i) => px + offsets[2 * i] * bw(box));
3351
+ }
3352
+ landed.set(key, xs);
3353
+ }
3354
+ const idx = part.mesh.indices;
3355
+ for (let t = from.get(role) ?? 0; t < idx.length; t += 3) {
3356
+ const [a, b, c] = [idx[t], idx[t + 1], idx[t + 2]];
3357
+ const [ax0, ay0] = rest[a];
3358
+ const [bx0, by0] = rest[b];
3359
+ const [cx0, cy0] = rest[c];
3360
+ const d = (by0 - cy0) * (ax0 - cx0) + (cx0 - bx0) * (ay0 - cy0);
3361
+ if (Math.abs(d) < 1e-9) continue;
3362
+ const u = ((by0 - cy0) * (x - cx0) + (cx0 - bx0) * (y - cy0)) / d;
3363
+ const v = ((cy0 - ay0) * (x - cx0) + (ax0 - cx0) * (y - cy0)) / d;
3364
+ const w = 1 - u - v;
3365
+ if (u < -1e-6 || v < -1e-6 || w < -1e-6) continue;
3366
+ return u * xs[a] + v * xs[b] + w * xs[c];
3367
+ }
3368
+ return x + field(role)(x, y, ax, 0)[0];
3120
3369
  };
3121
3370
  }
3122
3371
 
@@ -3193,12 +3442,20 @@ function buildFaceMesh(frame, b, head, neckCells) {
3193
3442
  frame.axisX - neck2.waist,
3194
3443
  frame.axisX + neck2.waist
3195
3444
  );
3445
+ if (neck2.bandEnd !== void 0)
3446
+ at.push(frame.axisX - neck2.bandEnd, frame.axisX + neck2.bandEnd);
3196
3447
  }
3448
+ const placed = /* @__PURE__ */ new Set();
3197
3449
  for (const k of at) {
3198
3450
  if (k <= x0 || k >= x1) continue;
3199
- const near = xs2.findIndex((x) => Math.abs(x - k) < 0.3 * step);
3200
- if (near >= 0 && near !== 0 && near !== xs2.length - 1) xs2[near] = k;
3201
- else if (near < 0) xs2.push(k);
3451
+ if ([...placed].some((i) => Math.abs(xs2[i] - k) < 0.5)) continue;
3452
+ const near = xs2.findIndex(
3453
+ (x, i) => !placed.has(i) && Math.abs(x - k) < 0.3 * step
3454
+ );
3455
+ if (near > 0 && near < n) {
3456
+ xs2[near] = k;
3457
+ placed.add(near);
3458
+ } else if (near < 0) placed.add(xs2.push(k) - 1);
3202
3459
  }
3203
3460
  return xs2.sort((a, b2) => a - b2);
3204
3461
  };
@@ -3214,7 +3471,7 @@ function buildFaceMesh(frame, b, head, neckCells) {
3214
3471
  );
3215
3472
  const sampleBelow = (x) => Math.max(
3216
3473
  1,
3217
- SAMPLE_BELOW * frame.hh * shadeAcross(Math.abs(x - frame.axisX), neck)
3474
+ SAMPLE_BELOW * frame.hh * fadeToOutline(Math.abs(x - frame.axisX), neck)
3218
3475
  );
3219
3476
  let highest = -Infinity;
3220
3477
  for (const x of xs2) {
@@ -3666,8 +3923,7 @@ function eyesInPlate(ctx, m) {
3666
3923
  function chinOverNeck(ctx, m) {
3667
3924
  const { frame } = ctx;
3668
3925
  if (frame.neck === void 0) return true;
3669
- const f = familyField(m, "face");
3670
- return Math.abs(f(frame.axisX, frame.chinY, 30, 0)[0]) <= frame.neck.half;
3926
+ return chinSlide(m) <= frame.neck.half;
3671
3927
  }
3672
3928
  function strandHeld(ctx, m) {
3673
3929
  const land2 = analyticLander(m);
@@ -3982,10 +4238,32 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
3982
4238
  checkedKeys("turnTargets", options.turnTargets, TURN_TARGET_KEYS);
3983
4239
  const targets = hasNose ? resolveTurnTargets(options.turnTargets, plateHalf) : { given: /* @__PURE__ */ new Set() };
3984
4240
  const turnOptions = hasNose ? options : {};
3985
- const frame = buildHeadFrame(layers, {
4241
+ const frameOptions = {
3986
4242
  headHalfWidth: targets.headHalfWidth,
3987
4243
  headEdges: targets.headHalfWidth !== void 0 ? options.headEdges : void 0
4244
+ };
4245
+ const solveFrame = buildHeadFrame(layers, frameOptions);
4246
+ const ctx = solveContext(solveFrame, byRole, turnOptions, hasNose);
4247
+ const boxes = Object.fromEntries(
4248
+ ["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
4249
+ );
4250
+ const landmarks = {
4251
+ boxes,
4252
+ noseAt: ctx.noseAt,
4253
+ noseTopY: ctx.noseBox?.y1,
4254
+ mouthAt: ctx.mouthAt,
4255
+ hairTop: has("hair_front") ? box("hair_front").y1 : solveFrame.face.y1,
4256
+ // The field reads the grid the mesh is built on (the rebuilt frame below
4257
+ // differs only in the jaw's cut, which the grid does not read).
4258
+ hairFrontGrid: has("hair_front") ? hairFrontGrid(box("hair_front"), solveFrame, boxes) : void 0
4259
+ };
4260
+ const build = (q) => buildTurn(solveFrame, landmarks, q);
4261
+ const fit = fitTurn(ctx, targets, style, build);
4262
+ const frame = buildHeadFrame(layers, {
4263
+ ...frameOptions,
4264
+ chinSlide: chinSlide(fit.model)
3988
4265
  });
4266
+ const turn = { ...fit.model, frame };
3989
4267
  const parts = [];
3990
4268
  const reach = /* @__PURE__ */ new Map();
3991
4269
  let regionOf = [];
@@ -3999,7 +4277,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
3999
4277
  parts.length,
4000
4278
  frame,
4001
4279
  byRole,
4002
- style
4280
+ style,
4281
+ landmarks.hairFrontGrid
4003
4282
  );
4004
4283
  if (built.region !== void 0) {
4005
4284
  regionOf = built.region;
@@ -4008,19 +4287,6 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
4008
4287
  reach.set(spec.role, built.reach);
4009
4288
  parts.push(built.part);
4010
4289
  }
4011
- const ctx = solveContext(frame, byRole, turnOptions, hasNose);
4012
- const landmarks = {
4013
- boxes: Object.fromEntries(
4014
- ["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
4015
- ),
4016
- noseAt: ctx.noseAt,
4017
- noseTopY: ctx.noseBox?.y1,
4018
- mouthAt: ctx.mouthAt,
4019
- hairTop: has("hair_front") ? box("hair_front").y1 : frame.face.y1
4020
- };
4021
- const build = (q) => buildTurn(frame, landmarks, q);
4022
- const fit = fitTurn(ctx, targets, style, build);
4023
- const turn = fit.model;
4024
4290
  const grids = /* @__PURE__ */ new Map();
4025
4291
  for (const family of Object.keys(WARP_ID)) {
4026
4292
  const members = parts.filter((p) => roleSpec(p.id).family === family);
@@ -4103,7 +4369,7 @@ function plateFields(turn, region, head) {
4103
4369
  const neck = neckField(turn);
4104
4370
  return (i) => region[i] === "ear" ? ear : region[i] === "neck" ? neck : head;
4105
4371
  }
4106
- function buildPart(spec, b, order, frame, byRole, style) {
4372
+ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
4107
4373
  const box = (role) => boxOfLayer(byRole.get(role));
4108
4374
  const part = {
4109
4375
  id: spec.role,
@@ -4126,30 +4392,15 @@ function buildPart(spec, b, order, frame, byRole, style) {
4126
4392
  mesh = fm.mesh;
4127
4393
  region = fm.region;
4128
4394
  headStart = fm.headStart;
4395
+ } else if (spec.role === "hair_front") {
4396
+ mesh = columnMesh(b, frontGrid);
4129
4397
  } else if (spec.family !== "body") {
4130
4398
  const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
4131
4399
  const px = Math.min(cells.px, meshScale(spec.role, frame));
4132
- const cols = cellsFor(bw(b), px, cells.min, cells.max);
4133
- const rows = cellsFor(bh(b), px, cells.min, cells.max);
4134
- if (spec.role === "hair_front") {
4135
- const boxes = Object.fromEntries(
4136
- ["eye_L", "eye_R"].filter((r) => byRole.has(r)).map((r) => [r, box(r)])
4137
- );
4138
- const { inner, left, right } = hairFrontBends(frame, boxes);
4139
- mesh = columnMesh(
4140
- b,
4141
- [
4142
- frame.axisX - left,
4143
- frame.axisX - inner,
4144
- frame.axisX + inner,
4145
- frame.axisX + right
4146
- ],
4147
- cols,
4148
- rows
4149
- );
4150
- } else {
4151
- mesh = gridMesh(cols, rows);
4152
- }
4400
+ mesh = gridMesh(
4401
+ cellsFor(bw(b), px, cells.min, cells.max),
4402
+ cellsFor(bh(b), px, cells.min, cells.max)
4403
+ );
4153
4404
  }
4154
4405
  if (mesh !== void 0) part.mesh = mesh;
4155
4406
  const side = spec.role.endsWith("_L") ? "L" : "R";
@@ -4268,16 +4519,35 @@ function buildPart(spec, b, order, frame, byRole, style) {
4268
4519
  headStart
4269
4520
  };
4270
4521
  }
4271
- function columnMesh(b, at, cols, rows) {
4522
+ function hairFrontGrid(b, frame, boxes) {
4523
+ const cells = MESH_CELLS.hair_front;
4524
+ const px = Math.min(cells.px, meshScale("hair_front", frame));
4525
+ const cols = cellsFor(bw(b), px, cells.min, cells.max);
4526
+ const rows = cellsFor(bh(b), px, cells.min, cells.max);
4527
+ const { inner, left, right } = hairFrontBends(frame, boxes);
4272
4528
  const step = bw(b) / cols;
4273
4529
  const xs = Array.from({ length: cols + 1 }, (_, c) => b.x0 + c * step);
4274
- for (const x of at) {
4530
+ for (const x of [
4531
+ frame.axisX - left,
4532
+ frame.axisX - inner,
4533
+ frame.axisX + inner,
4534
+ frame.axisX + right
4535
+ ]) {
4275
4536
  if (x <= b.x0 + 0.25 || x >= b.x1 - 0.25) continue;
4276
4537
  const near = xs.findIndex((v) => Math.abs(v - x) < 0.3 * step);
4277
4538
  if (near > 0 && near < xs.length - 1) xs[near] = x;
4278
4539
  else if (near < 0) xs.push(x);
4279
4540
  }
4280
4541
  xs.sort((p, q) => p - q);
4542
+ const ys = Array.from(
4543
+ { length: rows + 1 },
4544
+ (_, r) => b.y1 - bh(b) * r / rows
4545
+ );
4546
+ return { xs, ys };
4547
+ }
4548
+ function columnMesh(b, grid) {
4549
+ const { xs } = grid;
4550
+ const rows = grid.ys.length - 1;
4281
4551
  const vertices = [];
4282
4552
  const uvs = [];
4283
4553
  for (let r = 0; r <= rows; r++) {