# Phase 2: Физический интегратор

**Status:** ✅ Implemented (не закоммичено — коммит за оркестратором)
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend

## Objective
Добавить настоящую физику: тела с массой, гравитация/пружины/столкновения/трение,
перетаскивание тел силой, траектории. Динамика считается движком, а не формулой.
После фазы маятник/столкновения/брошенное тело идут динамически из спеки.

## Tasks
- [x] Блок `physics` в спеке: `{ enabled, gravity:{x,y}, friction, walls:[...], restitution, dt?, springs? }`. gravity/friction/restitution — числа ИЛИ выражения от params (вычисляются на reset).
- [x] Тело-объект: `body:{ mass, vx, vy, fixed }` на point/circle — интегрируется фикс-шагом (накопитель dt). Нач. позиция/vx/vy/масса — числа или выражения от params (вычисляются при reset/init, далее интегрируются). Опора на математику `_fx_motion` (полу-неявный Эйлер); см. ниже.
- [x] Пружины: `springs:[{ a, b, k, length, damping? }]` — концы: id тела ИЛИ якорь-точка `[x,y]`. Сила Гука + демпфирование вдоль оси. Рисуются зигзагом.
- [x] Столкновения: упругие круг-круг (по нормали, импульс + позиционная коррекция по обратным массам) и круг-стена (restitution). Broadphase O(n^2) (N мало).
- [x] Drag тела: тащишь — задаёт позицию (тело «приколото», не интегрируется); отпустил — сообщает скорость (бросок, кламп 40 м/с). Формульные объекты Ф0/Ф1 сосуществуют (drag-ручки и физ-тела в одной сцене).
- [x] Траектория: тело с `trail:true` пишет след центра (переиспользован существующий механизм trail; позиция берётся из env-полей тела).
- [x] Демо-спеки за флагом: «пружинный маятник» (`customphys`: тело+пружина+гравитация+drag) и «упругие шары» (`customballs`: 3 тела + 4 стены, g/упругость от слайдеров).

## Files to Modify/Create
- `frontend/js/labs/_sim_engine.js` — физический режим, интеграция с _fx_motion (modify)
- `frontend/js/labs/_sim_physics.js` — обёртка интегратора/коллизий, если чище отдельно (new, опц.)
- `frontend/js/labs/_sim_demo.js` — физ-демо (modify)

## Acceptance Criteria
- Тело под гравитацией падает/летит по параболе через интегратор (не по формуле).
- Пружина колеблет груз; шары упруго сталкиваются; стены отражают.
- Drag тела работает; формульные объекты Ф0 продолжают работать в той же сцене.

## Notes
- Шаг интегратора фиксированный (накопитель dt) для стабильности.
- Не переусложнять коллизии — школьный уровень (круги/стены).

## Review Checklist
- [x] Все задачи выполнены
- [x] Опирается на математику `_fx_motion.spring` (полу-неявный/симплектический Эйлер `v+=a·dt; x+=v·dt`) — обобщена на связанные тела в `SimPhysics`; API tween/spring-фабрики `_fx_motion` (rAF-замыкания на одно значение) не подходит для N связанных тел, поэтому тонкий модуль поверх той же математики, без дубля иного интегратора.
- [x] Стабильность (нет взрыва энергии): фикс-шаг dt (кламп 1/2000..1/30), накопитель + кап подшагов (8), кламп скорости (1e4), вязкое трение через `exp(-friction·dt)`. Headless-прогон: падение/маятник/шары — конечные, без NaN/∞.
- [x] Нет регрессий Ф0/Ф1: формульные point/segment/circle/rect/polyline/path/vector/label/plot/readout/drag работают; тела и формульные объекты в одной сцене (тест mixed).

## Handoff to Next Phase

### Что реализовано (Phase 2) — только `_sim_engine.js` + `_sim_demo.js`
Файл `_sim_physics.js` НЕ создавался: его нельзя подключить без правки `frontend/lab.html`
(зона параллельной сессии). Интегратор живёт внутри `_sim_engine.js` и экспортируется
как `window.SimPhysics` (переиспользуемо headless/билдером/доской). Если Ф4+ захочет
вынести в отдельный файл — добавить `<script>` в lab.html и `module.exports`-ветку.

