@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.mjs CHANGED
@@ -1559,7 +1559,6 @@ function createLayerSetMeasurer(canvas) {
1559
1559
  const opaque = new Uint8Array(canvasW * canvasH);
1560
1560
  const layerInputs = [];
1561
1561
  const rowSpansByRole = [];
1562
- let hairFrontMask;
1563
1562
  function add({ role, fileName, rgba }) {
1564
1563
  if (rgba.length !== rgbaLength) {
1565
1564
  throw new Error(
@@ -1570,19 +1569,29 @@ function createLayerSetMeasurer(canvas) {
1570
1569
  if (bbox === null) return null;
1571
1570
  const rowLeft = new Int32Array(canvasH).fill(canvasW);
1572
1571
  const rowRight = new Int32Array(canvasH).fill(-1);
1573
- const ownMask = role === "hair_front" ? new Uint8Array(canvasW * canvasH) : void 0;
1572
+ const rowRuns = role === "hair_front" || role === "hair_back" ? [] : void 0;
1574
1573
  for (let y = 0; y < canvasH; y++) {
1574
+ let runs;
1575
+ if (rowRuns !== void 0 && y >= bbox.y && y < bbox.y + bbox.h) {
1576
+ runs = [];
1577
+ rowRuns.push(runs);
1578
+ }
1579
+ let inRun = false;
1575
1580
  for (let x = 0; x < canvasW; x++) {
1576
1581
  const p = y * canvasW + x;
1577
- if (rgba[p * 4 + 3] < ALPHA_OPAQUE) continue;
1582
+ const on = rgba[p * 4 + 3] >= ALPHA_OPAQUE;
1583
+ if (runs !== void 0 && on !== inRun) {
1584
+ runs.push(x);
1585
+ inRun = on;
1586
+ }
1587
+ if (!on) continue;
1578
1588
  opaque[p] = 1;
1579
- if (ownMask !== void 0) ownMask[p] = 1;
1580
1589
  if (x < rowLeft[y]) rowLeft[y] = x;
1581
1590
  if (x > rowRight[y]) rowRight[y] = x;
1582
1591
  }
1592
+ if (runs !== void 0 && inRun) runs.push(canvasW);
1583
1593
  }
1584
1594
  rowSpansByRole.push({ role, rowLeft, rowRight });
1585
- if (ownMask !== void 0) hairFrontMask = ownMask;
1586
1595
  const layer = {
1587
1596
  role,
1588
1597
  fileName,
@@ -1592,6 +1601,7 @@ function createLayerSetMeasurer(canvas) {
1592
1601
  cropW: bbox.w,
1593
1602
  cropH: bbox.h
1594
1603
  };
1604
+ if (rowRuns !== void 0) layer.rowRuns = rowRuns;
1595
1605
  if (role === "face") {
1596
1606
  const rowHalfWidths = [];
1597
1607
  for (let y = bbox.y; y < bbox.y + bbox.h; y++) {
@@ -1636,8 +1646,10 @@ function createLayerSetMeasurer(canvas) {
1636
1646
  }
1637
1647
  let strandEdges;
1638
1648
  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));
1639
- if (hairFrontMask !== void 0 && irisLayers.length === 2) {
1640
- const mask = hairFrontMask;
1649
+ const hairFront = layerInputs.find((l) => l.role === "hair_front");
1650
+ const hairRuns = hairFront?.rowRuns;
1651
+ if (hairFront !== void 0 && hairRuns !== void 0 && irisLayers.length === 2) {
1652
+ const hairTop = hairFront.bbox.y;
1641
1653
  const half = canvasW / 2;
1642
1654
  const strandOn = (iris, other, side) => {
1643
1655
  const r = Math.floor(iris.bbox.y + iris.bbox.h / 2);
@@ -1648,15 +1660,11 @@ function createLayerSetMeasurer(canvas) {
1648
1660
  if (span2.rowRight[r] < span2.rowLeft[r]) return void 0;
1649
1661
  const minRun = STRAND_MIN_RUN_FRACTION * (span2.rowRight[r] + 1 - span2.rowLeft[r]);
1650
1662
  const strand = new Uint8Array(canvasW);
1651
- for (let a = 0; a < canvasW; ) {
1652
- if (mask[r * canvasW + a] !== 1) {
1653
- a++;
1654
- continue;
1663
+ const runs = r >= hairTop && r < hairTop + hairRuns.length ? hairRuns[r - hairTop] : [];
1664
+ for (let k = 0; k < runs.length; k += 2) {
1665
+ if (runs[k + 1] - runs[k] >= minRun) {
1666
+ strand.fill(1, runs[k], runs[k + 1]);
1655
1667
  }
1656
- let b = a;
1657
- while (b < canvasW && mask[r * canvasW + b] === 1) b++;
1658
- if (b - a >= minRun) strand.fill(1, a, b);
1659
- a = b;
1660
1668
  }
1661
1669
  const strandAt = (x2) => x2 >= 0 && x2 < canvasW && strand[x2] === 1;
1662
1670
  const outerBoundary = (x2) => (side < 0 ? x2 : x2 + 1) - half;
@@ -2102,6 +2110,23 @@ function checkLayers(layers, canvas) {
2102
2110
  }
2103
2111
  });
2104
2112
  }
2113
+ if (l.rowRuns !== void 0) {
2114
+ const rows = l.rowRuns;
2115
+ if (!Array.isArray(rows) || rows.length !== h) {
2116
+ throw new Error(
2117
+ `${at}: rowRuns must have one entry per crop row (${h})`
2118
+ );
2119
+ }
2120
+ rows.forEach((runs, i) => {
2121
+ if (!Array.isArray(runs) || runs.length % 2 !== 0 || !runs.every(
2122
+ (c, k) => Number.isInteger(c) && c >= x && c <= x + w && (k === 0 || c > runs[k - 1])
2123
+ )) {
2124
+ throw new Error(
2125
+ `${at}: rowRuns[${i}] ${JSON.stringify(runs)} is not an even-length, strictly increasing list of columns inside its crop (${x}..${x + w})`
2126
+ );
2127
+ }
2128
+ });
2129
+ }
2105
2130
  if (l.jawRows !== void 0) {
2106
2131
  const cols = l.jawRows;
2107
2132
  if (!Array.isArray(cols) || cols.length !== w) {
@@ -2359,12 +2384,17 @@ var TURN = {
2359
2384
  * its translation (bangs 1.18×, side locks at the eye row ≈ 1.0×). */
2360
2385
  hairFollow: 1.1,
2361
2386
  /** The head's outline at the eye row holds (−0.014 / −0.004 hh): where the
2362
- * front hair draws it, its outer edge follows the face this much. */
2387
+ * front hair draws it, its outer edge follows the face this much on a row
2388
+ * where no back hair paints behind that edge. Where back hair does, the
2389
+ * edge rides further, as far as that back hair stays behind it through
2390
+ * the turn and the nod, up to the face's follow there (`hairFollow`;
2391
+ * over the far eye's rows, as far as that eye's outer corner goes). */
2363
2392
  outlineFollow: 0,
2364
2393
  /** The back hair's slight counter-motion. */
2365
2394
  hairBack: -0.027,
2366
2395
  /** The ears lag the plate: the far one moves this share of its slide, the
2367
- * near one this (the samples' parallax ratios, 0.44 and 0.87). */
2396
+ * near one this at its widest reach, its root riding the head (the
2397
+ * samples' parallax ratios, 0.44 and 0.87). */
2368
2398
  earFar: 0.44,
2369
2399
  earNear: 0.87
2370
2400
  };
@@ -2378,6 +2408,13 @@ var NOD = {
2378
2408
  nose: { up: -0.207, down: 0.193 },
2379
2409
  mouth: { up: -0.172, down: 0.177 },
2380
2410
  hairFront: { up: -0.11, down: 0.189 },
2411
+ /** The front hair's cap top. The samples' cap top moves 1.215× their face's
2412
+ * centroid looking down (Mao 1.236, Haru 1.626, Hiyori 1.194, Natori
2413
+ * 0.730) and 0.415× looking up (0.385, 0.430, 0.400, 0.487); each ratio
2414
+ * times the plate's own nod halfway between its top and the chin (0.1435
2415
+ * down, 0.084 up). Looking down, the rig slides it only as far as the
2416
+ * back hair covers the crown behind it. */
2417
+ hairFrontTop: { up: -0.035, down: 0.174 },
2381
2418
  hairBack: { up: -0.011, down: 0.025 }
2382
2419
  };
2383
2420
  var ROLL_DEG = 10;
@@ -2425,418 +2462,82 @@ function resolveStyle(style, check) {
2425
2462
  };
2426
2463
  }
2427
2464
 
2428
- // src/auto-rig/fields.ts
2429
- var NOSE_TILT_DEG = 6;
2430
- function nodDy(v, ay, hh) {
2431
- const up = Math.max(0, ay) / 30;
2432
- const down = Math.max(0, -ay) / 30;
2433
- return -(up * v.up + down * v.down) * hh;
2434
- }
2435
- function spin(x, y, ox, oy, a) {
2436
- if (a === 0) return [0, 0];
2437
- const px = x - ox;
2438
- const py = y - oy;
2439
- return [
2440
- px * Math.cos(a) - py * Math.sin(a) - px,
2441
- px * Math.sin(a) + py * Math.cos(a) - py
2442
- ];
2443
- }
2444
- function faceField(m) {
2445
- const f = m.frame;
2446
- return (x, y, ax, ay) => {
2447
- const s = Math.sign(ax);
2448
- const a = Math.abs(ax) / 30;
2449
- const chin = smoothstep(f.mouthY, f.chinY, y);
2450
- const width = m.widthUpper + (m.widthJaw - m.widthUpper) * smoothstep(f.cheekY, f.jawY, y);
2451
- const shell = f.hairShell ? 0 : m.shellScale - 1;
2452
- const dx = s * a * (m.face + m.chinLead * chin) + a * (width - 1 + shell) * (x - f.axisX);
2453
- const t = Math.min(
2454
- 1,
2455
- Math.max(0, (f.face.y1 - y) / Math.max(1, f.face.y1 - f.chinY))
2456
- );
2457
- const v = {
2458
- up: NOD.faceTop.up + (NOD.chin.up - NOD.faceTop.up) * t,
2459
- down: NOD.faceTop.down + (NOD.chin.down - NOD.faceTop.down) * t
2460
- };
2461
- return [dx, nodDy(v, ay, f.hh)];
2462
- };
2463
- }
2464
- function earField(m) {
2465
- const face = faceField(m);
2466
- return (x, y, ax, ay) => {
2467
- const s = Math.sign(ax);
2468
- const a = Math.abs(ax) / 30;
2469
- const share = sideOf(m, x, s) === "far" ? TURN.earFar : TURN.earNear;
2470
- return [s * a * share * m.face, face(x, y, 0, ay)[1]];
2471
- };
2472
- }
2473
- var NECK_SHADE_MAX = 0.45;
2474
- var NECK_SHADE_FULL = 0.2;
2475
- function neckField(m) {
2476
- const f = m.frame;
2477
- const nk = f.neck;
2478
- if (nk === void 0) return () => [0, 0];
2479
- const face = faceField(m);
2480
- const full = Math.abs(face(f.axisX, f.chinY, 30, 0)[0]);
2481
- const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
2482
- const bottom = f.face.y0;
2483
- const tip = f.cutAt(f.axisX);
2484
- const top = tip - NECK_SHADE_FULL * (tip - bottom);
2485
- return (x, y, ax) => {
2486
- const wy = Math.min(
2487
- 1,
2488
- Math.max(0, (y - bottom) / Math.max(1, top - bottom))
2489
- );
2490
- const wx = Math.max(0, 1 - Math.abs(x - f.axisX) / nk.waist);
2491
- return [share * face(f.axisX, f.chinY, ax, 0)[0] * wx * wy, 0];
2492
- };
2493
- }
2494
- function sideOf(m, x, s) {
2495
- return s * (x - m.frame.axisX) > 0 ? "far" : "near";
2465
+ // src/auto-rig/head.ts
2466
+ function outerEdge(runs, axisX, side) {
2467
+ if (runs.length === 0) return void 0;
2468
+ const x = side > 0 ? runs[runs.length - 1] : runs[0];
2469
+ return side * (x - axisX) > 0 ? x : void 0;
2496
2470
  }
2497
- function eyeField(m, box) {
2498
- const xe = cx(box);
2499
- const ye = cy(box);
2500
- const hh = m.frame.hh;
2501
- return (x, y, ax, ay) => {
2502
- const s = Math.sign(ax);
2503
- const a = Math.abs(ax) / 30;
2504
- const side = sideOf(m, xe, s);
2505
- const dx = s * a * m.eye[side] + a * (m.eyeScale[side] - 1) * (x - xe);
2506
- const up = Math.max(0, ay) / 30;
2507
- const down = Math.max(0, -ay) / 30;
2508
- const squash = up * (NOD.eyeHeight.up - 1) + down * (NOD.eyeHeight.down - 1);
2509
- return [dx, nodDy(NOD.eye, ay, hh) + squash * (y - ye)];
2510
- };
2471
+ function runReach(runs, x, side) {
2472
+ const px = side > 0 ? x - 1 : x;
2473
+ for (let k = 0; k < runs.length; k += 2) {
2474
+ const [a, b] = [runs[k], runs[k + 1]];
2475
+ if (a <= px && px + 1 <= b) {
2476
+ return side > 0 ? { inward: x - a, outward: b - x } : { inward: b - x, outward: x - a };
2477
+ }
2478
+ }
2479
+ return { inward: 0, outward: 0 };
2511
2480
  }
2512
- function browField(m, box) {
2513
- const xb = cx(box);
2514
- return (_x, _y, ax, ay) => {
2515
- const s = Math.sign(ax);
2516
- const a = Math.abs(ax) / 30;
2517
- return [s * a * m.brow[sideOf(m, xb, s)], nodDy(NOD.brow, ay, m.frame.hh)];
2518
- };
2481
+ function filledProfile(rows) {
2482
+ if (rows === void 0 || !rows.some((v) => v > 0)) return null;
2483
+ const out = rows.slice();
2484
+ const first = out.findIndex((v) => v > 0);
2485
+ for (let i = 0; i < first; i++) out[i] = out[first];
2486
+ for (let i = first + 1; i < out.length; i++)
2487
+ if (!(out[i] > 0)) out[i] = out[i - 1];
2488
+ return out;
2519
2489
  }
2520
- function noseField(m) {
2521
- const at = m.noseAt;
2522
- const top = m.noseTopY ?? at.y;
2523
- return (x, y, ax, ay) => {
2524
- const s = Math.sign(ax);
2525
- const a = Math.abs(ax) / 30;
2526
- const [tx, ty] = spin(
2527
- x,
2528
- y,
2529
- at.x,
2530
- top,
2531
- s * a * m.tilt * NOSE_TILT_DEG * DEG
2532
- );
2533
- return [s * a * m.nose + tx, nodDy(NOD.nose, ay, m.frame.hh) + ty];
2534
- };
2490
+ var smoothProfile = (rows) => boxSmooth(rows, Math.max(1, Math.round(rows.length * 0.01)));
2491
+ var EAR_STEP = 0.03;
2492
+ var EAR_ROWS = 3;
2493
+ function findEars(raw, wMax, firstRow, lastRow, minRows, maxRows) {
2494
+ const step = Math.max(4, EAR_STEP * wMax);
2495
+ for (let t = Math.max(firstRow, EAR_ROWS); t < lastRow; t++) {
2496
+ if (raw[t] - raw[t - EAR_ROWS] < step) continue;
2497
+ for (let b = t + minRows; b <= Math.min(lastRow, t + maxRows); b++) {
2498
+ if (raw[b - EAR_ROWS] - raw[b] >= step) {
2499
+ return { top: t - EAR_ROWS, bottom: b };
2500
+ }
2501
+ }
2502
+ return void 0;
2503
+ }
2504
+ return void 0;
2535
2505
  }
2536
- function mouthField(m) {
2537
- const c = m.mouthAt;
2538
- return (x, y, ax, ay) => {
2539
- const s = Math.sign(ax);
2540
- const a = Math.abs(ax) / 30;
2541
- const wx = c.x + (x - c.x) * (1 + a * (m.mouthWidth - 1));
2542
- const [tx, ty] = spin(
2543
- wx,
2544
- y,
2545
- c.x,
2546
- c.y,
2547
- s * a * m.tilt * TURN.mouthTiltDeg * DEG
2548
- );
2549
- return [
2550
- s * a * m.mouth + wx - x + tx,
2551
- nodDy(NOD.mouth, ay, m.frame.hh) + ty
2552
- ];
2553
- };
2506
+ function boxSmooth(v, k) {
2507
+ return v.map((_, i) => {
2508
+ let s = 0;
2509
+ let n = 0;
2510
+ for (let j = i - k; j <= i + k; j++) {
2511
+ if (j < 0 || j >= v.length) continue;
2512
+ s += v[j];
2513
+ n++;
2514
+ }
2515
+ return s / n;
2516
+ });
2554
2517
  }
2555
- function hairField(m, shift, nod) {
2556
- const f = m.frame;
2557
- return (x, _y, ax, ay) => {
2558
- const s = Math.sign(ax);
2559
- const a = Math.abs(ax) / 30;
2560
- return [
2561
- s * a * shift + a * (m.shellScale - 1) * (x - f.axisX),
2562
- nodDy(nod, ay, f.hh)
2563
- ];
2564
- };
2518
+ var CUT_MARGIN = 2;
2519
+ var CUT_LOW = 3;
2520
+ function fadeToOutline(d, nk) {
2521
+ return Math.max(0, 1 - d / nk.waist);
2565
2522
  }
2566
- function hairFrontBends(frame, boxes) {
2567
- const f = frame;
2568
- const eyes = [boxes.eye_L, boxes.eye_R].filter(
2569
- (e) => e !== void 0
2570
- );
2571
- const inner = Math.max(
2572
- 1,
2573
- Math.min(
2574
- f.paintedHalfAt(f.eyeY),
2575
- ...eyes.map((e) => Math.max(f.axisX - e.x0, e.x1 - f.axisX))
2576
- )
2577
- );
2578
- return {
2579
- inner,
2580
- left: Math.max(inner + 1, f.axisX - f.shellLeft),
2581
- right: Math.max(inner + 1, f.shellRight - f.axisX)
2523
+ var STROKE_RUN = 4;
2524
+ var STROKE_SLACK = 6;
2525
+ var JAW_SHADE = 0.05;
2526
+ function buildHeadFrame(layers, opts) {
2527
+ const byRole = new Map(layers.map((l) => [l.role, l]));
2528
+ const faceLayer = byRole.get("face");
2529
+ const { canvasW, canvasH } = faceLayer;
2530
+ const face = boxOfLayer(faceLayer);
2531
+ const axisX = cx(face);
2532
+ const plateHalf = faceLayer.cropW / 2;
2533
+ const centreY = (role) => {
2534
+ const l = byRole.get(role);
2535
+ return l === void 0 ? void 0 : cy(boxOfLayer(l));
2582
2536
  };
2583
- }
2584
- function hairFrontField(m) {
2585
- const f = m.frame;
2586
- const back = hairField(m, m.hairBack, NOD.hairBack);
2587
- const top = f.face.y1;
2588
- const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
2589
- (e) => e !== void 0
2590
- );
2591
- const { inner, left, right } = hairFrontBends(f, m.boxes);
2592
- const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
2593
- const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
2594
- return (x, y, ax, ay) => {
2595
- const s = Math.sign(ax);
2596
- const a = Math.abs(ax) / 30;
2597
- const u = s * (x - f.axisX);
2598
- const across = Math.min(1, Math.max(0, (u + inner) / (2 * inner)));
2599
- const band = 1 - smoothstep(eyeH / 2, 1.5 * eyeH, Math.abs(y - eyeY));
2600
- const core = m.hairFront + (m.hairFar - m.hairFront) * across * band;
2601
- const out = Math.min(
2602
- 1,
2603
- Math.max(
2604
- 0,
2605
- (Math.abs(x - f.axisX) - inner) / ((x < f.axisX ? left : right) - inner)
2606
- )
2607
- );
2608
- const follow = core + (m.hairOuter - core) * out;
2609
- const rx = s * a * follow + a * (m.shellScale - 1) * (x - f.axisX);
2610
- const ry = nodDy(NOD.hairFront, ay, f.hh);
2611
- const [bx, by] = back(x, y, ax, ay);
2612
- const w = 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
2613
- return [bx + w * (rx - bx), by + w * (ry - by)];
2614
- };
2615
- }
2616
- function familyField(m, family) {
2617
- switch (family) {
2618
- case "face":
2619
- return faceField(m);
2620
- case "eye_L":
2621
- case "eye_R":
2622
- return eyeField(m, m.boxes[family]);
2623
- case "brow_L":
2624
- case "brow_R":
2625
- return browField(m, m.boxes[family]);
2626
- case "nose":
2627
- return noseField(m);
2628
- case "mouth":
2629
- return mouthField(m);
2630
- case "hair_front":
2631
- return hairFrontField(m);
2632
- case "hair_back":
2633
- return hairField(m, m.hairBack, NOD.hairBack);
2634
- case "body":
2635
- return () => [0, 0];
2636
- }
2637
- }
2638
-
2639
- // src/auto-rig/context.ts
2640
- function sidePair(byRole, a, b) {
2641
- const la = byRole.get(a);
2642
- const lb = byRole.get(b);
2643
- if (la === void 0 || lb === void 0) return void 0;
2644
- const [lo, hi] = [
2645
- { role: a, box: boxOfLayer(la) },
2646
- { role: b, box: boxOfLayer(lb) }
2647
- ].sort((p, q) => cx(p.box) - cx(q.box));
2648
- return { left: lo, right: hi };
2649
- }
2650
- function sideIrises(byRole) {
2651
- const pair = sidePair(byRole, "iris_L", "iris_R");
2652
- return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
2653
- }
2654
- function solveContext(frame, byRole, options, hasNose) {
2655
- const box = (role) => boxOfLayer(byRole.get(role));
2656
- const row = (s, strand2, sideSign) => {
2657
- if (strand2 !== void 0) {
2658
- const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
2659
- const hi = sideSign < 0 ? strand2.irisInner : strand2.irisOuter;
2660
- return { role: s.role, lo, hi, y: strand2.y };
2661
- }
2662
- const inset = bw(s.box) > 4 ? 1 : 0;
2663
- return {
2664
- role: s.role,
2665
- lo: s.box.x0 + inset,
2666
- hi: s.box.x1 - inset,
2667
- y: cy(s.box)
2668
- };
2669
- };
2670
- const eyes = sidePair(byRole, "eye_L", "eye_R");
2671
- const irises = sidePair(byRole, "iris_L", "iris_R");
2672
- const pair = irises ?? eyes;
2673
- const strand = irises ? options.strandEdges : void 0;
2674
- const left = row(pair.left, strand?.left, -1);
2675
- const right = row(pair.right, strand?.right, 1);
2676
- let candidates;
2677
- const edges = options.headEdges;
2678
- if (frame.hairShell && edges && edges.left.length > 0 && edges.right.length > 0) {
2679
- candidates = {
2680
- left: edges.left.map((e) => ({ role: e.role, x: e.x })),
2681
- // A right edge names its outermost pixel column; the boundary is one on.
2682
- right: edges.right.map((e) => ({ role: e.role, x: e.x + 1 }))
2683
- };
2684
- } else if (frame.hairShell) {
2685
- candidates = {
2686
- left: [{ role: "hair_back", x: frame.shellLeft }],
2687
- right: [{ role: "hair_back", x: frame.shellRight }]
2688
- };
2689
- } else {
2690
- const w = frame.paintedHalfAt(frame.eyeY);
2691
- candidates = {
2692
- left: [{ role: "face", x: frame.axisX - w }],
2693
- right: [{ role: "face", x: frame.axisX + w }]
2694
- };
2695
- }
2696
- const restHalf = (Math.max(...candidates.right.map((c) => c.x)) - Math.min(...candidates.left.map((c) => c.x))) / 2;
2697
- const nose = byRole.get("nose");
2698
- let noseAt;
2699
- let noseBox;
2700
- if (hasNose && nose !== void 0) {
2701
- noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
2702
- noseAt = { x: cx(noseBox), y: cy(noseBox) };
2703
- }
2704
- const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
2705
- const m = box("mouth");
2706
- return {
2707
- frame,
2708
- left,
2709
- right,
2710
- candidates,
2711
- restHalf,
2712
- strand,
2713
- hasHairFront: byRole.has("hair_front"),
2714
- eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
2715
- noseAt,
2716
- noseBox,
2717
- mouthAt: { x: cx(m), y: cy(m) },
2718
- mouthBox
2719
- };
2720
- }
2721
- function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
2722
- const analytic = /* @__PURE__ */ new Map();
2723
- const landed = /* @__PURE__ */ new Map();
2724
- const field = (role) => {
2725
- const family = roleSpec(role).family;
2726
- let f = analytic.get(family);
2727
- if (f === void 0) analytic.set(family, f = familyField(turn, family));
2728
- return f;
2729
- };
2730
- return (role, x, y, ax) => {
2731
- const family = roleSpec(role).family;
2732
- const part = parts.find((p) => p.id === role);
2733
- const g = grids.get(family);
2734
- const warp = part?.warps?.find((w) => w.parameter === P.AngleX);
2735
- if (part === void 0 || part.mesh === void 0 || !g && !warp) {
2736
- return x + field(role)(x, y, ax, 0)[0];
2737
- }
2738
- const box = {
2739
- x0: part.transform.x - part.width / 2,
2740
- x1: part.transform.x + part.width / 2,
2741
- y0: part.transform.y - part.height / 2,
2742
- y1: part.transform.y + part.height / 2
2743
- };
2744
- const rest = meshPoints(part.mesh, box);
2745
- const key = `${role}@${ax}`;
2746
- let xs = landed.get(key);
2747
- if (xs === void 0) {
2748
- if (g) {
2749
- const offsets = keyformAt(g.keyforms, ax);
2750
- xs = rest.map(([px, py]) => land(g.lattice, offsets, px, py)[0]);
2751
- } else {
2752
- const k = warp.keyforms;
2753
- const offsets = blendStops(
2754
- k.map((kf) => kf.value),
2755
- k.map((kf) => kf.offsets),
2756
- ax
2757
- );
2758
- xs = rest.map(([px], i) => px + offsets[2 * i] * bw(box));
2759
- }
2760
- landed.set(key, xs);
2761
- }
2762
- const idx = part.mesh.indices;
2763
- for (let t = from.get(role) ?? 0; t < idx.length; t += 3) {
2764
- const [a, b, c] = [idx[t], idx[t + 1], idx[t + 2]];
2765
- const [ax0, ay0] = rest[a];
2766
- const [bx0, by0] = rest[b];
2767
- const [cx0, cy0] = rest[c];
2768
- const d = (by0 - cy0) * (ax0 - cx0) + (cx0 - bx0) * (ay0 - cy0);
2769
- if (Math.abs(d) < 1e-9) continue;
2770
- const u = ((by0 - cy0) * (x - cx0) + (cx0 - bx0) * (y - cy0)) / d;
2771
- const v = ((cy0 - ay0) * (x - cx0) + (ax0 - cx0) * (y - cy0)) / d;
2772
- const w = 1 - u - v;
2773
- if (u < -1e-6 || v < -1e-6 || w < -1e-6) continue;
2774
- return u * xs[a] + v * xs[b] + w * xs[c];
2775
- }
2776
- return x + field(role)(x, y, ax, 0)[0];
2777
- };
2778
- }
2779
-
2780
- // src/auto-rig/head.ts
2781
- function filledProfile(rows) {
2782
- if (rows === void 0 || !rows.some((v) => v > 0)) return null;
2783
- const out = rows.slice();
2784
- const first = out.findIndex((v) => v > 0);
2785
- for (let i = 0; i < first; i++) out[i] = out[first];
2786
- for (let i = first + 1; i < out.length; i++)
2787
- if (!(out[i] > 0)) out[i] = out[i - 1];
2788
- return out;
2789
- }
2790
- var smoothProfile = (rows) => boxSmooth(rows, Math.max(1, Math.round(rows.length * 0.01)));
2791
- var EAR_STEP = 0.03;
2792
- var EAR_ROWS = 3;
2793
- function findEars(raw, wMax, firstRow, lastRow, minRows, maxRows) {
2794
- const step = Math.max(4, EAR_STEP * wMax);
2795
- for (let t = Math.max(firstRow, EAR_ROWS); t < lastRow; t++) {
2796
- if (raw[t] - raw[t - EAR_ROWS] < step) continue;
2797
- for (let b = t + minRows; b <= Math.min(lastRow, t + maxRows); b++) {
2798
- if (raw[b - EAR_ROWS] - raw[b] >= step) {
2799
- return { top: t - EAR_ROWS, bottom: b };
2800
- }
2801
- }
2802
- return void 0;
2803
- }
2804
- return void 0;
2805
- }
2806
- function boxSmooth(v, k) {
2807
- return v.map((_, i) => {
2808
- let s = 0;
2809
- let n = 0;
2810
- for (let j = i - k; j <= i + k; j++) {
2811
- if (j < 0 || j >= v.length) continue;
2812
- s += v[j];
2813
- n++;
2814
- }
2815
- return s / n;
2816
- });
2817
- }
2818
- var CUT_MARGIN = 2;
2819
- function shadeAcross(d, nk) {
2820
- return Math.max(0, 1 - d / nk.waist);
2821
- }
2822
- var STROKE_RUN = 4;
2823
- var STROKE_SLACK = 6;
2824
- var JAW_SHADE = 0.05;
2825
- function buildHeadFrame(layers, opts) {
2826
- const byRole = new Map(layers.map((l) => [l.role, l]));
2827
- const faceLayer = byRole.get("face");
2828
- const { canvasW, canvasH } = faceLayer;
2829
- const face = boxOfLayer(faceLayer);
2830
- const axisX = cx(face);
2831
- const plateHalf = faceLayer.cropW / 2;
2832
- const centreY = (role) => {
2833
- const l = byRole.get(role);
2834
- return l === void 0 ? void 0 : cy(boxOfLayer(l));
2835
- };
2836
- const pair = (a, b) => {
2837
- const ya = centreY(a);
2838
- const yb = centreY(b);
2839
- return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
2537
+ const pair = (a, b) => {
2538
+ const ya = centreY(a);
2539
+ const yb = centreY(b);
2540
+ return ya === void 0 || yb === void 0 ? void 0 : (ya + yb) / 2;
2840
2541
  };
2841
2542
  const eyeY = pair("iris_L", "iris_R") ?? pair("eye_L", "eye_R");
2842
2543
  const mouthY = centreY("mouth");
@@ -2974,20 +2675,30 @@ function buildHeadFrame(layers, opts) {
2974
2675
  };
2975
2676
  const chin = chinY;
2976
2677
  const shade = JAW_SHADE * HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chin);
2678
+ const reach = opts.chinSlide === void 0 ? nk.waist : clamp(
2679
+ nk.waist - opts.chinSlide - CUT_MARGIN - (CUT_LOW + 1),
2680
+ 0,
2681
+ nk.waist
2682
+ );
2683
+ let banded = -1;
2977
2684
  const cut = new Float64Array(faceLayer.cropW);
2978
2685
  for (let i = 0; i < faceLayer.cropW; i++) {
2979
2686
  const d = Math.abs(colX(i) - axisX);
2980
2687
  const stroke = d <= nk.half ? strokeAt(i) : void 0;
2981
- 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;
2688
+ const band = reach > 0 ? Math.max(0, 1 - d / reach) : 0;
2689
+ if (stroke !== void 0 && band > 0) banded = Math.max(banded, d);
2690
+ 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;
2982
2691
  cut[i] = y >= face.y1 ? face.y1 : y;
2983
2692
  }
2984
2693
  const low = cut.map((_, i) => {
2985
2694
  let m = Infinity;
2986
- for (let j = i - 3; j <= i + 3; j++) {
2695
+ for (let j = i - CUT_LOW; j <= i + CUT_LOW; j++) {
2987
2696
  if (j >= 0 && j < cut.length) m = Math.min(m, cut[j]);
2988
2697
  }
2989
2698
  return m;
2990
2699
  });
2700
+ if (opts.chinSlide !== void 0 && banded >= 0)
2701
+ nk.bandEnd = banded + CUT_LOW + 1;
2991
2702
  cutAt = (x) => {
2992
2703
  const f = x - (faceLayer.bbox.x - canvasW / 2) - 0.5;
2993
2704
  const i = clamp(Math.floor(f), 0, low.length - 1);
@@ -3012,42 +2723,580 @@ function buildHeadFrame(layers, opts) {
3012
2723
  }
3013
2724
  }
3014
2725
  }
3015
- const hh = HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chinY);
3016
- const headHalf = opts.headHalfWidth;
3017
- const hairShell = headHalf !== void 0;
3018
- let shellLeft = axisX - plateHalf;
3019
- let shellRight = axisX + plateHalf;
3020
- if (hairShell) {
3021
- if (opts.headEdges && opts.headEdges.left.length > 0 && opts.headEdges.right.length > 0) {
3022
- shellLeft = Math.min(...opts.headEdges.left.map((e) => e.x));
3023
- shellRight = Math.max(...opts.headEdges.right.map((e) => e.x)) + 1;
3024
- } else {
3025
- shellLeft = axisX - headHalf;
3026
- shellRight = axisX + headHalf;
2726
+ const hh = HH_PER_EYE_TO_CHIN * Math.max(1, eyeY - chinY);
2727
+ const headHalf = opts.headHalfWidth;
2728
+ const hairShell = headHalf !== void 0;
2729
+ let shellLeft = axisX - plateHalf;
2730
+ let shellRight = axisX + plateHalf;
2731
+ if (hairShell) {
2732
+ if (opts.headEdges && opts.headEdges.left.length > 0 && opts.headEdges.right.length > 0) {
2733
+ shellLeft = Math.min(...opts.headEdges.left.map((e) => e.x));
2734
+ shellRight = Math.max(...opts.headEdges.right.map((e) => e.x)) + 1;
2735
+ } else {
2736
+ shellLeft = axisX - headHalf;
2737
+ shellRight = axisX + headHalf;
2738
+ }
2739
+ }
2740
+ const hairRuns = (role, y) => {
2741
+ const l = byRole.get(role);
2742
+ if (l?.rowRuns === void 0) return void 0;
2743
+ const runs = l.rowRuns[Math.floor(canvasH / 2 - y) - l.bbox.y];
2744
+ return runs === void 0 ? [] : runs.map((c) => c - canvasW / 2);
2745
+ };
2746
+ return {
2747
+ canvasW,
2748
+ canvasH,
2749
+ face,
2750
+ axisX,
2751
+ eyeY,
2752
+ mouthY,
2753
+ chinY,
2754
+ hh,
2755
+ paintedHalfAt,
2756
+ wMax,
2757
+ headMax,
2758
+ hasProfile: profile !== null,
2759
+ neck,
2760
+ ears,
2761
+ cutAt,
2762
+ cheekY: eyeY - 0.5 * (eyeY - chinY),
2763
+ jawY: eyeY - 0.8 * (eyeY - chinY),
2764
+ shellLeft,
2765
+ shellRight,
2766
+ hairShell,
2767
+ holdBase: headHalf ?? plateHalf,
2768
+ hairRuns
2769
+ };
2770
+ }
2771
+
2772
+ // src/auto-rig/fields.ts
2773
+ var NOSE_TILT_DEG = 6;
2774
+ function nodDy(v, ay, hh) {
2775
+ const up = Math.max(0, ay) / 30;
2776
+ const down = Math.max(0, -ay) / 30;
2777
+ return -(up * v.up + down * v.down) * hh;
2778
+ }
2779
+ function spin(x, y, ox, oy, a) {
2780
+ if (a === 0) return [0, 0];
2781
+ const px = x - ox;
2782
+ const py = y - oy;
2783
+ return [
2784
+ px * Math.cos(a) - py * Math.sin(a) - px,
2785
+ px * Math.sin(a) + py * Math.cos(a) - py
2786
+ ];
2787
+ }
2788
+ function faceField(m) {
2789
+ const f = m.frame;
2790
+ return (x, y, ax, ay) => {
2791
+ const s = Math.sign(ax);
2792
+ const a = Math.abs(ax) / 30;
2793
+ const chin = smoothstep(f.mouthY, f.chinY, y);
2794
+ const width = m.widthUpper + (m.widthJaw - m.widthUpper) * smoothstep(f.cheekY, f.jawY, y);
2795
+ const shell = f.hairShell ? 0 : m.shellScale - 1;
2796
+ const dx = s * a * (m.face + m.chinLead * chin) + a * (width - 1 + shell) * (x - f.axisX);
2797
+ const t = Math.min(
2798
+ 1,
2799
+ Math.max(0, (f.face.y1 - y) / Math.max(1, f.face.y1 - f.chinY))
2800
+ );
2801
+ const v = {
2802
+ up: NOD.faceTop.up + (NOD.chin.up - NOD.faceTop.up) * t,
2803
+ down: NOD.faceTop.down + (NOD.chin.down - NOD.faceTop.down) * t
2804
+ };
2805
+ return [dx, nodDy(v, ay, f.hh)];
2806
+ };
2807
+ }
2808
+ function earField(m) {
2809
+ const f = m.frame;
2810
+ const ears = f.ears;
2811
+ if (ears === void 0) return () => [0, 0];
2812
+ const face = faceField(m);
2813
+ return (x, y, ax, ay) => {
2814
+ const s = Math.sign(ax);
2815
+ const a = Math.abs(ax) / 30;
2816
+ const dy = face(x, y, 0, ay)[1];
2817
+ if (sideOf(m, x, s) === "far") return [s * a * TURN.earFar * m.face, dy];
2818
+ const d = Math.abs(x - f.axisX);
2819
+ const attach = ears.attachAt(y);
2820
+ if (d <= attach) return [face(x, y, ax, 0)[0], dy];
2821
+ const root = face(f.axisX + Math.sign(x - f.axisX) * attach, y, ax, 0)[0];
2822
+ const rim = s * a * TURN.earNear * m.face;
2823
+ const t = Math.min(1, (d - attach) / Math.max(1, ears.outer - attach));
2824
+ return [root + (rim - root) * t, dy];
2825
+ };
2826
+ }
2827
+ function chinSlide(m) {
2828
+ const f = m.frame;
2829
+ return Math.abs(faceField(m)(f.axisX, f.chinY, 30, 0)[0]);
2830
+ }
2831
+ var NECK_SHADE_MAX = 0.45;
2832
+ var NECK_SHADE_FULL = 0.2;
2833
+ function neckField(m) {
2834
+ const f = m.frame;
2835
+ const nk = f.neck;
2836
+ if (nk === void 0) return () => [0, 0];
2837
+ const face = faceField(m);
2838
+ const full = chinSlide(m);
2839
+ const share = Math.min(1, NECK_SHADE_MAX * nk.waist / Math.max(1e-6, full));
2840
+ const bottom = f.face.y0;
2841
+ const tip = f.cutAt(f.axisX);
2842
+ const top = tip - NECK_SHADE_FULL * (tip - bottom);
2843
+ return (x, y, ax) => {
2844
+ const wy = Math.min(
2845
+ 1,
2846
+ Math.max(0, (y - bottom) / Math.max(1, top - bottom))
2847
+ );
2848
+ const wx = Math.max(0, 1 - Math.abs(x - f.axisX) / nk.waist);
2849
+ return [share * face(f.axisX, f.chinY, ax, 0)[0] * wx * wy, 0];
2850
+ };
2851
+ }
2852
+ function sideOf(m, x, s) {
2853
+ return s * (x - m.frame.axisX) > 0 ? "far" : "near";
2854
+ }
2855
+ function eyeField(m, box) {
2856
+ const xe = cx(box);
2857
+ const ye = cy(box);
2858
+ const hh = m.frame.hh;
2859
+ return (x, y, ax, ay) => {
2860
+ const s = Math.sign(ax);
2861
+ const a = Math.abs(ax) / 30;
2862
+ const side = sideOf(m, xe, s);
2863
+ const dx = s * a * m.eye[side] + a * (m.eyeScale[side] - 1) * (x - xe);
2864
+ const up = Math.max(0, ay) / 30;
2865
+ const down = Math.max(0, -ay) / 30;
2866
+ const squash = up * (NOD.eyeHeight.up - 1) + down * (NOD.eyeHeight.down - 1);
2867
+ return [dx, nodDy(NOD.eye, ay, hh) + squash * (y - ye)];
2868
+ };
2869
+ }
2870
+ function browField(m, box) {
2871
+ const xb = cx(box);
2872
+ return (_x, _y, ax, ay) => {
2873
+ const s = Math.sign(ax);
2874
+ const a = Math.abs(ax) / 30;
2875
+ return [s * a * m.brow[sideOf(m, xb, s)], nodDy(NOD.brow, ay, m.frame.hh)];
2876
+ };
2877
+ }
2878
+ function noseField(m) {
2879
+ const at = m.noseAt;
2880
+ const top = m.noseTopY ?? at.y;
2881
+ return (x, y, ax, ay) => {
2882
+ const s = Math.sign(ax);
2883
+ const a = Math.abs(ax) / 30;
2884
+ const [tx, ty] = spin(
2885
+ x,
2886
+ y,
2887
+ at.x,
2888
+ top,
2889
+ s * a * m.tilt * NOSE_TILT_DEG * DEG
2890
+ );
2891
+ return [s * a * m.nose + tx, nodDy(NOD.nose, ay, m.frame.hh) + ty];
2892
+ };
2893
+ }
2894
+ function mouthField(m) {
2895
+ const c = m.mouthAt;
2896
+ return (x, y, ax, ay) => {
2897
+ const s = Math.sign(ax);
2898
+ const a = Math.abs(ax) / 30;
2899
+ const wx = c.x + (x - c.x) * (1 + a * (m.mouthWidth - 1));
2900
+ const [tx, ty] = spin(
2901
+ wx,
2902
+ y,
2903
+ c.x,
2904
+ c.y,
2905
+ s * a * m.tilt * TURN.mouthTiltDeg * DEG
2906
+ );
2907
+ return [
2908
+ s * a * m.mouth + wx - x + tx,
2909
+ nodDy(NOD.mouth, ay, m.frame.hh) + ty
2910
+ ];
2911
+ };
2912
+ }
2913
+ function hairField(m, shift, nod) {
2914
+ const f = m.frame;
2915
+ return (x, _y, ax, ay) => {
2916
+ const s = Math.sign(ax);
2917
+ const a = Math.abs(ax) / 30;
2918
+ return [
2919
+ s * a * shift + a * (m.shellScale - 1) * (x - f.axisX),
2920
+ nodDy(nod, ay, f.hh)
2921
+ ];
2922
+ };
2923
+ }
2924
+ function hairFrontBends(frame, boxes) {
2925
+ const f = frame;
2926
+ const eyes = [boxes.eye_L, boxes.eye_R].filter(
2927
+ (e) => e !== void 0
2928
+ );
2929
+ const inner = Math.max(
2930
+ 1,
2931
+ Math.min(
2932
+ f.paintedHalfAt(f.eyeY),
2933
+ ...eyes.map((e) => Math.max(f.axisX - e.x0, e.x1 - f.axisX))
2934
+ )
2935
+ );
2936
+ return {
2937
+ inner,
2938
+ left: Math.max(inner + 1, f.axisX - f.shellLeft),
2939
+ right: Math.max(inner + 1, f.shellRight - f.axisX)
2940
+ };
2941
+ }
2942
+ function hairFrontField(m) {
2943
+ const f = m.frame;
2944
+ const back = hairField(m, m.hairBack, NOD.hairBack);
2945
+ const cap = { up: NOD.hairFrontTop.up, down: capSlide(m) };
2946
+ const eyes = [m.boxes.eye_L, m.boxes.eye_R].filter(
2947
+ (e) => e !== void 0
2948
+ );
2949
+ const { inner, left, right } = hairFrontBends(f, m.boxes);
2950
+ const eyeY = eyes.length > 0 ? eyes.reduce((t, e) => t + cy(e), 0) / eyes.length : f.eyeY;
2951
+ const eyeH = eyes.length > 0 ? eyes.reduce((t, e) => t + (e.y1 - e.y0), 0) / eyes.length : 1;
2952
+ const coreAt = (x, y, s) => {
2953
+ const u = s * (x - f.axisX);
2954
+ const across = Math.min(1, Math.max(0, (u + inner) / (2 * inner)));
2955
+ const band = 1 - smoothstep(eyeH / 2, 1.5 * eyeH, Math.abs(y - eyeY));
2956
+ return m.hairFront + (m.hairFar - m.hairFront) * across * band;
2957
+ };
2958
+ const followAt = (x, y, s) => {
2959
+ const core = coreAt(x, y, s);
2960
+ const out = Math.min(
2961
+ 1,
2962
+ Math.max(
2963
+ 0,
2964
+ (Math.abs(x - f.axisX) - inner) / ((x < f.axisX ? left : right) - inner)
2965
+ )
2966
+ );
2967
+ return core + (m.hairOuter - core) * out;
2968
+ };
2969
+ const ride = outerRide(m, inner, coreAt, followAt);
2970
+ return (x, y, ax, ay) => {
2971
+ const s = Math.sign(ax);
2972
+ const a = Math.abs(ax) / 30;
2973
+ const follow = followAt(x, y, s) + (ride === void 0 ? 0 : ride(x, y, s));
2974
+ const rx = s * a * follow + a * (m.shellScale - 1) * (x - f.axisX);
2975
+ const ry = nodDy(NOD.hairFront, ay, f.hh);
2976
+ const bx = back(x, y, ax, ay)[0];
2977
+ const by = nodDy(cap, ay, f.hh);
2978
+ const w = crownBlend(m, y);
2979
+ return [bx + w * (rx - bx), by + w * (ry - by)];
2980
+ };
2981
+ }
2982
+ function crownBlend(m, y) {
2983
+ const top = m.frame.face.y1;
2984
+ return 1 - smoothstep(top, Math.max(top + 1, m.hairTop), y);
2985
+ }
2986
+ var CAP_COVER_LOSS = 0.01;
2987
+ function capSlide(m) {
2988
+ const f = m.frame;
2989
+ const least = NOD.hairBack.down;
2990
+ const grid = m.hairFrontGrid;
2991
+ if (grid === void 0 || f.hairRuns("hair_front", grid.ys[0]) === void 0 || f.hairRuns("hair_back", grid.ys[0]) === void 0) {
2992
+ return least;
2993
+ }
2994
+ const { xs, ys } = grid;
2995
+ const top = ys[0];
2996
+ const n = ys.length;
2997
+ const bangs = NOD.hairFront.down;
2998
+ const wr = ys.map((y) => crownBlend(m, y));
2999
+ const blendAt = (y) => {
3000
+ const v = clamp((top - y) / (top - ys[n - 1]) * (n - 1), 0, n - 1);
3001
+ const r = Math.min(n - 2, Math.floor(v));
3002
+ return wr[r] + (wr[r + 1] - wr[r]) * (v - r);
3003
+ };
3004
+ const flat = ys[wr.findIndex((w) => w >= 1)] ?? ys[n - 1];
3005
+ const x0 = xs[0];
3006
+ const tops = new Float64Array(Math.round(xs[xs.length - 1] - x0)).fill(NaN);
3007
+ for (let i = 0; i < top - flat; i++) {
3008
+ const runs = f.hairRuns("hair_front", top - i - 0.5);
3009
+ for (let k = 0; k < runs.length; k += 2) {
3010
+ for (let px = runs[k]; px < runs[k + 1]; px++) {
3011
+ const j = Math.round(px - x0);
3012
+ if (Number.isNaN(tops[j])) tops[j] = i;
3013
+ }
3014
+ }
3015
+ }
3016
+ const hh = f.hh;
3017
+ const most = Math.ceil(Math.max(bangs, NOD.hairFrontTop.down) * hh);
3018
+ const bared = (s) => clamp(Math.ceil(s - 0.5), 0, most);
3019
+ const dropAt = (w, slide) => (w * bangs + (1 - w) * slide) * hh;
3020
+ const backNod = NOD.hairBack.down * hh;
3021
+ const backRows = /* @__PURE__ */ new Map();
3022
+ const columns = [];
3023
+ tops.forEach((i, j) => {
3024
+ if (Number.isNaN(i)) return;
3025
+ const w = blendAt(top - i);
3026
+ const empty = new Int32Array(most + 1);
3027
+ for (let r = 0; r < most; r++) {
3028
+ const at = Math.floor(i + r + 0.5 - backNod);
3029
+ let runs = backRows.get(at);
3030
+ if (runs === void 0) {
3031
+ runs = f.hairRuns("hair_back", top - at - 0.5);
3032
+ backRows.set(at, runs);
3033
+ }
3034
+ empty[r + 1] = empty[r] + (paints(runs, x0 + j) ? 0 : 1);
3035
+ }
3036
+ columns.push({ w, empty, base: empty[bared(dropAt(w, least))] });
3037
+ });
3038
+ const budget = CAP_COVER_LOSS * hh;
3039
+ const fits = (slide) => columns.every((c) => c.empty[bared(dropAt(c.w, slide))] - c.base <= budget);
3040
+ let lo = least;
3041
+ let hi = NOD.hairFrontTop.down;
3042
+ if (fits(hi)) return hi;
3043
+ for (let k = 0; k < 30; k++) {
3044
+ const mid = (lo + hi) / 2;
3045
+ if (fits(mid)) lo = mid;
3046
+ else hi = mid;
3047
+ }
3048
+ return lo;
3049
+ }
3050
+ function paints(runs, px) {
3051
+ for (let k = 0; k < runs.length; k += 2) {
3052
+ if (runs[k] <= px && px + 1 <= runs[k + 1]) return true;
3053
+ }
3054
+ return false;
3055
+ }
3056
+ function outerRide(m, inner, coreAt, followAt) {
3057
+ const f = m.frame;
3058
+ const ys = m.hairFrontGrid?.ys;
3059
+ if (ys === void 0) return void 0;
3060
+ const top = ys[0];
3061
+ if (f.hairRuns("hair_front", top) === void 0 || f.hairRuns("hair_back", top) === void 0) {
3062
+ return void 0;
3063
+ }
3064
+ const drift = Math.abs(m.hairBack);
3065
+ const sides = [-1, 1];
3066
+ const n = ys.length;
3067
+ const count = Math.round(top - ys[n - 1]);
3068
+ const below = (NOD.hairFront.down - NOD.hairBack.down) * f.hh;
3069
+ const above = (NOD.hairBack.up - NOD.hairFront.up) * f.hh;
3070
+ const j0 = Math.floor(-above);
3071
+ const backRows = Array.from(
3072
+ { length: Math.ceil(count + below) - j0 },
3073
+ (_, j) => f.hairRuns("hair_back", top - (j + j0) - 0.5)
3074
+ );
3075
+ const offset = sides.map(() => new Float64Array(count).fill(NaN));
3076
+ const far = sides.map(() => new Float64Array(count));
3077
+ const near = sides.map(() => new Float64Array(count));
3078
+ for (let i = 0; i < count; i++) {
3079
+ const y = top - i - 0.5;
3080
+ const front = f.hairRuns("hair_front", y);
3081
+ sides.forEach((side, k) => {
3082
+ const edge = outerEdge(front, f.axisX, side);
3083
+ if (edge === void 0) return;
3084
+ let outward = Infinity;
3085
+ let inward = Infinity;
3086
+ for (let j = Math.floor(i - above); j < Math.ceil(i + 1 + below); j++) {
3087
+ const c = runReach(backRows[j - j0], edge, side);
3088
+ outward = Math.min(outward, c.outward);
3089
+ inward = Math.min(inward, c.inward);
3090
+ }
3091
+ offset[k][i] = side * (edge - f.axisX);
3092
+ far[k][i] = m.shellScale * outward - drift;
3093
+ near[k][i] = m.shellScale * inward - drift;
3094
+ });
3095
+ }
3096
+ const end = new Float64Array(n * 2);
3097
+ const lift = new Float64Array(n * 4);
3098
+ for (let r = 0; r < n; r++) {
3099
+ const i0 = Math.max(0, Math.floor(top - ys[Math.max(0, r - 1)]));
3100
+ const i1 = Math.min(count, Math.ceil(top - ys[Math.min(n - 1, r + 1)]));
3101
+ sides.forEach((side, k) => {
3102
+ let d = Infinity;
3103
+ let allowFar = Infinity;
3104
+ let allowNear = Infinity;
3105
+ for (let i = i0; i < i1; i++) {
3106
+ if (Number.isNaN(offset[k][i])) continue;
3107
+ d = Math.min(d, offset[k][i]);
3108
+ allowFar = Math.min(allowFar, far[k][i]);
3109
+ allowNear = Math.min(allowNear, near[k][i]);
3110
+ }
3111
+ end[r * 2 + k] = d;
3112
+ if (d === Infinity || d <= inner) return;
3113
+ const x = f.axisX + side * d;
3114
+ sides.forEach((s, j) => {
3115
+ const allow = Math.min(
3116
+ coreAt(x, ys[r], s),
3117
+ s === side ? allowFar : allowNear
3118
+ );
3119
+ lift[(r * 2 + k) * 2 + j] = Math.max(0, allow - followAt(x, ys[r], s));
3120
+ });
3121
+ });
3122
+ }
3123
+ const rowRide = (r, k, j, d) => {
3124
+ const v = lift[(r * 2 + k) * 2 + j];
3125
+ return v === 0 ? 0 : v * Math.min(1, (d - inner) / (end[r * 2 + k] - inner));
3126
+ };
3127
+ return (x, y, s) => {
3128
+ const d = Math.abs(x - f.axisX);
3129
+ if (s === 0 || d <= inner) return 0;
3130
+ const k = x < f.axisX ? 0 : 1;
3131
+ const j = s < 0 ? 0 : 1;
3132
+ let v = clamp((top - y) / (top - ys[n - 1]) * (n - 1), 0, n - 1);
3133
+ if (Math.abs(v - Math.round(v)) < 1e-3) v = Math.round(v);
3134
+ const r = Math.min(n - 2, Math.floor(v));
3135
+ const t = v - r;
3136
+ return rowRide(r, k, j, d) * (1 - t) + rowRide(r + 1, k, j, d) * t;
3137
+ };
3138
+ }
3139
+ function familyField(m, family) {
3140
+ switch (family) {
3141
+ case "face":
3142
+ return faceField(m);
3143
+ case "eye_L":
3144
+ case "eye_R":
3145
+ return eyeField(m, m.boxes[family]);
3146
+ case "brow_L":
3147
+ case "brow_R":
3148
+ return browField(m, m.boxes[family]);
3149
+ case "nose":
3150
+ return noseField(m);
3151
+ case "mouth":
3152
+ return mouthField(m);
3153
+ case "hair_front":
3154
+ return hairFrontField(m);
3155
+ case "hair_back":
3156
+ return hairField(m, m.hairBack, NOD.hairBack);
3157
+ case "body":
3158
+ return () => [0, 0];
3159
+ }
3160
+ }
3161
+
3162
+ // src/auto-rig/context.ts
3163
+ function sidePair(byRole, a, b) {
3164
+ const la = byRole.get(a);
3165
+ const lb = byRole.get(b);
3166
+ if (la === void 0 || lb === void 0) return void 0;
3167
+ const [lo, hi] = [
3168
+ { role: a, box: boxOfLayer(la) },
3169
+ { role: b, box: boxOfLayer(lb) }
3170
+ ].sort((p, q) => cx(p.box) - cx(q.box));
3171
+ return { left: lo, right: hi };
3172
+ }
3173
+ function sideIrises(byRole) {
3174
+ const pair = sidePair(byRole, "iris_L", "iris_R");
3175
+ return pair === void 0 ? {} : { left: pair.left.box, right: pair.right.box };
3176
+ }
3177
+ function solveContext(frame, byRole, options, hasNose) {
3178
+ const box = (role) => boxOfLayer(byRole.get(role));
3179
+ const row = (s, strand2, sideSign) => {
3180
+ if (strand2 !== void 0) {
3181
+ const lo = sideSign < 0 ? strand2.irisOuter : strand2.irisInner;
3182
+ const hi = sideSign < 0 ? strand2.irisInner : strand2.irisOuter;
3183
+ return { role: s.role, lo, hi, y: strand2.y };
3027
3184
  }
3185
+ const inset = bw(s.box) > 4 ? 1 : 0;
3186
+ return {
3187
+ role: s.role,
3188
+ lo: s.box.x0 + inset,
3189
+ hi: s.box.x1 - inset,
3190
+ y: cy(s.box)
3191
+ };
3192
+ };
3193
+ const eyes = sidePair(byRole, "eye_L", "eye_R");
3194
+ const irises = sidePair(byRole, "iris_L", "iris_R");
3195
+ const pair = irises ?? eyes;
3196
+ const strand = irises ? options.strandEdges : void 0;
3197
+ const left = row(pair.left, strand?.left, -1);
3198
+ const right = row(pair.right, strand?.right, 1);
3199
+ let candidates;
3200
+ const edges = options.headEdges;
3201
+ if (frame.hairShell && edges && edges.left.length > 0 && edges.right.length > 0) {
3202
+ candidates = {
3203
+ left: edges.left.map((e) => ({ role: e.role, x: e.x })),
3204
+ // A right edge names its outermost pixel column; the boundary is one on.
3205
+ right: edges.right.map((e) => ({ role: e.role, x: e.x + 1 }))
3206
+ };
3207
+ } else if (frame.hairShell) {
3208
+ candidates = {
3209
+ left: [{ role: "hair_back", x: frame.shellLeft }],
3210
+ right: [{ role: "hair_back", x: frame.shellRight }]
3211
+ };
3212
+ } else {
3213
+ const w = frame.paintedHalfAt(frame.eyeY);
3214
+ candidates = {
3215
+ left: [{ role: "face", x: frame.axisX - w }],
3216
+ right: [{ role: "face", x: frame.axisX + w }]
3217
+ };
3218
+ }
3219
+ const restHalf = (Math.max(...candidates.right.map((c) => c.x)) - Math.min(...candidates.left.map((c) => c.x))) / 2;
3220
+ const nose = byRole.get("nose");
3221
+ let noseAt;
3222
+ let noseBox;
3223
+ if (hasNose && nose !== void 0) {
3224
+ noseBox = nose.denseCore ? boxOfPixels(nose.denseCore, nose.canvasW, nose.canvasH) : boxOfLayer(nose);
3225
+ noseAt = { x: cx(noseBox), y: cy(noseBox) };
3028
3226
  }
3227
+ const mouthBox = byRole.has("mouth_open") ? unionBoxes([box("mouth"), box("mouth_open")]) : box("mouth");
3228
+ const m = box("mouth");
3029
3229
  return {
3030
- canvasW,
3031
- canvasH,
3032
- face,
3033
- axisX,
3034
- eyeY,
3035
- mouthY,
3036
- chinY,
3037
- hh,
3038
- paintedHalfAt,
3039
- wMax,
3040
- headMax,
3041
- hasProfile: profile !== null,
3042
- neck,
3043
- ears,
3044
- cutAt,
3045
- cheekY: eyeY - 0.5 * (eyeY - chinY),
3046
- jawY: eyeY - 0.8 * (eyeY - chinY),
3047
- shellLeft,
3048
- shellRight,
3049
- hairShell,
3050
- holdBase: headHalf ?? plateHalf
3230
+ frame,
3231
+ left,
3232
+ right,
3233
+ candidates,
3234
+ restHalf,
3235
+ strand,
3236
+ hasHairFront: byRole.has("hair_front"),
3237
+ eyeBoxes: { left: eyes.left.box, right: eyes.right.box },
3238
+ noseAt,
3239
+ noseBox,
3240
+ mouthAt: { x: cx(m), y: cy(m) },
3241
+ mouthBox
3242
+ };
3243
+ }
3244
+ function renderedLander(turn, parts, grids, from = /* @__PURE__ */ new Map()) {
3245
+ const analytic = /* @__PURE__ */ new Map();
3246
+ const landed = /* @__PURE__ */ new Map();
3247
+ const field = (role) => {
3248
+ const family = roleSpec(role).family;
3249
+ let f = analytic.get(family);
3250
+ if (f === void 0) analytic.set(family, f = familyField(turn, family));
3251
+ return f;
3252
+ };
3253
+ return (role, x, y, ax) => {
3254
+ const family = roleSpec(role).family;
3255
+ const part = parts.find((p) => p.id === role);
3256
+ const g = grids.get(family);
3257
+ const warp = part?.warps?.find((w) => w.parameter === P.AngleX);
3258
+ if (part === void 0 || part.mesh === void 0 || !g && !warp) {
3259
+ return x + field(role)(x, y, ax, 0)[0];
3260
+ }
3261
+ const box = {
3262
+ x0: part.transform.x - part.width / 2,
3263
+ x1: part.transform.x + part.width / 2,
3264
+ y0: part.transform.y - part.height / 2,
3265
+ y1: part.transform.y + part.height / 2
3266
+ };
3267
+ const rest = meshPoints(part.mesh, box);
3268
+ const key = `${role}@${ax}`;
3269
+ let xs = landed.get(key);
3270
+ if (xs === void 0) {
3271
+ if (g) {
3272
+ const offsets = keyformAt(g.keyforms, ax);
3273
+ xs = rest.map(([px, py]) => land(g.lattice, offsets, px, py)[0]);
3274
+ } else {
3275
+ const k = warp.keyforms;
3276
+ const offsets = blendStops(
3277
+ k.map((kf) => kf.value),
3278
+ k.map((kf) => kf.offsets),
3279
+ ax
3280
+ );
3281
+ xs = rest.map(([px], i) => px + offsets[2 * i] * bw(box));
3282
+ }
3283
+ landed.set(key, xs);
3284
+ }
3285
+ const idx = part.mesh.indices;
3286
+ for (let t = from.get(role) ?? 0; t < idx.length; t += 3) {
3287
+ const [a, b, c] = [idx[t], idx[t + 1], idx[t + 2]];
3288
+ const [ax0, ay0] = rest[a];
3289
+ const [bx0, by0] = rest[b];
3290
+ const [cx0, cy0] = rest[c];
3291
+ const d = (by0 - cy0) * (ax0 - cx0) + (cx0 - bx0) * (ay0 - cy0);
3292
+ if (Math.abs(d) < 1e-9) continue;
3293
+ const u = ((by0 - cy0) * (x - cx0) + (cx0 - bx0) * (y - cy0)) / d;
3294
+ const v = ((cy0 - ay0) * (x - cx0) + (ax0 - cx0) * (y - cy0)) / d;
3295
+ const w = 1 - u - v;
3296
+ if (u < -1e-6 || v < -1e-6 || w < -1e-6) continue;
3297
+ return u * xs[a] + v * xs[b] + w * xs[c];
3298
+ }
3299
+ return x + field(role)(x, y, ax, 0)[0];
3051
3300
  };
3052
3301
  }
3053
3302
 
@@ -3124,12 +3373,20 @@ function buildFaceMesh(frame, b, head, neckCells) {
3124
3373
  frame.axisX - neck2.waist,
3125
3374
  frame.axisX + neck2.waist
3126
3375
  );
3376
+ if (neck2.bandEnd !== void 0)
3377
+ at.push(frame.axisX - neck2.bandEnd, frame.axisX + neck2.bandEnd);
3127
3378
  }
3379
+ const placed = /* @__PURE__ */ new Set();
3128
3380
  for (const k of at) {
3129
3381
  if (k <= x0 || k >= x1) continue;
3130
- const near = xs2.findIndex((x) => Math.abs(x - k) < 0.3 * step);
3131
- if (near >= 0 && near !== 0 && near !== xs2.length - 1) xs2[near] = k;
3132
- else if (near < 0) xs2.push(k);
3382
+ if ([...placed].some((i) => Math.abs(xs2[i] - k) < 0.5)) continue;
3383
+ const near = xs2.findIndex(
3384
+ (x, i) => !placed.has(i) && Math.abs(x - k) < 0.3 * step
3385
+ );
3386
+ if (near > 0 && near < n) {
3387
+ xs2[near] = k;
3388
+ placed.add(near);
3389
+ } else if (near < 0) placed.add(xs2.push(k) - 1);
3133
3390
  }
3134
3391
  return xs2.sort((a, b2) => a - b2);
3135
3392
  };
@@ -3145,7 +3402,7 @@ function buildFaceMesh(frame, b, head, neckCells) {
3145
3402
  );
3146
3403
  const sampleBelow = (x) => Math.max(
3147
3404
  1,
3148
- SAMPLE_BELOW * frame.hh * shadeAcross(Math.abs(x - frame.axisX), neck)
3405
+ SAMPLE_BELOW * frame.hh * fadeToOutline(Math.abs(x - frame.axisX), neck)
3149
3406
  );
3150
3407
  let highest = -Infinity;
3151
3408
  for (const x of xs2) {
@@ -3597,8 +3854,7 @@ function eyesInPlate(ctx, m) {
3597
3854
  function chinOverNeck(ctx, m) {
3598
3855
  const { frame } = ctx;
3599
3856
  if (frame.neck === void 0) return true;
3600
- const f = familyField(m, "face");
3601
- return Math.abs(f(frame.axisX, frame.chinY, 30, 0)[0]) <= frame.neck.half;
3857
+ return chinSlide(m) <= frame.neck.half;
3602
3858
  }
3603
3859
  function strandHeld(ctx, m) {
3604
3860
  const land2 = analyticLander(m);
@@ -3915,10 +4171,32 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
3915
4171
  checkedKeys("turnTargets", options.turnTargets, TURN_TARGET_KEYS);
3916
4172
  const targets = hasNose ? resolveTurnTargets(options.turnTargets, plateHalf) : { given: /* @__PURE__ */ new Set() };
3917
4173
  const turnOptions = hasNose ? options : {};
3918
- const frame = buildHeadFrame(layers, {
4174
+ const frameOptions = {
3919
4175
  headHalfWidth: targets.headHalfWidth,
3920
4176
  headEdges: targets.headHalfWidth !== void 0 ? options.headEdges : void 0
4177
+ };
4178
+ const solveFrame = buildHeadFrame(layers, frameOptions);
4179
+ const ctx = solveContext(solveFrame, byRole, turnOptions, hasNose);
4180
+ const boxes = Object.fromEntries(
4181
+ ["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
4182
+ );
4183
+ const landmarks = {
4184
+ boxes,
4185
+ noseAt: ctx.noseAt,
4186
+ noseTopY: ctx.noseBox?.y1,
4187
+ mouthAt: ctx.mouthAt,
4188
+ hairTop: has("hair_front") ? box("hair_front").y1 : solveFrame.face.y1,
4189
+ // The field reads the grid the mesh is built on (the rebuilt frame below
4190
+ // differs only in the jaw's cut, which the grid does not read).
4191
+ hairFrontGrid: has("hair_front") ? hairFrontGrid(box("hair_front"), solveFrame, boxes) : void 0
4192
+ };
4193
+ const build = (q) => buildTurn(solveFrame, landmarks, q);
4194
+ const fit = fitTurn(ctx, targets, style, build);
4195
+ const frame = buildHeadFrame(layers, {
4196
+ ...frameOptions,
4197
+ chinSlide: chinSlide(fit.model)
3921
4198
  });
4199
+ const turn = { ...fit.model, frame };
3922
4200
  const parts = [];
3923
4201
  const reach = /* @__PURE__ */ new Map();
3924
4202
  let regionOf = [];
@@ -3932,7 +4210,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
3932
4210
  parts.length,
3933
4211
  frame,
3934
4212
  byRole,
3935
- style
4213
+ style,
4214
+ landmarks.hairFrontGrid
3936
4215
  );
3937
4216
  if (built.region !== void 0) {
3938
4217
  regionOf = built.region;
@@ -3941,19 +4220,6 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
3941
4220
  reach.set(spec.role, built.reach);
3942
4221
  parts.push(built.part);
3943
4222
  }
3944
- const ctx = solveContext(frame, byRole, turnOptions, hasNose);
3945
- const landmarks = {
3946
- boxes: Object.fromEntries(
3947
- ["eye_L", "eye_R", "brow_L", "brow_R"].filter(has).map((r) => [r, box(r)])
3948
- ),
3949
- noseAt: ctx.noseAt,
3950
- noseTopY: ctx.noseBox?.y1,
3951
- mouthAt: ctx.mouthAt,
3952
- hairTop: has("hair_front") ? box("hair_front").y1 : frame.face.y1
3953
- };
3954
- const build = (q) => buildTurn(frame, landmarks, q);
3955
- const fit = fitTurn(ctx, targets, style, build);
3956
- const turn = fit.model;
3957
4223
  const grids = /* @__PURE__ */ new Map();
3958
4224
  for (const family of Object.keys(WARP_ID)) {
3959
4225
  const members = parts.filter((p) => roleSpec(p.id).family === family);
@@ -4036,7 +4302,7 @@ function plateFields(turn, region, head) {
4036
4302
  const neck = neckField(turn);
4037
4303
  return (i) => region[i] === "ear" ? ear : region[i] === "neck" ? neck : head;
4038
4304
  }
4039
- function buildPart(spec, b, order, frame, byRole, style) {
4305
+ function buildPart(spec, b, order, frame, byRole, style, frontGrid) {
4040
4306
  const box = (role) => boxOfLayer(byRole.get(role));
4041
4307
  const part = {
4042
4308
  id: spec.role,
@@ -4059,30 +4325,15 @@ function buildPart(spec, b, order, frame, byRole, style) {
4059
4325
  mesh = fm.mesh;
4060
4326
  region = fm.region;
4061
4327
  headStart = fm.headStart;
4328
+ } else if (spec.role === "hair_front") {
4329
+ mesh = columnMesh(b, frontGrid);
4062
4330
  } else if (spec.family !== "body") {
4063
4331
  const cells = MESH_CELLS[spec.role] ?? FEATURE_MESH_CELLS;
4064
4332
  const px = Math.min(cells.px, meshScale(spec.role, frame));
4065
- const cols = cellsFor(bw(b), px, cells.min, cells.max);
4066
- const rows = cellsFor(bh(b), px, cells.min, cells.max);
4067
- if (spec.role === "hair_front") {
4068
- const boxes = Object.fromEntries(
4069
- ["eye_L", "eye_R"].filter((r) => byRole.has(r)).map((r) => [r, box(r)])
4070
- );
4071
- const { inner, left, right } = hairFrontBends(frame, boxes);
4072
- mesh = columnMesh(
4073
- b,
4074
- [
4075
- frame.axisX - left,
4076
- frame.axisX - inner,
4077
- frame.axisX + inner,
4078
- frame.axisX + right
4079
- ],
4080
- cols,
4081
- rows
4082
- );
4083
- } else {
4084
- mesh = gridMesh(cols, rows);
4085
- }
4333
+ mesh = gridMesh(
4334
+ cellsFor(bw(b), px, cells.min, cells.max),
4335
+ cellsFor(bh(b), px, cells.min, cells.max)
4336
+ );
4086
4337
  }
4087
4338
  if (mesh !== void 0) part.mesh = mesh;
4088
4339
  const side = spec.role.endsWith("_L") ? "L" : "R";
@@ -4201,16 +4452,35 @@ function buildPart(spec, b, order, frame, byRole, style) {
4201
4452
  headStart
4202
4453
  };
4203
4454
  }
4204
- function columnMesh(b, at, cols, rows) {
4455
+ function hairFrontGrid(b, frame, boxes) {
4456
+ const cells = MESH_CELLS.hair_front;
4457
+ const px = Math.min(cells.px, meshScale("hair_front", frame));
4458
+ const cols = cellsFor(bw(b), px, cells.min, cells.max);
4459
+ const rows = cellsFor(bh(b), px, cells.min, cells.max);
4460
+ const { inner, left, right } = hairFrontBends(frame, boxes);
4205
4461
  const step = bw(b) / cols;
4206
4462
  const xs = Array.from({ length: cols + 1 }, (_, c) => b.x0 + c * step);
4207
- for (const x of at) {
4463
+ for (const x of [
4464
+ frame.axisX - left,
4465
+ frame.axisX - inner,
4466
+ frame.axisX + inner,
4467
+ frame.axisX + right
4468
+ ]) {
4208
4469
  if (x <= b.x0 + 0.25 || x >= b.x1 - 0.25) continue;
4209
4470
  const near = xs.findIndex((v) => Math.abs(v - x) < 0.3 * step);
4210
4471
  if (near > 0 && near < xs.length - 1) xs[near] = x;
4211
4472
  else if (near < 0) xs.push(x);
4212
4473
  }
4213
4474
  xs.sort((p, q) => p - q);
4475
+ const ys = Array.from(
4476
+ { length: rows + 1 },
4477
+ (_, r) => b.y1 - bh(b) * r / rows
4478
+ );
4479
+ return { xs, ys };
4480
+ }
4481
+ function columnMesh(b, grid) {
4482
+ const { xs } = grid;
4483
+ const rows = grid.ys.length - 1;
4214
4484
  const vertices = [];
4215
4485
  const uvs = [];
4216
4486
  for (let r = 0; r <= rows; r++) {