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

325 lines
47 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.
# Feature Context: Конструктор симуляций (SimForge)
## Current State
- **РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) ЗАВЕРШЁН — P5 «Прямое манипулирование + история» РЕАЛИЗОВАН**
(рабочее дерево, не закоммичено; ветка `feature/sim-builder`). Файл: ТОЛЬКО `frontend/js/sim-builder.js`.
`_sim_engine.js` НЕ тронут — `_toWorld`/`_toPx`/`_niceStep` уже публичны на инстансе движка, хука не
потребовалось (в IMPROVEMENTS.md P5 предполагались правки движка — не понадобились).
- **Прямое манипулирование** (`bindPreviewDrag` переписан): «ручки» через `handlesOf(obj)` для ВСЕХ
позиционируемых типов — point/circle/label/readout/rect (одна ручка x,y), segment/vector (origin x1,y1 +
end x2,y2 ИЛИ origin+dx/dy), polyline/path (по ручке на числовую вершину `points`). Хит-тест `pickHandle`
(14px, через `_toPx`); режимы pointerdown: `handle`/`place` (единств. ручка — клик ставит)/`body`
(несколько ручек — относительный сдвиг)/`none`. Поля-выражения `blocked` (не затираются). `refreshObjFields`
расширен на x1/y1/x2/y2/dx/dy/points.
- **Snap-к-сетке**: тумблер в тулбаре (`_snap`, `toggleSnap`, иконка `ICON.grid`, активность — инлайн
`SNAP_ACTIVE_CSS`); округление к `_niceStep(34)` (минорный шаг сетки; fallback 0.5). Выравнивание к чужим
координатам не делалось (бонус; snap достаточно — отмечено как частичное).
- **Undo/Redo**: стек `JSON.stringify(this.st)` (глубина 50), `pushHistory` (до мутации, без дублей, сброс
redo), `snapField` (один снапшот на сессию правки поля через focusin/`_fieldSnapTaken`). Структурные
операции — снапшот сразу; drag — один на сессию (no-op откатывается). Кнопки undo/redo (SVG `.ic`) +
Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y (`bindKeyboardShortcuts`, игнорит фокус в полях). `loadFromSim` обнуляет
историю; `_restoreSnapshot` → renderPanels + scheduleRemount.
- Верификация: `node --check` OK; эмодзи/eval — 0; vm-смоук 38/38 PASS (drag всех типов + body-move; snap;
защита выражений; undo/redo drag+add; лимит стека; round-trip идемпотентен). buildSpec/валидация не тронуты.
git status: тронут только sim-builder.js (`_sim_engine.js` в статусе — чужой коммит параллельной сессии
«goal/game», мной НЕ редактировался).
- **РАУНД УЛУЧШЕНИЙ (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=regId` → `registerSpecSim(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>` → `customSimGet` → `loadFromSim` раскладывает по панелям.
- **Клиентская валидация** зеркалит серверную (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/Delete` → `req(...)`, добавлены в `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.js` → `window.SimExpr` (безопасный движок выражений, без eval/Function; `compile/evaluate/evalSafe/compileValue/parse/tokenize`, whitelist + сравнения/логика/тернарник/multi-var env).
- `frontend/js/labs/_sim_engine.js` → `window.SimEngine.mount(host, spec) -> { play, pause, reset, setParam, getParam, isRunning, destroy, el }`. Canvas (мир→экран, Y вверх) + KaTeX-оверлей подписей + слайдеры/play/pause/reset. Формат спеки v1 задокументирован в шапке файла.
- `frontend/js/labs/_sim_adapter.js` → `window.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.