- **`window.SimPhysics`** — `{ step(state, dtFrame), integrate(...), resolveCollisions(...) }`.
  Полу-неявный Эйлер (математика `_fx_motion.spring`), фикс-шаг, пружины (Гук+демпф),
  упругие столкновения круг-круг и круг-стена. Без DOM/eval — чистая функция над state.
- **Engine**: `_preparePhysics()` (сборка тел/пружин/стен из спеки на reset),
  `_stepPhysics(dt)` (вызывается в rAF до `_renderFrame`; удерживаемое тело временно
  fixed), drag тел (`_dragBody`), `_drawSprings()`, рендер point/circle-тел из состояния
  интегратора, body-поля в `_buildEnv`.

### Формат (точные поля)
```jsonc
// глобальный блок физики
physics: {
  enabled: true,                 // false/отсутствие -> чистая кинематика (Ф0/Ф1)
  gravity: { x:0, y:-9.8 },       // число ИЛИ выражение от params (вычисляется на reset)
  friction?: 0,                   // вязкое затухание (1/с), exp(-friction·dt); число/выражение
  restitution?: 1,                // упругость 0..1 (кламп); число/выражение
  dt?: 1/240,                     // фикс-шаг (кламп 1/2000..1/30)
  walls?: [
    { side:'bottom'|'top'|'left'|'right' },  // из границ viewport
    { x1,y1, x2,y2 }                          // произвольный отрезок (нормаль к центру)
  ],
  springs?: [
    { a:'bodyId'|[x,y], b:'bodyId'|[x,y],     // конец: id тела ИЛИ якорь-точка
      k:40, length:5, damping?:0.4 }          // k/length/damping — число/выражение
  ]
}
// тело на point/circle (интегрируется, НЕ формула)
{ id:'bob', type:'circle', r:0.6, x:'3', y:'-4',   // нач. позиция — число/выражение
  trail?:true, trailColor?:'#...',                  // след тела (механизм Ф0)
  body:{ mass?:1, vx?:0, vy?:0, fixed?:false } }    // масса/vx/vy — число/выражение
```

### env-поля тел (доступны readout/plot/label/привязкам)
Для каждого тела в env кладутся `<id>.x`, `<id>.y`, `<id>.vx`, `<id>.vy` ИЗ СОСТОЯНИЯ
ИНТЕГРАТОРА (не из выражения). Кладутся ПЕРВЫМИ в `_buildEnv` — снимает forward-ref
проблему однопроходного env: формульные объекты, ссылающиеся на тело, видят его
актуальную позицию/скорость в этом же кадре.

### Гочи / решения / риски
- **Имя param `e` зарезервировано** (число Эйлера в SimExpr). Не использовать `e` как
  имя параметра в выражениях физики — взять `el`/`elast` и т.п. (демо «шары» учтено).
- **Радиус тела для коллизий**: circle — мировой `r`; point — экранные px → переводятся
  в мир через текущий масштаб (фолбэк 0.3) при `_preparePhysics`. Зависит от `_scale`,
  поэтому физика собирается ПОСЛЕ `_fit()` (в reset, который зовётся после первого fit).
- **Изменение params на лету**: gravity/k/length/restitution/масса/нач.условия пересчит. на
  **reset** (и на паузе в `t==0` — для предпросмотра старта). Во время проигрывания слайдеры
  меняют только сами params в env (для readout/формул), но не телепортируют тела/не меняют силы
  до следующего reset. Это намеренно (стабильность). Если Ф4 захочет live-силы — пересобирать
  `opts`/springs каждый кадр (тела не трогать).
- **Drag во время play**: удерживаемое тело временно `fixed`; на отпускании — скорость из
  сглаженной оценки движения курсора (кламп 40 м/с).
- **Сериализация для Ф3 (БД/API)**: в спеке надо хранить/валидировать блок `physics`
  (gravity x/y, friction, restitution, dt, walls[], springs[]) и `body{}` на объектах
  (mass, vx, vy, fixed — числа или строки-выражения). Глубину/число springs/walls и
  диапазоны (k, mass>0, restitution 0..1, dt) — проверять на сервере как и остальные
  выражения. Строки-выражения санитизировать так же, как x/y/expr Ф0/Ф1.
