fix(biochem 3D): корректная глубина + объёмные связи-цилиндры

Два дефекта, из-за которых 3D читался как плоская диаграмма:
- painter-сортировка была по возрастанию z (ближние первыми) — дальние
  атомы рисовались поверх ближних. Теперь единый список примитивов
  (атомы + половинки связей) сортируется по убыванию z (дальние первыми).
- связи были тонкими плоскими линиями. Теперь — затенённые «цилиндры»:
  толстый штрих с поперечным градиентом (центр светлее, края темнее),
  двухцветные (каждая половина под цвет своего атома) — фирменный вид
  ball-and-stick. Ширина зависит от перспективы (ближе — толще).
- усилена перспектива (fov 900→700), добавлен тёмный ободок сфер для объёма.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 12:58:39 +03:00
parent 3b6481b1df
commit 410eb8a862
12 changed files with 559 additions and 27 deletions
+61
View File
@@ -0,0 +1,61 @@
# Feature Context: Контент-движок лаборатории
## Current State
- Лаборатория работает на захардкоженной регистрации (см. PLAN.md Summary).
- Ветка `feature/lab-content-engine` создана от `master`.
## Architecture map (как было ДО рефактора)
- `frontend/lab.html` — sim-тела `<div id="sim-xxx">` (inline HTML, ~3000 строк) + 58 `<script>` тегов (4800-4861) + three.js.
- `frontend/js/labs/lab-glue.js`:
- `_catFilter`, `_disabledSimIds`, `_simModuleDisabled` (вкл/выкл из админки)
- `filterSims()`, `renderSims()` (карточки каталога)
- preview-хелперы `_grid/_axes/_svg` + ~60 констант `P_*`
- массив `SIMS` (821-866), `window.SIMS`/`window.LAB_SIMS`
- `frontend/js/labs/lab-init.js`:
- объявления переменных симуляций (gSim, pSim, …)
- `ALL_SIM_BODIES` / `ALL_CTRL_BARS` (33-48)
- `_pauseAllSims()` (54-91), `openSim(id)` if-цепочка (93-160), `closeSim()` (212-258)
- `_simShow()`, `_addTouchSupport()` (touch-bridge + ResizeObserver)
- объект `THEORY` + `loadTheory()` + `_theoryToggle()`
- функции `_openXxx()` (603-756) — единый шаблон: `_simShow('sim-xxx')` + ленивое `new XxxSim(...)` + показ `ctrl-xxx`
- `frontend/js/admin/sections/sims.js` — админ-секция (пока только вкл/выкл, `_disabledSimIds`).
## Загрузочный порядок (КРИТИЧНО)
В lab.html: движки `_fx_*`, `_phys_visuals`, `_graph_panel`, `_chem_visuals` грузятся ПЕРЕД симуляциями.
`lab-init.js` (4826) грузится ПЕРЕД `lab-glue.js` (4827). `renderSims()` вызывается в конце lab-glue.
Некоторые sim-файлы (graph.js) грузятся РАНЬШЕ lab-glue.js → preview `P_*` ещё не определены на момент исполнения их тел.
=> В манифестах `preview` поддерживает функцию (ленивое вычисление в renderSims), не только строку.
## Контракт LabRegistry (Фаза 0)
```
LabRegistry.register(manifest) // manifest.id уникален; повторная регистрация перезаписывает
LabRegistry.get(id) // по base-id (без ':arg')
LabRegistry.has(id)
LabRegistry.all() // в порядке регистрации
LabRegistry.setActive(sim) / stopActive() / destroyActive() // менеджер жизненного цикла
```
manifest: `{ id, cat, title, desc, preview(string|fn), theory?, bodyId?, mount?(host), open(ctx), stop?(), destroy?(), subject?, grade?, topics? }`
## Адаптер (Фаза 0): реестр в приоритете, иначе legacy
- `renderSims()` — порядок берём из исходного `SIMS`; для id, который есть в реестре, используем манифест (resolve preview), иначе legacy-запись; в конце добавляем registry-only записи, которых нет в SIMS.
- `openSim(id)``base = id.split(':')[0]`; если `LabRegistry.has(base)``stopActive()`; `get(base).open({arg})`; `setActive`; иначе старый if-путь.
- `loadTheory(id)` — если `get(base).theory` есть → рендерим из него; иначе `THEORY[base]`.
- `closeSim()`/`_pauseAllSims()` — дополнительно `LabRegistry.stopActive()` / `destroyActive()`.
## Temporary Workarounds
- (пока нет)
## Cross-Phase Dependencies
- Фаза 1 опирается на ядро реестра из Фазы 0.
- Фаза 3 (ленивая загрузка) опирается на манифесты с зависимостями движков (Фаза 1/2).
- Фаза 4 (БД) мёржит код-манифесты Фазы 1 с оверрайдами.
- Фаза 5 использует поля subject/grade/topics из манифестов.
## Deep-links (сохранить!)
`openSim('stereo:figure')`, `?stereofig=`, обратная совместимость `magnetic/coulomb→emfield`, `thinlens/mirrors/refraction→opticsbench`.
## Проектные правила (НЕ нарушать)
- Иконки: только inline SVG `.ic`, НЕ эмоджи.
- Поиск по коду: ast-index, НЕ Grep tool.
- БД: встроенный `node:sqlite` DatabaseSync, НЕ better-sqlite3.
- Git: коммитить только изменённые файлы.
+53
View File
@@ -0,0 +1,53 @@
# Feature: Контент-движок лаборатории (симуляции как данные)
**Branch:** `feature/lab-content-engine`
**Base branch:** `master`
**Created:** 2026-05-30
**Status:** 🟡 In Progress
**Strategy:** Big Bang
**Mode:** Automated
**Execution:** Direct
## Summary
Превратить захардкоженную в 6 местах регистрацию ~49 симуляций лаборатории в единый
декларативный манифест + реестр (`LabRegistry`). Каждая симуляция сама себя регистрирует
объектом `{id, cat, title, desc, preview, theory, bodyId/mount, open, stop, destroy, subject, grade, topics}`.
Ядро (renderSims/openSim/closeSim/loadTheory) работает с реестром, а не с массивами и
if-цепочками. Далее — ленивая загрузка кода, БД-бэкенд с админкой и курикулумная привязка.
## Build & Test Commands
- **Build:** — (фронт без сборки, статика через Express)
- **Test:** `cd backend && npm test` (актуально для Фаз 4-5; Фазы 0-3 — статическая проверка + ревью по диффу)
- **Lint:** `cd backend && npm run lint:routes` (актуально для Фаз 4-5)
## Phases
- [ ] Phase 0: Ядро реестра + адаптер + 3 пилота [domain: frontend] → [subplan](./phase-0-registry-core.md)
- [ ] Phase 1: Миграция всех симуляций на манифесты [domain: frontend] → [subplan](./phase-1-migrate-all.md)
- [ ] Phase 2: Тела симуляций как шаблоны + ленивый mount [domain: frontend] → [subplan](./phase-2-lazy-mount.md)
- [ ] Phase 3: Ленивая загрузка кода симуляций [domain: frontend] → [subplan](./phase-3-lazy-load.md)
- [ ] Phase 4: Реестр в БД + API + админка [domain: fullstack] → [subplan](./phase-4-db-admin.md)
- [ ] Phase 5: Курикулумная привязка [domain: fullstack] → [subplan](./phase-5-curriculum.md)
## Phase Progress Log
| Phase | Domain | Status | Review | Build | Committed |
|-------|--------|--------|--------|-------|-----------|
| Phase 0: Ядро реестра | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 1: Миграция всех | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 2: Ленивый mount | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 3: Ленивая загрузка | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 4: БД + админка | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 5: Курикулум | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
## Final Review
- [ ] Comprehensive code review
- [ ] Full build passes
- [ ] Full test suite passes
- [ ] Merged to `master`
## Notes (Big Bang temporary breakage map)
- Фаза 1 может временно ломать каталог/открытие симуляций пока миграция не завершена — устраняется внутри Фазы 1.
- Фаза 2 временно меняет структуру lab.html (вынос тел) — устраняется внутри Фазы 2.
- Полная работоспособность лаборатории гарантируется после ФИНАЛЬНОЙ фазы.
@@ -0,0 +1,46 @@
# Phase 0: Ядро реестра + адаптер + 3 пилота
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Создать `LabRegistry` (реестр + менеджер активной симуляции). Подключить адаптер: ядро
лаборатории сначала смотрит в реестр, иначе — старый путь. Мигрировать 3 пилота
(graph, quadratic, pendulum) и доказать паритет. Полностью обратимо.
## Tasks
- [ ] Создать `frontend/js/labs/_registry.js``window.LabRegistry` (register/get/has/all + setActive/stopActive/destroyActive). Без эмоджи.
- [ ] Подключить `_registry.js` в lab.html ПЕРВЫМ среди labs-скриптов (до graph.js).
- [ ] Адаптер `renderSims()` (lab-glue.js): порядок из SIMS, registry-override + resolve preview (string|fn), append registry-only.
- [ ] Адаптер `openSim()` (lab-init.js): base-id, registry-first → stopActive/open/setActive; deep-link `:arg` сохранить.
- [ ] Адаптер `loadTheory()` (lab-init.js): registry.theory в приоритете, иначе THEORY[base].
- [ ] Адаптер `closeSim()`/`_pauseAllSims()`: добавить `LabRegistry.stopActive()`/`destroyActive()`.
- [ ] Зарегистрировать 3 пилота в конце lab-init.js (после _openXxx): graph, quadratic, pendulum — с preview-fn, theory-объектом, open=_openXxx, stop/destroy.
- [ ] Удалить graph/quadratic/pendulum из legacy `THEORY` и `SIMS` (проверка, что адаптер их подхватывает из реестра).
## Files to Modify/Create
- `frontend/js/labs/_registry.js` — новый: ядро реестра.
- `frontend/lab.html` — добавить `<script src="/js/labs/_registry.js">` первым (в обоих местах, если дублируется список).
- `frontend/js/labs/lab-glue.js` — renderSims адаптер; убрать 3 пилота из SIMS.
- `frontend/js/labs/lab-init.js` — openSim/loadTheory/closeSim/_pauseAllSims адаптеры; регистрация 3 пилотов; убрать 3 пилота из THEORY.
## Acceptance Criteria
- Каталог отображает все симуляции в прежнем порядке; 3 пилота открываются и работают идентично.
- Остальные 46 симуляций открываются по-старому (legacy путь не сломан).
- Deep-links и обратная совместимость id работают.
- Нет дублей карточек (пилот не показан дважды).
- Нет эмоджи; иконки `.ic`.
## Notes
- Порядок загрузки: см. CONTEXT.md. preview как функция спасает от undefined P_*.
- `_disabledSimIds` фильтрация должна продолжать работать для registry-записей.
## Review Checklist
- [ ] Адаптер не ломает legacy симуляции
- [ ] Паритет 3 пилотов (open/stop/close/theory/preview)
- [ ] Соблюдены конвенции проекта (no emoji, .ic)
- [ ] Нет дублирования карточек
## Handoff to Next Phase
<!-- заполнить после фазы -->
@@ -0,0 +1,40 @@
# Phase 1: Миграция всех симуляций на манифесты
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Перевести все ~49 симуляций на сам/регистрацию через `LabRegistry`. Перенести данные
(catalogue meta, preview, theory) и поведение (open/stop/destroy) в манифесты. Удалить
legacy-структуры. Сохранить глобальные имена через shim.
## Tasks
- [ ] Для каждой симуляции зарегистрировать манифест (метаданные из SIMS, preview из P_*, theory из THEORY, open/stop/destroy из _openXxx + _pauseAllSims/closeSim веток).
- [ ] Удалить массив `SIMS` (lab-glue.js) и объект `THEORY` (lab-init.js).
- [ ] Удалить if-цепочку `openSim`, `_pauseAllSims`, switch в `closeSim`, `ALL_SIM_BODIES`/`ALL_CTRL_BARS`.
- [ ] lab-init.js усохнуть до generic-логики (openSim/closeSim через реестр).
- [ ] Shim глобальных имён (gSim, pSim, …) — их дёргают deep-link/поиск/инлайн-обработчики.
- [ ] Сохранить обратную совместимость id (magnetic/coulomb→emfield, thinlens/mirrors/refraction→opticsbench, stereo:fig, hydrostatics:arg, molphys:arg, chemistry:arg, dynamics:arg, emfield:mode, opticsbench:mode).
## Files to Modify/Create
- Все `frontend/js/labs/*.js` симуляции — добавить `LabRegistry.register(...)`.
- `frontend/js/labs/lab-glue.js`, `frontend/js/labs/lab-init.js` — удалить legacy.
## Acceptance Criteria
- Все симуляции открываются/работают как раньше (паритет).
- Удалены все 6 точек дублирования из CONTEXT.md.
- Deep-links и алиасы работают.
## Notes
- Мигрировать пачками (по категориям) с проверкой паритета после каждой пачки (Big Bang допускает временную поломку между пачками).
- Превью с зависимостями (random в P_ELECTROLYSIS) перенести как есть.
## Review Checklist
- [ ] Ни одна симуляция не потеряна
- [ ] Глобальные shim'ы на месте
- [ ] Алиасы/deep-links работают
- [ ] Legacy полностью удалён
## Handoff to Next Phase
<!-- заполнить после фазы -->
@@ -0,0 +1,37 @@
# Phase 2: Тела симуляций как шаблоны + ленивый mount
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Вынести inline-HTML тел симуляций (`<div id="sim-xxx">`, ~3000 строк) из lab.html в
манифесты: `mount(host)` создаёт DOM лениво при первом открытии. lab.html худеет.
## Tasks
- [ ] Добавить в манифест поле `mount(host)` (или `bodyHtml`) — строит/возвращает тело симуляции.
- [ ] Ядро: при первом open — если тело не смонтировано, вызвать mount() в контейнер `#lab-sim`.
- [ ] Перенести разметку каждого `sim-xxx` тела + его `ctrl-xxx` бара из lab.html в соответствующий модуль.
- [ ] Удалить вынесенные блоки из lab.html.
- [ ] Сохранить id элементов (canvas ids, ctrl ids) — на них завязаны Sim-классы.
## Files to Modify/Create
- Все `frontend/js/labs/*.js` — добавить mount/bodyHtml.
- `frontend/lab.html` — удалить inline тела.
## Acceptance Criteria
- Все симуляции монтируются и работают.
- lab.html значительно меньше (~3000 строк вынесено).
- Повторное открытие не дублирует DOM.
## Notes
- Theory-panel и общий sim-topbar остаются в lab.html.
- KaTeX/lucide ре-инициализация после mount при необходимости.
## Review Checklist
- [ ] Нет дублей DOM при повторном open
- [ ] id элементов сохранены
- [ ] Все тела перенесены
## Handoff to Next Phase
<!-- заполнить после фазы -->
@@ -0,0 +1,39 @@
# Phase 3: Ленивая загрузка кода симуляций
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Грузить тяжёлый код симуляции и движков по клику, а не 58 скриптов + three.js сразу.
Лёгкий манифест каталога загружается сразу.
## Tasks
- [ ] Вынести лёгкие метаданные каталога (id/cat/title/desc/preview) в отдельный `sims.manifest.js`, грузимый сразу.
- [ ] Тяжёлый код симуляции (Sim-класс + open/mount) грузить динамически при openSim (инъекция script или import()).
- [ ] Объявить зависимости движков в манифесте (`deps: ['_fx_core','_phys_visuals',...]`); загрузчик резолвит и грузит до кода симуляции, кешируя загруженное.
- [ ] three.js грузить только для 3D-симуляций (stereo).
- [ ] Лоадер с дедупликацией (один и тот же файл не грузится дважды).
## Files to Modify/Create
- `frontend/js/labs/_loader.js` — новый: динамический загрузчик + резолв зависимостей.
- `frontend/js/labs/sims.manifest.js` — новый: лёгкий каталог.
- `frontend/lab.html` — убрать массовые `<script>`, оставить ядро (api, registry, loader, manifest).
## Acceptance Criteria
- Первый рендер каталога без загрузки кода симуляций.
- Открытие симуляции догружает её код+движки и работает.
- three.js грузится только для 3D.
- Заметное падение объёма стартовой загрузки lab.html.
## Notes
- Учесть defer-скрипты (solutions/organic/periodic/qualanalysis).
- Кеш загруженных модулей в Map.
## Review Checklist
- [ ] Нет двойной загрузки
- [ ] Зависимости движков соблюдены
- [ ] Старт лаборатории легче
## Handoff to Next Phase
<!-- заполнить после фазы -->
@@ -0,0 +1,42 @@
# Phase 4: Реестр в БД + API + админка
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
## Objective
Хранить оверрайды каталога в БД, мёржить с код-манифестами, управлять каталогом из
админки (вкл/выкл, порядок, теги, рекомендуемые).
## Tasks
- [ ] Миграция БД: таблица `lab_sims` (id PK, title, cat, subject, grade, desc, enabled, sort, topic_id, textbook_ref, flags JSON, updated_at). Через `node:sqlite` DatabaseSync.
- [ ] Backend route `GET /api/lab/sims` — отдаёт мёрж: код-манифест (база) + БД-оверрайды.
- [ ] Backend admin routes: upsert/enable/disable/reorder/tag (под RBAC admin).
- [ ] Frontend: каталог берёт enabled/order/теги из `/api/lab/sims` (с фолбэком на код-манифест офлайн).
- [ ] Расширить `frontend/js/admin/sections/sims.js`: список, вкл/выкл, drag-reorder, теги, «рекомендуемые».
- [ ] Сохранить совместимость с `_disabledSimIds`.
## Files to Modify/Create
- `backend/src/db/migrations/0XX_lab_sims.sql` — новая миграция.
- `backend/src/routes/lab.js` (или расширить существующий) — API.
- `backend/src/server.js` — подключить роут (если новый файл).
- `frontend/js/admin/sections/sims.js` — расширить админку.
- `frontend/js/labs/_loader.js`/manifest — учитывать БД-данные.
## Acceptance Criteria
- `npm test` зелёный; `npm run lint:routes` без ошибок (auth на роутах).
- Админ может вкл/выкл/переупорядочить/тегировать симуляцию, изменения видны в каталоге.
- Офлайн/без БД — фолбэк на код-манифест.
## Notes
- RBAC: мутации только admin. Чтение каталога — для роли с доступом к лаборатории.
- Не дублировать данные: код-манифест = источник базовых полей; БД = оверрайды/доп.
## Review Checklist
- [ ] Миграция идемпотентна
- [ ] Роуты под auth (lint:routes)
- [ ] Мёрж корректен, фолбэк работает
- [ ] Тесты проходят
## Handoff to Next Phase
<!-- заполнить после фазы -->
@@ -0,0 +1,43 @@
# Phase 5: Курикулумная привязка
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
## Objective
Связать симуляции с учебной программой: § учебника, узел knowledge-map, тема банка
вопросов. Двусторонняя навигация.
## Tasks
- [ ] Схема связей: использовать поля манифеста (subject/grade/topics) + таблицу связей `lab_sim_links` (sim_id, kind[textbook|topic|kmap|question], ref_id).
- [ ] API: `GET /api/lab/sims/:id/related` — связанные § / темы / задачи.
- [ ] Frontend учебник/теория: кнопка «Открыть в лаборатории» в § (deep-link openSim).
- [ ] Frontend knowledge-map: узел темы → ссылка на симуляцию.
- [ ] Страница симуляции: блок «Связанная теория и задачи».
- [ ] Админка: редактирование связей симуляции.
## Files to Modify/Create
- `backend/src/db/migrations/0XX_lab_sim_links.sql`
- `backend/src/routes/lab.js` — related endpoint.
- `frontend/textbooks.html` / theory / учебник-рендер — кнопки в §.
- `frontend/knowledge-map.html` — ссылки с узлов.
- `frontend/lab.html` — блок связей на странице sim.
- `frontend/js/admin/sections/sims.js` — редактор связей.
## Acceptance Criteria
- Из § учебника можно открыть нужную симуляцию.
- На странице симуляции видны связанные теория/задачи.
- Узлы knowledge-map ведут на симуляции.
- `npm test` зелёный, роуты под auth.
## Notes
- Привязки опциональны: отсутствие связей не ломает страницы.
- Переиспользовать существующие topic_id банка вопросов и структуру учебников.
## Review Checklist
- [ ] Навигация в обе стороны работает
- [ ] Пустые связи не ломают UI
- [ ] Роуты под auth, тесты проходят
## Handoff to Next Phase
<!-- финальная фаза -->