# Меш со скелетом — `$.mesh`

Псевдо-3D персонаж: части-квады с текстурой, которые **деформируются скелетом**,
а z-буфер разбирается с их самопересечениями. Это вторая половина §4.1 (первая —
текстура и UV в `engine.submitMesh`, см. [depth.md](highlevel/depth) §4).

```js
const rig = $.mesh.skeleton({
    root: { x: 200, y: 300, length: 40, angle: 0 },
    arm:  { parent: 'root', length: 40, angle: -0.3 },
});

const arm = $.mesh.part({
    texture: atlasTexture,                 // id текстуры (необязательно)
    verts: [240, 300, 280, 300, 280, 320, 240, 320],   // x, y
    uv:    [0, 0, 1, 0, 1, 1, 0, 1],
    tris:  [0, 1, 2, 0, 2, 3],
    bones: ['arm', 'arm', 'arm', 'arm'],   // кость на вершину
});

$.update(() => $.mesh.draw(arm, { arm: handAngle }, rig));
```

---

## 1. Скелет: угол + длина, а не координаты

```js
$.mesh.skeleton({
    имя: { parent?, length?, angle?, x?, y? },
});
```

| Поле | Смысл |
|---|---|
| `parent` | имя родителя; без него кость — корень |
| `length` | длина: **начало ребёнка ставится на конец родителя** |
| `angle` | угол покоя (радианы) |
| `x`, `y` | позиция корня |

Кость — это «угол + длина». Мировые позиции считаются сложением по дереву,
поэтому анимация задаёт **один угол на кость**, а не координаты вершин.

```js
rig.bones();          // имена — родитель раньше ребёнка
rig.rest();           // мировые кости в покое
rig.pose({ arm: 0.6 });   // мировые кости; углы СКЛАДЫВАЮТСЯ по родителям
rig.tip(pose, 'arm');     // конец кости — удобно вешать дочернюю часть
```

**Порядок костей исправляется сам**: если в описании ребёнок стоит раньше
родителя, `bones()` всё равно вернёт родителя первым. Иначе мировые позиции
считались бы по ещё не посчитанному родителю — молчаливая ошибка в картинке.

**Углы складываются**: угол ребёнка — это его собственный поворот **поверх**
поворота родителя. Поэтому анимация описывает движение сустава, а не абсолютную
ориентацию.

---

## 2. Часть: вершины, UV, кости

```js
$.mesh.part({
    texture, verts, uv, tris, bones | weights, colors, depth,
});
```

| Поле | Смысл |
|---|---|
| `verts` | плоский `x, y` — локальные координаты части |
| `uv` | плоский `u, v` (`0..1`) |
| `tris` | индексы по три (обязательны: `draw` без них вернёт `0`) |
| `bones` | имя кости **на вершину**, вес `1` |
| `weights` | до **двух** костей на вершину: `[['root',0.5,'arm',0.5], …]` |
| `colors` | цвет на вершину `[r,g,b]` в `0..255` (иначе белый) |
| `depth` | глубина `z` для всех вершин части (по умолчанию `0.5`) |
| `texture` | id текстуры; `-1` — белая (виден только цвет) |

Формат `weights` — **плоский список пар на вершину**: `['имя', вес, 'имя', вес]`.
Пары сверх двух отбрасываются: в 2D больше не нужно.

### 2.1. Часть из слайса Aseprite

```js
const hero = $.atlas.load('hero', 'art/hero.json');
const hand = $.mesh.fromSlice(hero, 'hand', frame, { bone: 'hand' });
```

Слайс Aseprite несёт **пивот**, и он становится **началом координат части**:
тогда `$.mesh.draw` крутит часть вокруг сустава, а не вокруг угла картинки.
UV берутся из кадра атласа, текстура — из атласа.

`fromSlice(sheet, имя, frame?, opts?)` возвращает готовую часть (или `null`, если
слайса/кадра нет, — с записью в журнал).
`opts`: `bone` (кость для всех вершин), `bones` (по вершине), `depth`, `texture`.
У части появляются поля `slice` (`{name, frame}`) и `pivot` (`{x, y}` — локальный).

**Костей в Aseprite JSON нет** — они только в `.ase`. Дерево костей задаётся
`$.mesh.skeleton` руками, слайсы дают привязку частей и пивоты.

---

## 3. Деформация

```js
$.mesh.draw(part, angles, rig, opts);   // → число вершин
$.mesh.draw(part, $.mesh.posed(rig, angles), null, opts);
```

Деформированная часть уходит в `engine.submitMesh`, поэтому:

