Files
Learn_System/plans/sim-builder/phase-2-physics.md
T

105 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.