2 Commits

Author SHA1 Message Date
Maxim Dolgolyov 2067e6efb1 feat(labs): Фаза2 — сохранить кадр симуляции в «Мои материалы» + скачать PNG
Кнопки в топбаре лаборатории: снимок активного canvas → MaterialSave.image
(аплоад + kind:image в /api/materials) и «Скачать PNG». Захват — крупнейший
видимый canvas сцены. material-save.js подключён в lab.html.
(3D/WebGL-кадр может быть пустым без preserveDrawingBuffer — доработать позже.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:02:07 +03:00
Maxim Dolgolyov d1d52d806d chore(sim-builder): план фичи (8 фаз) — конструктор симуляций 2026-06-13 10:54:45 +03:00
12 changed files with 529 additions and 1 deletions
+59
View File
@@ -347,6 +347,16 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>
</button> </button>
<!-- save screenshot to «Мои материалы» -->
<button class="zoom-btn" id="lab-save-btn" onclick="labSaveToMaterials(this)" title="Сохранить кадр в «Мои материалы»">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
</button>
<!-- download PNG -->
<button class="zoom-btn" id="lab-png-btn" onclick="labDownloadPng()" title="Скачать кадр (PNG)">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</button>
<!-- sound toggle --> <!-- sound toggle -->
<button class="zoom-btn" id="labfx-sound-btn" onclick="(function(){var e=window.LabFX&&window.LabFX.sound;if(!e)return;e.setEnabled(!e.isEnabled());document.getElementById('labfx-sound-btn').setAttribute('aria-pressed',e.isEnabled());document.getElementById('labfx-sound-icon-on').style.display=e.isEnabled()?'':'none';document.getElementById('labfx-sound-icon-off').style.display=e.isEnabled()?'none':'';})()" title="Звук симуляций" style="position:relative" aria-pressed="true"> <button class="zoom-btn" id="labfx-sound-btn" onclick="(function(){var e=window.LabFX&&window.LabFX.sound;if(!e)return;e.setEnabled(!e.isEnabled());document.getElementById('labfx-sound-btn').setAttribute('aria-pressed',e.isEnabled());document.getElementById('labfx-sound-icon-on').style.display=e.isEnabled()?'':'none';document.getElementById('labfx-sound-icon-off').style.display=e.isEnabled()?'none':'';})()" title="Звук симуляций" style="position:relative" aria-pressed="true">
<!-- speaker on --> <!-- speaker on -->
@@ -412,6 +422,7 @@
</div><!-- /.app-layout --> </div><!-- /.app-layout -->
<script src="/js/api.js"></script> <script src="/js/api.js"></script>
<script src="/js/material-save.js"></script>
<script src="/js/sidebar.js"></script> <script src="/js/sidebar.js"></script>
<!-- ════════════════════════════════════════════════════════════════════════ <!-- ════════════════════════════════════════════════════════════════════════
Контент-движок, Фаза 3 — ЛЕНИВАЯ ЗАГРУЗКА КОДА СИМУЛЯЦИЙ. Контент-движок, Фаза 3 — ЛЕНИВАЯ ЗАГРУЗКА КОДА СИМУЛЯЦИЙ.
@@ -432,6 +443,11 @@
<script src="/js/labs/_fx_motion.js"></script> <script src="/js/labs/_fx_motion.js"></script>
<script src="/js/labs/_fx_sound.js"></script> <script src="/js/labs/_fx_sound.js"></script>
<script src="/js/labs/_graph_panel.js"></script> <script src="/js/labs/_graph_panel.js"></script>
<!-- Конструктор симуляций (Фаза 0): движок выражений + рантайм + адаптер LabRegistry.
Лёгкие модули каркаса (~30 КБ), грузятся eager как _registry/_loader. -->
<script src="/js/labs/_sim_expr.js"></script>
<script src="/js/labs/_sim_engine.js"></script>
<script src="/js/labs/_sim_adapter.js"></script>
<script src="/js/labs/_tasks.js"></script> <script src="/js/labs/_tasks.js"></script>
<script src="/js/labs/_phys_visuals.js"></script> <script src="/js/labs/_phys_visuals.js"></script>
<script src="/js/labs/_chem_visuals.js"></script> <script src="/js/labs/_chem_visuals.js"></script>
@@ -443,6 +459,9 @@
<script src="/js/lab-previews.js"></script> <script src="/js/lab-previews.js"></script>
<script src="/js/labs/lab-glue.js"></script> <script src="/js/labs/lab-glue.js"></script>
<script src="/js/labs/_register-all.js"></script> <script src="/js/labs/_register-all.js"></script>
<!-- Конструктор симуляций (Фаза 0): демо-спека за флагом (?simdemo=1). Грузится
после _register-all, чтобы LabRegistry/registerSpecSim уже существовали. -->
<script src="/js/labs/_sim_demo.js"></script>
<script> <script>
/* Sync sound toggle button icon with localStorage state on load */ /* Sync sound toggle button icon with localStorage state on load */
(function() { (function() {
@@ -468,6 +487,46 @@
off.style.display = eco ? 'none' : ''; off.style.display = eco ? 'none' : '';
btn.setAttribute('aria-pressed', eco ? 'true' : 'false'); btn.setAttribute('aria-pressed', eco ? 'true' : 'false');
})(); })();
/* ── Снимок симуляции: «В мои материалы» / «Скачать PNG» (Фаза 2) ──
Берём самый крупный видимый canvas в области симуляции. Для 3D (WebGL)
кадр может выйти пустым без preserveDrawingBuffer — допустимо для v1. */
function _labSimTitle() {
var t = document.getElementById('sim-topbar-title');
return t ? (t.textContent || '').trim() : '';
}
function _labActiveCanvas() {
var best = null, bestArea = 0;
document.querySelectorAll('canvas').forEach(function (c) {
if (c.offsetParent === null) return; // скрытый
var r = c.getBoundingClientRect();
if (r.width < 60 || r.height < 60) return; // мелкие (иконки/спарклайны)
var area = r.width * r.height;
if (area > bestArea) { bestArea = area; best = c; }
});
return best;
}
function labDownloadPng() {
var c = _labActiveCanvas();
if (!c) { if (window.LS && LS.toast) LS.toast('Нет изображения', 'warn'); return; }
try {
var a = document.createElement('a');
a.href = c.toDataURL('image/png');
a.download = (_labSimTitle() || 'simulation') + '.png';
document.body.appendChild(a); a.click(); a.remove();
} catch (e) { if (window.LS && LS.toast) LS.toast('Не удалось сохранить кадр', 'error'); }
}
function labSaveToMaterials(btn) {
var c = _labActiveCanvas();
if (!c) { if (window.LS && LS.toast) LS.toast('Нет изображения', 'warn'); return; }
if (!window.MaterialSave) { if (window.LS && LS.toast) LS.toast('Модуль сохранения не загружен', 'error'); return; }
try {
c.toBlob(function (blob) {
if (!blob) { if (window.LS && LS.toast) LS.toast('Не удалось снять кадр', 'error'); return; }
MaterialSave.image({ blob: blob, title: _labSimTitle() || 'Симуляция', name: 'sim.png', sourceTitle: 'Лаборатория' }, btn);
}, 'image/png');
} catch (e) { if (window.LS && LS.toast) LS.toast('Не удалось снять кадр', 'error'); }
}
</script> </script>
</body> </body>
</html> </html>
+42
View File
@@ -0,0 +1,42 @@
# Feature Context: Конструктор симуляций (SimForge)
## Current State
- Фаза 0 не начата. Ветка `feature/sim-builder` от `master`.
- Лаборатория уже декларативна на уровне регистрации: `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
- Последний коммит фичи: — (ещё нет)
- Текущая фаза: Phase 0 — Runtime core (Not Started)
- Режим: Automated / Orchestrator / Incremental
+68
View File
@@ -0,0 +1,68 @@
# Feature: Конструктор симуляций (SimForge)
**Branch:** `feature/sim-builder`
**Base branch:** `master`
**Created:** 2026-06-13
**Status:** 🟡 In Progress
**Strategy:** Incremental
**Mode:** Automated
**Execution:** Orchestrator
## Summary
Полноценный движок авторинга интерактивных 2D-симуляций для учителя-непрограммиста.
Учитель собирает симуляцию из **данных** (JSON-спека): параметры-слайдеры, объекты
(фигуры/векторы/точки/подписи с LaTeX), привязанные **формулами** к параметрам и времени
`t`; настоящая физика (гравитация/пружины/столкновения/трение); графики; перетаскивание.
Сохраняет в БД, публикует в каталог лаборатории, раздаёт классу, открывает на доске
онлайн-урока, клонирует чужие и стартует из шаблонов.
Спека — это **данные, не код**. Движок выражений — безопасный (whitelisted-математика),
⛔ без `eval`/`Function`/доступа к DOM/глобалам: спека шарится между людьми.
## Build & Test Commands
- **Build:** нет (vanilla JS, без бандлера)
- **Test:** `npm test``backend/`, `node --test tests/*.test.js`)
- **Lint:** `npm run lint:routes``backend/`)
- ⚠️ После роутов/миграций: `npm run migrate` (живая БД) + рестарт сервера (авто-перезагрузки нет).
- ⚠️ `npm test` имеет baseline 3 pre-existing fail (auth.test.js) — хук толерантен (BASELINE_FAILS=3).
## Project Constraints (соблюдают ВСЕ агенты)
- ⛔ Никаких эмодзи в коде — только inline SVG `.ic`.
- Поиск по коду: `ast-index` (символы/usages/callers) + `vex` (semantic). НЕ Grep tool.
- БД — встроенный `node:sqlite` (`DatabaseSync`), НЕ better-sqlite3. Живая БД `backend/data/learnspace.db`.
- Frontend — vanilla JS, `window.LS.*` (js/api.js), без бандлера. Статика через Express.
- Стейджить файлы поимённо (НЕ `git add -A` — в репо много мусорных untracked).
- Движок выражений — безопасный парсер, не `eval`/`new Function`.
- Переиспользовать: LabRegistry, `_fx_motion`, `_graph_panel`, `_phys_visuals`, `_util`,
парсер `y=f(x)` из graph.js; паттерн раздачи из «Мои материалы»; `lab_sim_links`;
конвейер встраивания sim на доску (`simOpen/simState/simMode/simAnnotate`).
## Phases
- [ ] Phase 0: Спека v1 + рантайм (формульные сцены) [domain: frontend] → [subplan](./phase-0-runtime-core.md)
- [ ] Phase 1: Графики + интеракции [domain: frontend] → [subplan](./phase-1-plots-interactions.md)
- [ ] Phase 2: Физический интегратор [domain: frontend] → [subplan](./phase-2-physics.md)
- [ ] Phase 3: БД + API (custom_sims) [domain: backend] → [subplan](./phase-3-persistence-api.md)
- [ ] Phase 4: Билдер (редактор) [domain: frontend] → [subplan](./phase-4-builder-ui.md)
- [ ] Phase 5: Каталог (custom-sims в /lab) [domain: fullstack] → [subplan](./phase-5-catalog.md)
- [ ] Phase 6: Раздача / шаблоны / клон / программа [domain: fullstack] → [subplan](./phase-6-sharing.md)
- [ ] Phase 7: Доска онлайн-урока [domain: fullstack] → [subplan](./phase-7-classroom.md)
## Phase Progress Log
| Phase | Domain | Status | Review | Build | Committed |
|-------|--------|--------|--------|-------|-----------|
| Phase 0: Runtime core | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 1: Plots & interactions | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 2: Physics | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 3: Persistence + API | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 4: Builder UI | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 5: Catalog | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 6: Sharing | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 7: Classroom | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
## Final Review
- [ ] Comprehensive code review (final-reviewer)
- [ ] Security review (safe expression eval, ownership, sanitization)
- [ ] Full test suite passes (within baseline)
- [ ] Merged to `master`
+48
View File
@@ -0,0 +1,48 @@
# Phase 0: Спека v1 + рантайм (формульные сцены)
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Заложить ядро: формат JSON-спеки v1, безопасный движок выражений, рантайм `SimEngine`,
адаптер регистрации в `LabRegistry`. После фазы рукописная спека «брошенное тело» играет в /lab.
## Tasks
- [ ] Задокументировать формат спеки v1 в шапке нового файла + в CONTEXT.md (params, objects, viewport, controls).
- [ ] `frontend/js/labs/_sim_expr.js` — безопасный движок выражений: токенайзер → AST → `evaluate(ast, env)`. Whitelist математики (см. CONTEXT.md). Парсер расширяет логику `y=f(x)` из `graph.js` (посмотреть и переиспользовать, не дублировать). ⛔ без `eval`/`Function`. `compile(src) -> {fn(env), error}`.
- [ ] `frontend/js/labs/_sim_engine.js``window.SimEngine.mount(host, spec)`:
- создаёт canvas (мир→экран трансформация по `viewport`) + оверлей-слой для подписей (KaTeX через существующий путь рендера формул);
- объекты: point|segment|vector|circle|rect|polyline|path|label (числовые свойства = число или строка-выражение, компилируются один раз);
- игровой цикл `requestAnimationFrame`: пересчёт `t`, перевычисление привязок, перерисовка;
- контролы: слайдеры-параметры (из `params[]`), кнопки play/pause/reset; API инстанса `{ play, pause, reset, setParam, destroy }`.
- [ ] `frontend/js/labs/_sim_adapter.js``registerSpecSim(spec)` строит манифест LabRegistry (`open(ctx)``SimEngine.mount`, `stop/destroy`, `preview` из спеки) и регистрирует.
- [ ] Фикстура-демо: рукописная спека «projectile» (угол/скорость слайдеры, точка x=v·cosθ·t, y=v·sinθ·t−5t²) — зарегистрировать как `customdemo` для проверки в /lab (за временным флагом/разделом, не светить ученикам).
- [ ] Подключить новые файлы в /lab (lazy через существующий `_loader`/`_sim_deps` или прямыми тегами — выбрать минимально-инвазивно, не ломая старт).
## Files to Modify/Create
- `frontend/js/labs/_sim_expr.js` — движок выражений (new)
- `frontend/js/labs/_sim_engine.js` — рантайм (new)
- `frontend/js/labs/_sim_adapter.js` — адаптер LabRegistry (new)
- `frontend/js/labs/_sim_demo.js` — демо-спека-фикстура (new, временная)
- `frontend/lab.html` или `_sim_deps.js` — подключение файлов (минимальная правка)
## Acceptance Criteria
- В /lab открывается демо-симуляция, слайдеры меняют движение, play/pause/reset работают.
- Движок выражений не использует eval/Function; некорректная формула не роняет рантайм (показывает ошибку/0).
- Существующие ~40 симуляций и старт /lab не сломаны.
## Notes
- Подписи с LaTeX — переиспользовать существующий рендер формул (KaTeX), не тянуть новый.
- Мир-координаты с осью Y вверх (математические), трансформация в экранные внутри движка.
- Производительность: компилировать выражения один раз при mount, в цикле только evaluate.
## Review Checklist
- [ ] Все задачи выполнены
- [ ] Нет eval/new Function в движке выражений
- [ ] Нет эмодзи (только SVG .ic)
- [ ] Старт /lab и существующие симуляции не регрессировали
- [ ] Код в стиле проекта (vanilla, IIFE-модули labs/)
## Handoff to Next Phase
<!-- Заполняет реализатор -->
@@ -0,0 +1,40 @@
# Phase 1: Графики + интеракции
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Добавить в рантайм графики (plot-объекты), перетаскиваемые ручки (drag → параметр),
векторы и числовые readout. После фазы спека со слайдером, draggable-точкой и live-графиком работает.
## Tasks
- [ ] Plot-объект в спеке: `{ type:'plot', expr:'...', var:'x', range:[a,b], samples, trace? }`
рисует график выражения; `trace:true` — накапливает след по `t`. Переиспользовать `_graph_panel.js` (посмотреть API).
- [ ] Draggable-ручка: объект/маркер с `drag:{ param:'<name>', axis:'x|y|xy', min,max }` — перетаскивание мышью/тачем меняет параметр (и наоборот, позиция следует за параметром). Хит-тест в мир-координатах.
- [ ] Readout: `{ type:'readout', label, expr, unit, precision }` — живое значение выражения как текст/бейдж.
- [ ] Vector-объект с привязкой к (origin, dx, dy)-выражениям + стрелка.
- [ ] Тач-поддержка drag (pointer events), не ломая существующую логику доски/лабы.
- [ ] Обновить демо-спеку: добавить слайдер, draggable-точку, plot + readout.
## Files to Modify/Create
- `frontend/js/labs/_sim_engine.js` — типы plot/readout/vector, drag-интеракции (modify)
- `frontend/js/labs/_sim_demo.js` — расширить демо (modify)
## Acceptance Criteria
- Перетаскивание ручки меняет параметр; зависимые объекты/график обновляются.
- График строится по выражению; trace накапливает след во времени.
- Readout показывает живое значение. Тач работает.
## Notes
- Drag не должен конфликтовать с pan/zoom рантайма (если есть). Приоритет хит-теста — ручки.
- Сэмплинг графика разумный (без фриза на больших range).
## Review Checklist
- [ ] Все задачи выполнены
- [ ] Drag работает мышью и тачем
- [ ] Нет регрессий рантайма Ф0
- [ ] Нет эмодзи, стиль проекта
## Handoff to Next Phase
<!-- Заполняет реализатор -->
+42
View File
@@ -0,0 +1,42 @@
# Phase 2: Физический интегратор
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Добавить настоящую физику: тела с массой, гравитация/пружины/столкновения/трение,
перетаскивание тел силой, траектории. Динамика считается движком, а не формулой.
После фазы маятник/столкновения/брошенное тело идут динамически из спеки.
## Tasks
- [ ] Блок `physics` в спеке: `{ enabled, gravity:{x,y}, friction, walls:[...], restitution }`.
- [ ] Тело-объект: `body:{ mass, vx, vy, fixed }` — интегрируется (опора на `_fx_motion.js`, посмотреть API; не дублировать интегратор).
- [ ] Пружины: `springs:[{ a, b, k, length }]` (между телами или телом и точкой-якорем).
- [ ] Столкновения: упругие шары/стены (restitution), базовый бродфейз достаточно (N небольшое).
- [ ] Drag тела: перетаскивание задаёт позицию/скорость (отпустил — летит). Кинематические (формульные) объекты Ф0 сосуществуют с физическими.
- [ ] Траектория: накопление следа центра тела (toggle в спеке).
- [ ] Демо-спеки: «маятник» (груз+нить как пружина/констрейнт), «упругие шары».
## 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, без своего дубля интегратора без причины
- [ ] Стабильность (нет взрыва энергии на разумных параметрах)
- [ ] Нет регрессий Ф0/Ф1
## Handoff to Next Phase
<!-- Заполняет реализатор -->
@@ -0,0 +1,51 @@
# Phase 3: БД + API (custom_sims)
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Сохранение custom-симуляций: таблица БД, CRUD API под авторизацией с проверкой владения,
серверная валидация спеки. После фазы спека сохраняется/грузится/удаляется через API.
## Tasks
- [ ] Миграция `backend/src/db/migrations/0NN_custom_sims.sql` (следующий свободный номер):
таблица `custom_sims` (id, owner_id FK users ON DELETE CASCADE, title, description, subject,
grade, cat, spec_json TEXT, status TEXT 'draft|published' DEFAULT 'draft', version INT,
created_at, updated_at) + индекс по owner_id, status.
- [ ] Контроллер `backend/src/controllers/customSimController.js`: list (own + published), get,
create, update, remove. Владение проверяется на mutate (owner или admin).
- [ ] Серверная валидация спеки `validateSpec(spec)`: размер JSON, `specVersion`, лимиты
(число params/objects, глубина строк-выражений), типы объектов из whitelist, строки-подписи
обрезаются/санитизируются. Отказ с 400 при нарушении. ⛔ Никакого исполнения спеки на сервере.
- [ ] Роуты `backend/src/routes/customSims.js`: `GET /api/custom-sims` (свои+published),
`GET /api/custom-sims/:id`, `POST /` (teacher/admin), `PUT /:id`, `DELETE /:id`. Смонтировать в server.js под authMiddleware + фича-гейт при необходимости.
- [ ] Клиент `js/api.js`: `customSimsList/Get/Create/Update/Delete` + добавить в `window.LS`.
- [ ] Тесты `backend/tests/custom-sims.test.js`: CRUD, ownership (403 чужой), валидация (400 кривая спека). Использовать `seedRow()` паттерн (устойчив к дрейфу схемы).
## Files to Modify/Create
- `backend/src/db/migrations/0NN_custom_sims.sql` (new)
- `backend/src/controllers/customSimController.js` (new)
- `backend/src/routes/customSims.js` (new)
- `backend/src/server.js` — монтирование роутера (modify)
- `js/api.js` — клиентские методы (modify)
- `backend/tests/custom-sims.test.js` (new)
## Acceptance Criteria
- POST сохраняет спеку, GET возвращает свои+published, PUT/DELETE только владельцу/админу (403 иначе).
- Кривая/огромная спека → 400. Тесты зелёные (в пределах baseline).
- `npm run migrate` применяет таблицу; роут не отдаёт SPA-fallback после рестарта.
## Notes
- node:sqlite (DatabaseSync), НЕ better-sqlite3.
- Спека хранится как TEXT(JSON); парс/валидация — на входе.
- Не делать blanket `router.use(requireRole('admin'))` — read-роуты auth-only, мутации — inline requireRole.
## Review Checklist
- [ ] Все задачи выполнены
- [ ] Ownership и валидация спеки покрыты тестами
- [ ] Миграция идемпотентна (IF NOT EXISTS / INSERT OR IGNORE где надо)
- [ ] `npm run lint:routes` чисто; тесты в пределах baseline
## Handoff to Next Phase
<!-- Заполняет реализатор -->
+46
View File
@@ -0,0 +1,46 @@
# Phase 4: Билдер (редактор)
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Учительский редактор: страница с живым превью и панелями для сборки спеки без кода.
После фазы учитель собирает рабочую симуляцию с нуля в UI и сохраняет в БД (Ф3).
## Tasks
- [ ] Страница `frontend/sim-builder.html` (доступ teacher/admin; сайдбар как у других страниц).
- [ ] Левая/правая панель + центр-превью (встроенный `SimEngine` инстанс, перемонтаж при правках).
- [ ] Панель **Параметры**: добавить/удалить параметр (имя, min, max, step, начальное, единица) → слайдер в превью.
- [ ] Панель **Объекты**: добавить объект (тип из whitelist), редактор свойств; числовые поля
принимают число ИЛИ выражение; палитра-помощник функций/параметров; подпись с LaTeX.
- [ ] Панель **Графики/Физика**: добавить plot (expr/var/range/trace); тумблер физики + её поля (Ф2).
- [ ] Размещение объектов мышью на превью (клик-поставить, drag-двигать) с синхроном в свойства.
- [ ] Мета: заголовок, описание, предмет, класс, категория, превью-картинка (опц.).
- [ ] Save/Load через `LS.customSims*` (Ф3): новый / редактировать существующий; кнопка «Тест» (play inline).
- [ ] Валидация на клиенте (понятные ошибки до сохранения) + показ ошибок выражений.
## Files to Modify/Create
- `frontend/sim-builder.html` (new) — страница + инлайн-логика редактора
- `frontend/js/labs/_sim_engine.js` — при необходимости hook'и для билдера (live re-mount, highlight) (modify, минимально)
- `js/api.js` — если нужны доп. методы (modify, опц.)
- ссылка в сайдбаре/навигации для учителя (modify соответствующего include)
## Acceptance Criteria
- Учитель с нуля добавляет параметры/объекты/график, видит живое превью, сохраняет, открывает заново и видит то же.
- Ошибка в выражении показывается понятно, не роняет редактор.
- Нет эмодзи, дизайн в системе ls.css.
## Notes
- Прагматично: форма-панели + лёгкий drag на превью. НЕ полноценный node-граф.
- Это frontend-фаза — использовать гайдлайны frontend-design.
- Большой файл — держать логику модульной (можно вынести в `frontend/js/sim-builder.js`).
## Review Checklist
- [ ] Все задачи выполнены
- [ ] Полный цикл build→save→reload→edit работает
- [ ] Доступ только teacher/admin
- [ ] Нет эмодзи; дизайн-система соблюдена
## Handoff to Next Phase
<!-- Заполняет реализатор -->
+42
View File
@@ -0,0 +1,42 @@
# Phase 5: Каталог (custom-sims в /lab)
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
## Objective
Сохранённые custom-симуляции появляются и играют в /lab наравне со встроенными;
раздел «Мои симуляции», редактирование/удаление из каталога, deep-link.
## Tasks
- [ ] При загрузке /lab подтягивать custom-sims (свои + published) из `GET /api/custom-sims`
и регистрировать через `registerSpecSim` (Ф0-адаптер) с id `custom:<dbid>`.
- [ ] Карточки в каталоге: категория/предмет/класс из меты; бейдж «Моя»/«Опубликована».
- [ ] Раздел/фильтр «Мои симуляции» в /lab.
- [ ] Кнопки на карточке custom-sim: «Редактировать» → `sim-builder.html?id=<id>`, «Удалить» (владельцу).
- [ ] Deep-link `/lab?sim=custom:<id>` открывает напрямую (расширить существующий `LAB_SIM_ALIASES`/openSim).
- [ ] Ленивая загрузка движка (`_sim_*.js`) — только когда открыта custom-sim (через `_loader`/`_sim_deps`).
## Files to Modify/Create
- `frontend/js/labs/lab-glue.js` и/или `lab-init.js` — загрузка+регистрация custom-sims, карточки, фильтр (modify)
- `frontend/js/labs/_sim_deps.js``_sim_*.js` в ленивые зависимости (modify)
- `js/api.js` — при необходимости (modify, опц.)
## Acceptance Criteria
- Сохранённая в Ф4 симуляция видна в /lab, открывается и играет.
- «Мои симуляции» показывает свои (вкл. draft); published видят и другие.
- Edit/Delete с карточки работают; deep-link открывает.
- Старт /lab не тормозит (движок грузится лениво).
## Notes
- НЕ ломать существующий каталог встроенных (lab_sims) — custom-список добавляется поверх.
- id-неймспейс `custom:` чтобы не конфликтовать со встроенными.
## Review Checklist
- [ ] Все задачи выполнены
- [ ] Встроенные симуляции и старт /lab не регрессировали
- [ ] Draft видит только владелец; published — все
- [ ] Ленивая загрузка движка работает
## Handoff to Next Phase
<!-- Заполняет реализатор -->
+47
View File
@@ -0,0 +1,47 @@
# Phase 6: Раздача / шаблоны / клон / программа
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
## Objective
Экосистема вокруг custom-sims: публикация, раздача классу, клонирование чужих,
старт из шаблонов, привязка к программе (учебник/тема).
## Tasks
- [ ] Публикация: тумблер draft↔published в билдере/каталоге (PUT status). Только владелец/админ.
- [ ] Раздача классу: `POST /api/custom-sims/:id/share { classId }` — по паттерну «Мои материалы»
(`shareMaterial`): ученики класса получают доступ/уведомление. Решить — ссылка-доступ или копия
(рекоменд.: доступ-ссылка на published + запись в lab_sim_links/доступ; копия избыточна).
- [ ] Клон: `POST /api/custom-sims/:id/clone` — копия спеки новому владельцу (draft). Кнопка «Клонировать» на чужой published-карточке.
- [ ] Шаблоны: набор стартовых спек (встроенные JSON-фикстуры: пустая, маятник, график, бросок) →
«Создать из шаблона» в билдере; «Создать из существующей» = clone.
- [ ] Привязка к программе: переиспользовать `lab_sim_links` (kind=textbook|topic) для `custom:<id>`;
чип «Связано с программой» (как у встроенных, `_loadRelated`) и кнопка «В лабораторию» с карточки учебника.
- [ ] Тесты: share (доступ ученику), clone (новый владелец, draft), ownership на публикации.
## Files to Modify/Create
- `backend/src/controllers/customSimController.js` — share/clone/publish (modify)
- `backend/src/routes/customSims.js` — роуты share/clone (modify)
- `backend/src/controllers/.../lab links` — связи для custom (reuse `lab.js` links, modify при необходимости)
- `frontend/sim-builder.html` / `frontend/js/labs/lab-glue.js` — шаблоны, кнопки публикации/клона/раздачи (modify)
- `js/api.js` — методы share/clone (modify)
- `backend/tests/custom-sims-share.test.js` (new)
## Acceptance Criteria
- Учитель публикует; другой учитель видит и клонирует к себе (draft).
- Выданная классу симуляция доступна ученикам класса (с уведомлением).
- Старт из шаблона создаёт рабочую заготовку. Привязка к учебнику показывает чип/кнопку.
## Notes
- Раздача — переиспользовать существующий механизм доступа/уведомлений, не строить новый.
- Решение копия-vs-ссылка зафиксировать в CONTEXT.md.
## Review Checklist
- [ ] Все задачи выполнены
- [ ] Ownership на publish/share/clone покрыт тестами
- [ ] Ученик класса получает доступ; чужой — нет
- [ ] Reuse материалов/доступа/links, без дублей
## Handoff to Next Phase
<!-- Заполняет реализатор -->
+43
View File
@@ -0,0 +1,43 @@
# Phase 7: Доска онлайн-урока
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
## Objective
Открывать custom-симуляцию на доске онлайн-урока через существующий конвейер встраивания
sim, с синхроном параметров классу и аннотациями поверх.
## Tasks
- [ ] Учитель в classroom выбирает sim для доски: добавить в список источников свои+published custom-sims (рядом со встроенными).
- [ ] Открытие на доске через существующий `simOpen` (controller `classroom/sim.js`, роут `/:id/sim`) —
для custom передаётся `custom:<id>`; рантайм `SimEngine` монтируется в sim-контейнер доски.
- [ ] Синхрон состояния: параметры/play-pause/время транслировать классу через `simState/simMode`
(как для встроенных) — ученики видят те же значения слайдеров и фазу анимации.
- [ ] Аннотации поверх — через существующий `simAnnotate` (без изменений конвейера).
- [ ] Закрытие/смена sim корректно размонтирует `SimEngine` (destroy).
- [ ] Тест/проверка: открыть custom-sim на доске, подвигать слайдер у учителя → у ученика синхрон.
## Files to Modify/Create
- `frontend/classroom.html` — выбор custom-sim в источниках доски, монтаж SimEngine, проброс состояния (modify)
- `backend/src/controllers/classroom/sim.js` — поддержать `custom:<id>` (валидация доступа к published/own) (modify)
- `js/api.js` — при необходимости (modify, опц.)
## Acceptance Criteria
- Учитель открывает custom-sim на доске; ученики видят её синхронно (параметры/анимация/режим).
- Аннотации поверх работают; закрытие чистит ресурсы.
- Существующее встраивание встроенных sim не регрессировало.
## Notes
- Reuse simOpen/simState/simMode/simAnnotate — НЕ строить новый канал синхрона.
- Доступ: на доску можно класть только свои или published (проверка на сервере).
- classroom.html большой (8240 строк) — править точечно.
## Review Checklist
- [ ] Все задачи выполнены
- [ ] Синхрон параметров учитель→ученики работает
- [ ] Доступ к custom-sim на доске проверяется
- [ ] Встроенные sim на доске не сломаны; SimEngine корректно размонтируется
## Handoff to Next Phase
<!-- Финальная фаза -->
+1 -1
View File
@@ -70,7 +70,7 @@
## Прогресс ## Прогресс
- [x] Фаза 0 (фундамент заложен) — эконом-режим/reduced-motion (LabFX, тумблер), выбор симуляции из списка в редакторе урока, удалён мёртвый `SimUtil`, добавлены `LabPalette` (_palette.js) и `SimBase` (_simbase.js) как опциональные основания. **Адаптация симуляций к SimBase/LabPalette и удаление «дробовика» `_pauseAllSims/closeSim` — постепенно, по мере правок каждой симуляции (требует поштучной проверки, нет фронт-тестов).** - [x] Фаза 0 (фундамент заложен) — эконом-режим/reduced-motion (LabFX, тумблер), выбор симуляции из списка в редакторе урока, удалён мёртвый `SimUtil`, добавлены `LabPalette` (_palette.js) и `SimBase` (_simbase.js) как опциональные основания. **Адаптация симуляций к SimBase/LabPalette и удаление «дробовика» `_pauseAllSims/closeSim` — постепенно, по мере правок каждой симуляции (требует поштучной проверки, нет фронт-тестов).**
- [~] Фаза 1 — сделано: фреймворк `LabTasks` (_tasks.js) + интеграция в теорию; задания на 17 симуляций. Осталось: XP за задания, deep-link на §, наполнение остальных. - [~] Фаза 1 — сделано: фреймворк `LabTasks` (_tasks.js) + интеграция в теорию; задания на 17 симуляций. Осталось: XP за задания, deep-link на §, наполнение остальных.
- [ ] Фаза 2 - [~] Фаза 2 — сделано: «Сохранить кадр в Мои материалы» + «Скачать PNG» в топбаре лаборатории (захват активного canvas, переиспользует MaterialSave.image). Осталось: сохранение/возобновление параметров симуляции, измерительные инструменты. (3D/WebGL-кадр может быть пустым без preserveDrawingBuffer — доработать.)
- [ ] Фаза 3 - [ ] Фаза 3
- [ ] Фаза 4 - [ ] Фаза 4
- [ ] Фаза 5 - [ ] Фаза 5