* `tris` **разворачиваются** в список вершин — их порядок и задаёт картинку;
* глубина каждой вершины — `part.depth`, то есть **между частями** перекрытие
  решает z-буфер (`depth` частей задавайте так, чтобы ближняя была меньше);
* `texture` сэмплится по `u`/`v`.

**Мягкий сгиб.** Вершина с весом `0.5` на две кости «тянется» между ними:
каждая кость двигает её на свою долю, результат усредняется. Так стык сустава не
рвётся.

**Без аллокаций в кадре.** Деформация пишется в **два переиспользуемых**
`Float32Array` (вершины и развёртка); они растут только при нехватке места.
Размеры видны: `$.mesh.scratchSize()`, `$.mesh.flatSize()`.

---

## 3.1. Обратная кинематика

Прямая задача («по углам найти конец») решается `pose()`. Обратная — «дай такие
углы, чтобы конец попал в ЦЕЛЬ» — нужна для ступни на неровном полу, руки на
рукояти, взгляда на игрока:

```js
const rig = $.mesh.skeleton({
    thigh: { x: 0, y: 0, length: 60, angle: 0 },
    shin:  { parent: 'thigh', length: 60, angle: 0 },
});

const solved = $.mesh.ik(rig, {}, { x: 60, y: 80 },
                         { chain: ['thigh', 'shin'], bend: 1 });

$.mesh.draw(shin_part, solved.angles, rig);   // нога достала до цели
solved.reached;    // дотянулись ли
solved.distance;   // промах в пикселях
solved.tip;        // { x, y } — куда встал конец
```

`chain` — имена костей **от корня цепочки к концу** (обязателен). `bend` —
сторона сгиба для двух костей (`+1`/`-1`): колено внутрь или наружу.
`iterations`/`tolerance` — для длинных цепочек.

**Две кости решаются точно** (закон косинусов). Более длинная цепочка — **FABRIK**
(прямые и обратные проходы по позициям суставов).

**Почему не CCD.** Я сначала написал CCD (доворачивать каждую кость, чтобы конец
смотрел на цель) — и он **застревал намертво на коллинеарном старте**: если все
кости уже вытянуты в сторону цели, направления на конец и на цель совпадают,
поворот выходит нулевым, и цепочка не двигается, хотя конец не дотянулся. Тест
поймал это сразу: промах не менялся вовсе. FABRIK работает с позициями и такой
конфигурации не боится.

**Цель вне досягаемости**: кости вытягиваются в её сторону, `reached` = `false`,
`distance` — насколько не дотянулись. Молча «прилипать» к цели нельзя: картинка
дёрнется. Игра сама решает — подвинуть тело или оставить как есть.

---

## 4. Ограничения (честно)

* **Линейное смешивание весов (LBS) «схлопывает» вершины при большом повороте** —
  это candy-wrapper, известное свойство метода, а не дефект. При повороте
  больше ~120° вершина с весом `0.5` уезжает к центру; при `180°` квад может
  сжаться в точку. Держитесь умеренных углов (до ~90°) или разрезайте часть на
  больше костей;
* **скелет не рисуется**: это кости для деформации, а не визуальные «шарниры»;
* **нормалей и освещения нет**: цвет берётся из вершин и текстуры, 3D-свет не
  считается;
* **части не сортируются автоматически**: `depth` задаёт игра. Если части
  пересекаются и `depth` одинаков — порядок будет порядком вызовов;
* **спрайты всегда поверх меша** (спрайтовый шейдер пишет `z = 0`) — см.
  [depth.md](highlevel/depth) §4;
* **нет скелетной анимации как данных**: дорожки углов кладутся на существующий
  `$.anim` / `$.anim.player` — своего формата клипов у `$.mesh` нет;
* **IK без ограничений углов**: суставы не имеют пределов поворота, поэтому
  колено может выгнуться в неестественную сторону. Выбирайте `bend` и не
  ставьте цель слишком близко;
* **IK не учитывает столкновения**: цепочка пройдёт сквозь стену — препятствия
  обходите сами (например, двигая цель).

---

## 5. Проверка

```bash
# чистая часть (без движка): дерево, углы, покой, поворот, мягкий сгиб
build/_deps/quickjs-build/qjs tests/js/mesh_test.mjs

# в движке, ПО ПИКСЕЛЯМ: покой, поворот на 90°, мягкий сгиб, текстура, буферы
python3 tests/agent/highlevel_mesh_rig_test.py
```

Юнит-тест проверяет и то, что деформация **пишет ровно в переданный буфер** —
это и есть обещание «без аллокаций в кадре».
