Files
Learn_System/plans/sim-builder/CONTEXT.md
T

44 KiB
Raw Blame History

Feature Context: Конструктор симуляций (SimForge)

Current State

  • РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) — P4 «UI билдера + контролы стиля» РЕАЛИЗОВАН (рабочее дерево, не закоммичено; ветка feature/sim-builder). Файлы: только frontend/sim-builder.html + frontend/js/sim-builder.js. _sim_engine.js/js/api.js/lab.* НЕ тронуты — билдер лишь генерит спеку, которую движок (P2/P3) уже умеет рисовать.
    • Контролы стиля объекта (блок «Стиль», STYLE_FOR[type]): слайдер непрозр.(opacity 0..1), select lineStyle(solid/dashed/dotted), pointStyle(только point), тумблер glow, тумблер градиент- заливки(circle/rect → gradient:[c0,c1]). Цвета — colorCtl: нативный <input type=color> + текст (источник истины, держит rgba/named) + очистка для fill/trailColor. Синхрон — wireColorControls, toHexColor#rrggbb. Per-объект уже были width/color в OBJ_FIELDS — переведены на color-пикеры.
    • Редактор кривых plot (plotEditor/curveEditor): UI-модель {var,range_a/b,samples,trace,legend, plotFill,plotMarker,curves:[{expr,color,label,width,lineStyle,opacity,fill,fillColor,marker}]}. Список кривых (add/del, минимум 1), на кривую все P3-поля + fx-палитра, plot-уровневые fill/marker/легенда. loadPlot (spec→UI: curves[]→exprs[]→legacy expr; легаси plot-level width/lineStyle/opacity → в кривую), normalizePlotForSpec+stripCurve (UI→spec). Одиночная простая кривая → легаси {expr,color}, иначе curves:[...]. legend:false эмитится только при выкл.
    • Список объектов/графиков: z-order вверх/вниз (порядок массива = порядок отрисовки), видимость (hidden:true — чисто билдерский флаг, фильтруется в buildSpec, движок не знает), дублировать (deep-clone+новый _uid, id+'_copy'), удалить. Иконки — новые inline SVG .ic (up/down/copy/eye/eyeOff/clearX).
    • Минимизация спеки + стабильный round-trip: stripObj.isDefaultStyle выбрасывает дефолты (glow:false, lineStyle:'solid', pointStyle:'filled', opacity:1, trail/closed:false) и hidden. Save→load→ save идемпотентен (loadFromSim восстанавливает дефолты из контролов).
    • Дизайн/мобайл: новые CSS-классы в ls.css-стиле (.sbu-obj-style/.sbu-style-row/.sbu-color-*/ .sbu-range/.sbu-curve(s)/.is-hidden/.sbu-grad-row); заголовок объекта flex-wrap + 26px-кнопки; медиа ≤920px (раскладка) + новый ≤560px (поля/стили в один столбец). Пустые состояния дополнены.
    • Безопасность: выражения только через SimExpr.compile; цвета попадают лишь в спеку (canvas-стоки движка), DOM-style с польз.цветом не используется; eval/new Function — нет.
    • Верификация: node --check sim-builder.js + извлечённого инлайна html — OK; эмодзи нет (скан кодпойнтов обоих файлов — 0); eval/new Function — 0; headless vm-смоук (DOM/SimExpr-стаб) 27+12 PASS: стили объекта в спеке, round-trip объектов идемпотентен ×2, plot с 2 кривыми (label/marker/lineStyle/opacity/fill-цвет/ range/samples) + round-trip ×2, легаси-одиночная кривая → легаси-форма + round-trip, hidden исключает из спеки, z-order=порядок массива, дефолты-стрип; +шаблонные легаси-plot save→load→save стабильны (2 PASS). Temp удалены. git status: тронуты только sim-builder.html и sim-builder.js.
    • Следующее (P5): прямое манипулирование на сцене (drag всех типов + snap-к-сетке + выравнивание) и undo/redo. Потребуются правки _sim_engine.js (хит-тесты/ручки) + sim-builder.js (стек снапшотов this.st).
  • РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) — P3 «Графики/диаграммы» РЕАЛИЗОВАН (рабочее дерево, не закоммичено; ветка feature/sim-builder). Файл: только frontend/js/labs/_sim_engine.js. Расширен _drawPlot + ветка type==='plot' в _prepareObjects. Оси/сетка/подписи уже из P1 — не дублировались.
    • Несколько кривых: нормализуются в prep.curves[], приоритет источника curves:[{...}]exprs:['sin(x)','x^2'] → одиночный expr (легаси, обратная совместимость сохранена). Каждой кривой свой цвет (явный color или DEFAULT_PALETTE[i%8]). prep.exprFn = первой кривой (для trace-режима).
    • Поля кривой (curves[i]): expr, color, label(→легенда), width, lineStyle, opacity, fill(true→полупрозр. цвет / строка), marker(none|dot|ring). Не заданные наследуют plot-уровень. Plot-уровневые fill/marker — дефолт для всех кривых.
    • Заливка под кривой _fillUnderCurve (между кривой и y=0, посегментно — разрывы у не-finite не сливаются; baseY клиппится к canvas). Маркеры _drawCurveMarkers (переиспользует _drawPoint, прорежены ~28px). Легенда _drawLegend на canvas (тёмная плашка + свотч + светлый текст, верх-право, авто при label, legend:false отключает). Новые модульные хелперы _markerStyle/_fillAlpha.
    • Безопасность: цвета только в canvas-стоки (strokeStyle/fillStyle/fillText фикс-цвет легенды); DOM-style с пользовательским цветом не используется; eval нет. Каждая кривая в своём save/restore, легенда на внешнем уровне.
    • Верификация: node --check OK; headless vm-смоук (canvas-стаб со счётчиком save/restore + РЕАЛЬНЫЕ _sim_expr+_sim_engine) 10/10: легаси/exprs[]/curves+fill+marker+legend/наследование/не-finite (1/x,tan)/legend:false/trace±range/fillUnder+markers с null/регресс point-vector-circle-rect — все PASS; ctx сбалансирован (depth→0, нет underflow). Эмодзи нет (только пре-существующие → в комментариях); eval=0. Temp-смоук удалён. git status: тронут только _sim_engine.js.
    • Следующее (P4): UI билдера + контролы стиля (sim-builder.html/sim-builder.js) — дать новым полям plot контролы: список кривых (add/del, expr+color+label+width+lineStyle+opacity+fill+marker), plot-fill/ marker, тумблер легенды; плюс per-объект color/opacity/width/dash, z-order, дублирование, мобайл.
  • РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) — P2 «Качество графики объектов» РЕАЛИЗОВАН (рабочее дерево, не закоммичено; ветка feature/sim-builder). Файл: только frontend/js/labs/_sim_engine.js. Один движок → эффект и в билдере, и в /lab, и на доске.
    • Чтение стилей расширено в _prepareObjects; применение — через два хелпера: _applyStroke(ctx,o) (ставит globalAlpha=opacity, lineWidth=width, lineJoin/Cap='round', setLineDash по lineStyle, glow→shadow) и _fillStyleFor(ctx,o,x0,y0,x1,y1) (линейный градиент gradient:[c0,c1] по bbox ИЛИ сплошной fillColor; всё — canvas-стоки, мусорный цвет игнорится). Каждая ветка _drawObject в своём save/restore.
    • Новые поля стиля спеки (контракт для P4-контролов): opacity 0..1, lineStyle solid|dashed|dotted, fill/gradient:[c0,c1], glow:true/shadow, pointStyle filled|hollow|cross|ring, trailFade/ trailWidth/trailLen. Полный список с дефолтами — в IMPROVEMENTS.md (Handoff P2→P3/P4).
    • Стрелки векторов (_arrowHead/_arrowHeadLen): заполненный барбед-треугольник (вырез у основания), длина max(9,width*3.2)px, тело линии укорочено на длину головы. Точки _drawPoint — 4 стиля (filled-деф. = кружок + тонкая белая обводка). Трассы _drawTrail(ctx,pts,o) — посегментное затухание (alpha 0.08→0.68 от хвоста к голове) либо одна линия без fade.
    • Палитра по умолчанию DEFAULT_PALETTE (8 холодно-ярких тонов, циклически по индексу) вместо единого #06D6E0; явный color/fill всегда сохраняется. _drawPlot теперь зовёт _applyStroke (dash/opacity/ glow на кривых).
    • Верификация: node --check OK; headless-смоук (vm + DOM/canvas-стаб со счётчиками вызовов + РЕАЛЬНЫЕ _sim_expr.js+_sim_engine.js) 23/23: рендер 18-объектной спеки (все типы + все новые поля) ×4 кадра без throw; ctx не протекает (save/restore-баланс depth=0, globalAlpha→1, shadowBlur→0, lineDash→[] после кадра); setLineDash/createLinearGradient/fill/stroke/arc вызваны (dashed/dotted/gradient/fills); arrowHeadLen масштаб; все поля прочитаны; палитра применена + явный color сохранён; трасса накоплена; destroy чист. Эмодзи нет (скан кодпойнтов: только пре-существующие →/─/═/∞ в комментариях); eval=0; new Function — только в комментарии стр.15. git status: тронут только _sim_engine.js.
    • Следующее (P3): РЕАЛИЗОВАНО (см. блок P3 выше) — несколько кривых, заливка, маркеры, легенда.
  • РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) — P1 «Рабочее поле» РЕАЛИЗОВАН (рабочее дерево, не закоммичено; ветка feature/sim-builder, общая с параллельной сессией materials/quota). Файл: только frontend/js/labs/_sim_engine.js (sim-builder.html НЕ потребовался). Один движок → эффект и в билдере, и в /lab, и на доске.
    • Fix смещения вправо: _build больше не делит строку flex с фикс-панелью 260px. Теперь root(relative) → stage(absolute inset:0, canvas+labels на всю площадь) + плавающая panel (absolute left/top:10px, z-index:5, pointer-events:auto, сворачивается _togglePanel, есть только при params)
      • бар кнопок вида (right/bottom:10px). Сцена центрирована во всю ширину хоста; пустая спека не съезжает.
    • Сетка: minor(~34px)/major(×5), адаптивна к zoom (_niceStep(targetPx) завязан на _scale, шаги 1/2/5·10^n), рисуется через всю видимую область (_visibleWorld), линии на .5px (резкость, без «ступенек»).
    • Оси: X/Y (прижимаются к краю canvas, если 0 вне видимой области) + числовые подписи делений (светлый текст + тень на тёмном фоне, _axisNum/_stepDecimals) + маркер origin (0,0).
    • Zoom/Pan: колесо → _zoomAt(lx,ly,factor) (мир-точка под курсором инвариантна, зум-кламп 0.1..50×); pan = drag пустого места (_setupZoomPan), приоритет ручек/тел через общий _pickHandleAt (pan стартует, только если хит-тест вернул null). Кнопки вида: inst.fitView() / inst.resetView() (оба → центрированный viewport, SVG .ic в углу сцены). _viewLocked сохраняет вид при ресайзе. DPR-резкость сохранена.
    • destroy снимает wheel+pan-листенеры и ResizeObserver. Верификация: node --check OK; headless-смоук (DOM/canvas-стаб + реальные _sim_expr.js+_sim_engine.js) 40/40 (центрирование пустой спеки, zoom-инвариант курсора+кламп, pan-сдвиг _off, приоритет ручек над pan, drag-ручка пишет param, подписи-оверлей следуют zoom/pan, fit/reset вида, ресайз сохраняет вид, рендер всех 10 типов объектов без throw, destroy чист); эмодзи нет (только в комментариях, как в существующем коде), eval/Function нет.
    • Следующее (P2): качество графики объектов (_drawObject/_drawTrail/_arrowHead/_drawPlot/ _prepareObjects в _sim_engine.js).
  • ВСЕ ФАЗЫ (0–7) РЕАЛИЗОВАНЫ (в рабочем дереве, не закоммичено — коммит за оркестратором). Фича «Конструктор симуляций» функционально полна: рантайм+физика, БД+API, билдер, каталог в /lab, раздача/клон/шаблоны/привязка, доска онлайн-урока с синхроном классу.
  • Фаза 7 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено). Custom-sim на доске онлайн-урока через существующий iframe-конвейер. Аддитивные правки трёх файлов; рабочее дерево по ним было ЧИСТЫМ до начала (classroom.html не имел чужих незакоммиченных правок параллельной сессии — проверено git status).
    • backend/src/controllers/classroom/sim.js (+21/-2): simOpen принимает simId='custom:<dbid>', валидирует доступ (владелец ИЛИ published ИЛИ admin; иначе 404/403). Встроенный id — прежний regex ^[a-z0-9_-]{1,40}$. simState/simMode/simAnnotate/simClose НЕ тронуты (state-объект уже произвольный).
    • frontend/classroom.html (+31/-4): _crLoadCustomSims() (кэш LS.customSimsList), crOpenSimPicker async с предзагрузкой, _crRenderSimGrid мёржит свои+published custom (бейдж «Моя», id custom:<dbid>, XSS-escape). Существующий crPickSim передаёт id как есть; onSimOpen грузит iframe /lab?embed=1&sim=custom:<id> (encodeURIComponent безопасен, lab декодирует param).
    • frontend/js/labs/lab-glue.js (+48/-1): _bridgeCustomSimState(real) — подключает custom-sim к тому же мосту sim_state/apply_sim_state, что и встроенные. getState={params,running} / applyState=setParam+play/pause поверх SimEngine-инстанса (real.instance()). Регистрируется под ключом _autoSim (custom:<dbid>, т.к. apply у ученика берёт _simStateRegistry[_autoSim]), запускает _startStateEmit. Вызов в _registerLazy.open() после real.open(ctx) (только embed).
    • Синхрон: параметры слайдеров + play/pause — полный (demo-режим). Время t (фаза анимации) покадрово НЕ синхронится (by design; ученик крутит свой rAF при running). Аннотации/режим — через существующий конвейер без изменений (id-agnostic). Закрытие/смена: frame.src='about:blank' сносит весь документ iframe (SimEngine+rAF+слушатели) — утечек нет.
    • Доступ: двойная проверка — simOpen на сервере (постановка на доску) + GET /custom-sims/:id при загрузке спеки в iframe. Чужой draft → 403 на обоих. На доску только своё или published.
    • Верификация: node --check sim.js / lab-glue.js / извлечённого инлайна classroom.html — OK; эмодзи нет (UTF-8-скан текст-элементов: 0 в js, 11 в classroom.html — все ПРЕ-существующие ×/⇒/реакции, не в моих строках); eval/new Function — 0 call-sites; npm test 240/248 pass (8 fail = тот же baseline: 3 auth.test + 5 page-тестов без jsdom; обе custom-sims-сьюты зелёные). git status: тронуты только мои 3 файла (+плановые .md); js/api.js НЕ нужен (методы есть с Ф3).
  • Фаза 6 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Файлы: backend/src/controllers/customSimController.js (+share/clone/related/addLink/removeLink, импорт pushNotif), backend/src/routes/customSims.js (+POST /:id/share, POST /:id/clone, GET /:id/related, POST /:id/links, DELETE /:id/links/:linkId), js/api.js (+customSimShare/ Clone/Related/AddLink/DelLink), frontend/js/labs/lab-glue.js (аддитивно в IIFE LabCustom: кнопки share/clone/publish-toggle на карточках + делегат + shareToClass/clone/setStatus, ICON-блок), frontend/js/sim-builder.js (тулбар: «Шаблон»/«Раздать»/publish-toggle; методы setStatus/ openShareModal/openTemplateModal; данные TEMPLATES×4; ICON.template/unpublish), backend/tests/custom-sims-share.test.js (new, 15 it, все зелёные).
    • РЕШЕНИЕ копия-vs-доступ (зафиксировано): published custom-sim видна ВСЕМ в каталоге /lab (list/get отдают published любому; custom-sim НЕ гейтится allowlist'ом content_access 'sim' — тот гейтит только legacy lab_sims). Поэтому «раздать классу» = (1) авто-публикация (status→published), (2) ДОЛГОВЕЧНОЕ адресное уведомление ученикам класса через pushNotif (notifications-таблица + SSE) со ссылкой /lab?sim=custom:<id>. БЕЗ копии (в отличие от «Моих материалов», где оригинал приватный и копия обязательна) и БЕЗ записи content_access.
    • Привязка к программе: переиспользован lab_sim_links с sim_id='custom:<id>' (sim_id TEXT — отдельная таблица не нужна). Связями СВОЕЙ симуляции управляет владелец/admin (не только admin как у lab_sims). Backend + GET /related готовы; UI-редактор связей + чипы в каталоге — остаток (handoff).
    • Клон: копия spec вызвавшему как draft (title += ' (копия)', version=1). Источник: своя любая ИЛИ чужая published (чужой draft → 403).
    • Верификация: node --check всех 6 изм. файлов OK; эмодзи нет (скан — только / в комментариях, как в существующем коде); eval/Function нет; npm run lint:routes 0 unprotected (baseline 0); npm test 216/224 pass (8 fail = тот же baseline: 3 auth.test + 5 page-тестов без jsdom — не моя фаза; обе custom-sims-сьюты зелёные). git status: только мои файлы; classroom.html/lab.html не тронуты.
  • Фаза 5 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только аддитивные правки двух файлов параллельной сессии (без рефактора их кода): рабочее дерево по ним было ЧИСТЫМ до начала. classroom.html / backend / _sim_deps.js НЕ тронуты.
    • frontend/js/labs/lab-init.js (+7 строк): в начало openSim(id) добавлен хук if (window.LabCustom && LabCustom.resolveId) id = LabCustom.resolveId(id) || id; — переводит deep-link/клик custom:<dbid> в реестровый id customsim_<dbid> (LabRegistry.get/has обрезают часть после :, поэтому в реестре двоеточие недопустимо). Для встроенных id — no-op.
    • frontend/js/labs/lab-glue.js: (а) renderSims() merge +&& !m._custom (custom не в основной сетке) и вызов LabCustom.renderSection(_catFilter); (б) init-блок (non-embed и embed) зовёт LabCustom.init(), отложенное открытие ?sim=custom:* до загрузки списка; (в) новый IIFE window.LabCustom в конце файла.
    • Поток: LS.customSimsList() (мета без spec) → _registerLazy кладёт в LabRegistry манифест-заглушку customsim_<dbid> (_custom:true) с ленивым open(). Секция «Мои симуляции» #custom-sim-section (создаётся динамически в #lab-home, без правок lab.html/CSS) рендерит карточки из _meta. Открытие: resolveId → дисп. реестра → open() заглушки → ensureSpec(dbid) (LS.customSimGet, кэш+дедуп) → spec.id=regIdregisterSpecSim(spec) (Ф0-адаптер, заменяет заглушку на месте) → setActive(real)+real.open(ctx) (монтирует SimEngine). spec лениво — на старте /lab не грузится. Движок (_sim_*) уже eager (Ф0), ленивый файл не нужен.
    • Карточка: preview-SVG + cat-бейдж + бейджи «Моя»(owner)/«Опубликована»(status)/«Черновик»
      • кнопки «Редактировать»→/sim-builder?id=<dbid> / «Удалить»→LS.customSimDelete (владельцу, owner_id===user.id). Делегированный клик по #custom-sim-section. Иконки — inline SVG .ic.
    • Верификация: node --check обоих изменённых файлов OK; эмодзи нет (скан кодпойнтов — только math/box-drawing глифы ∑/═/─/→, как в существующем коде); eval/Function нет; headless-смоук (vm + DOM/SimEngine/LS-стабы, РЕАЛЬНЫЕ _registry.js+_sim_adapter.js) 22/22: resolveId, регистрация ленивых манифестов+флаг _custom, секция/карточки, бейджи, owner-only edit/del, deep-link data-open, lazy spec→registerSpecSim→mount, reopen синхронно, delete, встроенные не сломаны. git status: изменены только lab-init.js/lab-glue.js (+ плановые .md); classroom.html/backend чисты.
  • Фаза 4 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только новые файлы frontend/sim-builder.html + frontend/js/sim-builder.js + аддитивная правка js/sidebar.js (lab.html/lab-glue.js НЕ тронуты — зона параллельной сессии).
    • Учительский редактор /sim-builder (гейт teacher/admin через LS.initPage()): панели- аккордеоны (Мета+сцена / Параметры / Объекты / Графики / Физика) слева + живое превью (SimEngine.mount, перемонтаж с debounce 280мс) справа + тулбар (Тест/Сброс/Сохранить/ Опубликовать). window.SimBuilder.create({host,previewHost,panelHost,toolbarHost}).
    • Генерация спеки buildSpec() → JSON v1 (specVersion:1, meta, viewport, time, params[], objects[]+merged plots, physics?). _uid — UI-метка, вырезается; plot материализуется (range_a/range_b → range[a,b]); числовые поля — число ИЛИ строка-выражение (движок ест оба).
    • Выражения: каждое поле проверяется SimExpr.compile → inline-ошибка у поля; палитра функций/констант/параметров/id.x через модалку. Запрет имени param e (и pi/t/w/h/...).
    • Drag-on-preview: кнопка-«прицел» у объекта → клик/перетаскивание по inst.canvas (px→мир через inst._toWorld()) пишет x/y (или конец segment/vector) в свойства. Только на паузе.
    • Save/Load: customSimCreate/customSimUpdate (?id= → update + replaceState), публикация status:'published'; ?id=<id>customSimGetloadFromSim раскладывает по панелям.
    • Клиентская валидация зеркалит серверную (params≤50/objects≤200/walls≤20/springs≤50/ expr≤500/restitution 0..1/JSON≤200КБ) с дружелюбной модалкой-списком ошибок ДО запроса.
    • Сайдбар: пункт /sim-builder «Конструктор симуляций» (teacher-only, icon pencil-ruler) в группе «Практика и игры» после «Лаборатория» — минимальная правка js/sidebar.js.
    • Верификация: node --check обоих новых .js + извлечённого инлайна html OK; эмодзи нет (скан кодпойнтов, включая no-entry sign — заменён на текст); eval/Function нет (вычисления — SimExpr); headless-смоук (vm + DOM/Blob-стаб) 23/23: buildSpec форма, merge plot+range, strip _uid, physics-блок, валидация valid/reserved-e/syntax-error, loadFromSim round-trip стабилен. lab.html/lab-glue.js/_sim_engine.js/_sim_expr.js НЕ тронуты (git status).
  • Фаза 3 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только backend + клиент js/api.js (lab.html/lab-glue.js НЕ тронуты — зона параллельной сессии).
    • Миграция 071 backend/src/db/migrations/071_custom_sims.sql — таблица custom_sims (применена к живой БД через npm run migrate, без ошибок).
    • API /api/custom-sims (роутер backend/src/routes/customSims.js, контроллер backend/src/controllers/customSimController.js, смонтировано в server.js): GET / (свои+published), GET /:id (own ИЛИ published), POST / (teacher/admin), PUT /:id (owner/admin), DELETE /:id (owner/admin). Read — router-level authMiddleware; мутации — inline requireRole + per-row ownership.
    • validateSpec(spec) в контроллере — серверная валидация БЕЗ исполнения: ≤200KB, specVersion=1, лимиты (params≤50/objects≤200/walls≤20/springs≤50/expr≤500/глубина≤8/points≤1000), whitelist типов объектов, physics (restitution 0..1, dt 1/2000..1/30, mass>0), санитизация текст-полей (escape &<>). Возврат { ok, error?, clean? }.
    • Клиент js/api.js: customSimsList/Get/Create/Update/Deletereq(...), добавлены в window.LS.
    • Верификация: node --check всех новых/изменённых .js OK; npm run migrate OK; npm run lint:routes чисто (0 unprotected, baseline 0); backend/tests/custom-sims.test.js 24/24 pass; общий suite 201/209 (8 fail = 3 baseline auth.test.js + 5 page-тестов без devDep jsdom — окружение, не моя фаза). Эмодзи нет; БД через node:sqlite.
  • Фаза 2 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только _sim_engine.js + _sim_demo.js (lab.html/lab-glue.js НЕ тронуты — зона параллельной сессии).
    • Физический режим: блок physics:{ enabled, gravity:{x,y}, friction?, restitution?, dt?, walls?:[...], springs?:[...] } + body:{ mass, vx, vy, fixed } на point/circle. Фикс-шаговый полу-неявный Эйлер (накопитель dt, кламп шага/скорости), опора на математику _fx_motion.spring. Упругие столкновения круг-круг и круг-стена (restitution), пружины (Гук+демпф) между телами/якорями. Drag тел (тащишь — позиция, отпускаешь — бросок со скоростью). Тела сосуществуют с формульными объектами Ф0/Ф1.
    • env-поля тел: <id>.x/.y/.vx/.vy берутся из СОСТОЯНИЯ интегратора и кладутся в env первыми — снимает forward-ref проблему однопроходного env для тел.
    • Интегратор экспортирован как window.SimPhysics (для билдера/доски/headless). Отдельного файла _sim_physics.js НЕТ (нельзя подключить без правки lab.html — зона параллельной сессии); код внутри _sim_engine.js.
    • Демо за флагом: +customphys (пружинный маятник), +customballs (упругие шары). Гочи: имя param e зарезервировано (число Эйлера) — в демо «шары» упругость названа el.
    • Верификация: node --check обоих файлов OK; eval/Function — только в комментарии; эмодзи нет (скан кодпойнтов); headless (vm+DOM/canvas-стаб) 28/28: падение под гравитацией (парабола, без NaN), упругие шары (скорости меняются, тела в коробке, ограничены), пружинный маятник (колебания, без взрыва), drag тела (позиция+бросок), смешанная сцена (формульный point + segment на ball.x/y + readout ball.y живут вместе), SimPhysics.step raw.
  • Фаза 1 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только _sim_engine.js + _sim_demo.js (lab.html/lab-glue.js НЕ тронуты — зона параллельной сессии).
    • Новые типы объектов спеки: plot (график f(var) на canvas движка, trace — след по t), readout (живой бейдж, мягкая ошибка через evalSafe), vector с формой origin+dx/dy. drag на point/circle (drag:{param,axis,min,max,paramY}) — pointer events (мышь+тач), хит-тест в px (16px), двойной clamp (drag.min/max + диапазон параметра). Точные поля — в шапке _sim_engine.js и handoff phase-1.
    • Демо customdemo расширено: +слайдеры x0/y0, draggable-старт (axis xy), plot траектории, 2 readout (R, H). По-прежнему за флагом.
    • Верификация: node --check обоих файлов OK; eval/Function — только в комментарии, ни одного call-site; эмодзи нет (скан кодпойнтов); headless-тесты (vm + DOM-стаб): подготовка типов, vector end=origin+(dx,dy), plot evaluate, readout evalSafe, drag clamp+slider-sync, рендер всех 8 типов демо ×6 кадров без ошибок, trail/readout-слоты накапливаются корректно.
  • Фаза 0 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Ветка feature/sim-builder от master.
    • frontend/js/labs/_sim_expr.jswindow.SimExpr (безопасный движок выражений, без eval/Function; compile/evaluate/evalSafe/compileValue/parse/tokenize, whitelist + сравнения/логика/тернарник/multi-var env).
    • frontend/js/labs/_sim_engine.jswindow.SimEngine.mount(host, spec) -> { play, pause, reset, setParam, getParam, isRunning, destroy, el }. Canvas (мир→экран, Y вверх) + KaTeX-оверлей подписей + слайдеры/play/pause/reset. Формат спеки v1 задокументирован в шапке файла.
    • frontend/js/labs/_sim_adapter.jswindow.registerSpecSim(spec) / window.SimAdapter — строит манифест LabRegistry (ленивый хост #sim-spec-host-<id> в #lab-sim).
    • frontend/js/labs/_sim_demo.js — демо customdemo (бросок тела) за флагом ?simdemo=1 / ?sim=customdemo / LAB_SHOW_SPEC_DEMO / localStorage lab-spec-demo=1. Ученикам не светится.
    • Подключение в frontend/lab.html: 3 каркасных модуля eager после _graph_panel.js, демо после _register-all.js. _sim_deps.js НЕ тронут.
    • Верификация: node --check все 4 файла OK; eval/Function отсутствуют (только в комментариях); эмодзи нет; SimExpr self-test 29/30 (единственный «FAIL» -2^2=4 — это парити с graph.js).
  • Лаборатория уже декларативна на уровне регистрации: frontend/js/labs/_registry.js (LabRegistry.register/get/all/setActive/stop/destroy/resolvePreview), манифест с open(ctx)/mount(host)/stop/destroy. ~40 симуляций — рукописные JS-модули в frontend/js/labs/.
  • Каталог в БД: миграция 042_lab_sims.sql (lab_sims), роуты backend/src/routes/lab.js (GET /api/lab/sims, PATCH/:id, POST /reorder, links). Привязка к программе: 043_lab_sim_links.sql.

Архитектурные решения (зафиксированы при планировании)

  • Спека = JSON-данные. Версия specVersion. Корень: { specVersion, meta, viewport, params[], objects[], physics?, plots[], controls }.
  • Движок выражений безопасный — собственный парсер (расширение y=f(x) из graph.js): токенайзер → AST → eval по окружению { params, t, объекты, whitelisted Math fns }. Без eval/Function. Whitelist: + - * / ^ %, sin cos tan asin acos atan sqrt abs exp ln log min max floor ceil round sign pi e, сравнения, ?:.
  • Рантайм window.SimEngine.mount(host, spec) -> instance{ play, pause, reset, setParam, destroy }. Рендер: canvas для геометрии/трасс + SVG/absolute-div оверлей для подписей (KaTeX). Регистрируется в LabRegistry адаптером (одна функция строит манифест из спеки).
  • Объект: { id, type, ...props-with-bindings }. type ∈ point|segment|vector|circle|rect|polyline|path|label|image. Любое числовое свойство может быть числом ИЛИ строкой-выражением.
  • Физический режим (Фаза 2): объект с body:{ mass, vx, vy, fixed } интегрируется _fx_motion; силы physics:{ gravity, springs[], collisions, friction, walls }. Формульный и физический режимы сосуществуют (формульные объекты — кинематические).
  • Безопасность шаринга: published-спека валидируется на сервере (размер, схема, глубина AST, число объектов/параметров); подписи-строки санитизируются как svg/текст.

Temporary Workarounds

  • (нет)

Cross-Phase Dependencies

  • Ф1 (графики/drag) зависит от рантайма Ф0.
  • Ф2 (физика) зависит от Ф0 (модель объектов/цикл).
  • Ф4 (билдер) зависит от Ф0–Ф2 (что строить) + Ф3 (куда сохранять).
  • Ф5 (каталог) зависит от Ф3 (БД) + Ф0 (адаптер LabRegistry).
  • Ф6 (раздача) зависит от Ф3+Ф5.
  • Ф7 (доска) зависит от Ф0 (рантайм) + Ф5 (источник sim) + существующего simOpen/simState.

Implementation Notes

  • Каждая фаза должна оставлять /lab рабочим (Incremental).
  • Тестировать рантайм Ф0–Ф2 рукописными спеками-фикстурами (без билдера).
  • Reuse > переписывание: сначала смотреть _fx_motion, _graph_panel, graph.js.

RESUME STATE

  • РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md): P1+P2+P3 закоммичены; P4 «UI билдера + контролы стиля» РЕАЛИЗОВАН (рабочее дерево, не закоммичено — ждёт ревьюера/оркестратора). Файлы: только frontend/sim-builder.html + frontend/js/sim-builder.js. Дальше — независимый ревью P4, затем P5 (прямое манипулирование на сцене для всех типов + snap/выравнивание + undo/redo; правки _sim_engine.js
    • sim-builder.js). Контракт стилей/кривых из P2/P3-handoff полностью покрыт контролами билдера.
  • Последний коммит фичи: — (Ф0..Ф7 ВСЕ реализованы, ещё не закоммичены — ждут оркестратора)
  • Текущая фаза: Phase 7 — Доска онлайн-урока ( Implemented, pending commit) — ФИНАЛЬНАЯ. Дальше: Final Review (final-reviewer + security review) → коммит всех фаз → merge в master.
  • Ф7 файлы (аддитивно, рабочее дерево по ним было чистым до правок): backend/src/controllers/classroom/sim.js (simOpen принимает custom:<dbid> + access-check own|published|admin), frontend/classroom.html (пикер: свои+published custom через _crLoadCustomSims/ _crRenderSimGrid; id custom:<dbid>), frontend/js/labs/lab-glue.js (_bridgeCustomSimState — мост sim_state/apply_sim_state для custom-sim поверх SimEngine; вызов в _registerLazy.open). js/api.js НЕ менялся. Синхрон: параметры+play/pause (не время t). Открытие — iframe /lab?embed=1&sim=custom:<id>.
  • Эндпоинты Ф6: share/clone/related/links на /api/custom-sims/:id/*; клиент LS.customSimShare/ Clone/Related/AddLink/DelLink. Раздача = авто-publish + pushNotif (НЕ копия). Связи — lab_sim_links sim_id='custom:<id>'. Остаток Ф6: UI-редактор связей в билдере + чипы в каталоге (backend готов).
  • Файлы Ф5 (аддитивные правки зоны параллельной сессии — БЕЗ рефактора): frontend/js/labs/lab-init.js (+7 строк: хук LabCustom.resolveId в openSim), frontend/js/labs/lab-glue.js (renderSims +!m._custom и вызов renderSection; init зовёт LabCustom.init(); новый IIFE window.LabCustom). _sim_deps.js, classroom.html, backend — НЕ тронуты. Публичное API: window.LabCustom.{init,resolveId,renderSection,ensureSpec,del}.
  • id-неймспейс custom: deep-link/клик/data-open = custom:<dbid>; LabRegistry/host = customsim_<dbid>.
  • Режим: Automated / Orchestrator / Incremental
  • Файлы Ф4 (несведённые с параллельной сессией): frontend/sim-builder.html (new), frontend/js/sim-builder.js (new), js/sidebar.js (modify, аддитивный пункт /sim-builder). lab.html/lab-glue.js НЕ тронуты. Публичное API билдера: window.SimBuilder.create(...).
  • Номер миграции Ф3: 071 (071_custom_sims.sql); следующая свободная — 072.
  • Новые публичные API для следующих фаз: window.SimExpr, window.SimEngine.mount, window.SimPhysics (step/integrate/resolveCollisions), window.registerSpecSim / window.SimAdapter. Формат спеки v1 + типы plot/readout/drag/vector + блок physics/body/springs/walls — в шапке _sim_engine.js и в handoff phase-0/1/2.
  • API персистентности (Ф3): /api/custom-sims (GET /, GET/PUT/DELETE /:id, POST /) + клиент LS.customSimsList/Get/Create/Update/Delete. Контракт спеки на вход/санитизация — в handoff phase-3.
  • Файлы Ф2 (несведённые с параллельной сессией): frontend/js/labs/_sim_engine.js, frontend/js/labs/_sim_demo.js.
  • Файлы Ф3: backend/src/db/migrations/071_custom_sims.sql, backend/src/controllers/customSimController.js, backend/src/routes/customSims.js, backend/tests/custom-sims.test.js (new); backend/src/server.js, js/api.js (точечные добавления). lab.html/lab-glue.js НЕ тронуты.
  • Для Ф4 (билдер): слать/получать спеку через LS.customSimCreate/Update/Get; сервер вернёт спеку санитизированной (escaped-текст). Лимиты/коды 400 — см. handoff phase-3.