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

10 KiB
Raw Permalink Blame History

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

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

Objective

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

Tasks

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

  • Все задачи выполнены
  • Опирается на математику _fx_motion.spring (полу-неявный/симплектический Эйлер v+=a·dt; x+=v·dt) — обобщена на связанные тела в SimPhysics; API tween/spring-фабрики _fx_motion (rAF-замыкания на одно значение) не подходит для N связанных тел, поэтому тонкий модуль поверх той же математики, без дубля иного интегратора.
  • Стабильность (нет взрыва энергии): фикс-шаг dt (кламп 1/2000..1/30), накопитель + кап подшагов (8), кламп скорости (1e4), вязкое трение через exp(-friction·dt). Headless-прогон: падение/маятник/шары — конечные, без NaN/∞.
  • Нет регрессий Ф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.

Формат (точные поля)

// глобальный блок физики
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.