feat(sim-builder): фаза 4 — редактор симуляций (sim-builder.html: панели, живое превью, save/publish)

This commit is contained in:
Maxim Dolgolyov
2026-06-13 12:29:13 +03:00
parent 014c96db1e
commit a13c0b77fa
7 changed files with 1419 additions and 20 deletions
+12
View File
@@ -93,3 +93,15 @@ git push origin master
- **lint:routes (baseline 0)**: `:id`-роуты прикрыты router-level `authMiddleware` (линтер видит `router.use(<guard>)`); read `GET/PUT/DELETE /:id` дополнительно помечены `// @public-by-design:` с указанием на ownership-проверку в хендлере (как в materials.js).
- **Тесты**: setup.js строит СВОЙ Express-app и НЕ монтирует новые роуты — тест-файл должен сам `app.use('/api/custom-sims', require(...))` (как lab-links.test.js). `getToken(role)`/`inject(method,path,body,token)` — готовые хелперы. seedRow(PRAGMA table_info) — для прямого посева строк, устойчив к дрейфу схемы (здесь не понадобился: пользователи создаются через getToken, спеки — через API).
- **Окружение тестов**: 8 fail из 209 = 3 baseline (`auth.test.js` — bcrypt/JWT в тест-окружении) + 5 page-тестов (`chemistry7/8-*`, `math5/6-page`), падающих на `Cannot find module 'jsdom'` (devDep не установлен) — оба класса не связаны с бэкенд-фазой.
### Phase 4 — Learnings
- **Билдер = `frontend/sim-builder.html` + `frontend/js/sim-builder.js`** (логика модульна: html держит только разметку/стили/bootstrap). `window.SimBuilder.create({host,previewHost,panelHost,toolbarHost}) -> Builder`. Состояние `Builder.st`; `_uid` на объектах/стенах/пружинах — UI-метка, вырезается в `buildSpec()`. Доступ teacher/admin: `LS.initPage()``{isTeacher,isAdmin}` → редирект `/dashboard` (паттерн live-quiz.html).
- **Подключение движка тем же путём, что lab.html**: `<script src="/js/labs/_sim_expr.js">` + `_sim_engine.js`. Гочи маршрутизации: `/js` мапится на **корневой** `js/` (api.js/sidebar.js/mobile.js/notifications.js), а в нём НЕТ `labs/` → запрос `/js/labs/*` и `/js/sim-builder.js` проваливается на `express.static(frontendDir)` и отдаёт `frontend/js/...`. Это уже работающий механизм (lab.html), не трогать server.js.
- **Генерация спеки**: `buildSpec()` → JSON v1. `stripObj()` убирает `_uid`/пустые поля. **plot** хранит в UI `range_a/range_b` отдельно и материализуется `normalizePlotForSpec``range:[a,b]` (границы — число ИЛИ выражение). `stripObj` переопределён в конце IIFE на plot-aware версию — работает т.к. `buildSpec` вызывает её в рантайме (function-declaration binding мутабелен). Числовые поля хранятся «как введено» (число/строка) — `SimExpr.compileValue` ест оба, серверная `validateSpec` не парсит.
- **Выражения = только SimExpr** (без eval/Function): `SimExpr.compile(v).error` → inline-ошибка у поля; `FUNCTIONS`/`CONSTANTS`**обычные объекты** (ключи=имена, не Set) → палитра через `Object.keys`. `exprError()` пропускает чистые числа и пустые строки.
- **Запрет имени param**: не только `e` (число Эйлера), но и `pi/E/PI/tau/t/w/h` (служебные env-переменные движка) — иначе слайдер затрёт системное имя.
- **Drag-on-preview**: переиспользует геометрию движка — `inst.canvas` + `inst._toWorld(px,py)` (px относит. canvas getBoundingClientRect). Пишет x/y (point/circle/label/readout/rect) или x2/y2 (segment/vector). Только на паузе (`!inst.isRunning()`), чтобы не конфликтовать со встроенным drag/анимацией движка.
- **Клиентская валидация зеркалит серверную** (Ф3 лимиты) и показывает дружелюбную модалку-список ДО запроса — экономит round-trip и даёт понятные ошибки вместо сырого 400.
- **Сайдбар — аддитивно**: пункт `/sim-builder` в `js/sidebar.js` в группе `G('practice',...)` после `/lab`, паттерн `{ cls:'sb-teacher-only', hidden:!isTch }`. `isActive('/sim-builder')` подсвечивает на странице (clean URL без .html). НЕ ломает остальной сайдбар.
- **Верификация без jsdom**: headless-смоук — `vm.createContext` + ручной DOM/window/Blob-стаб (canvas getContext-заглушка, setTimeout no-op чтобы debounce не стрелял), грузим `_sim_expr.js`+`sim-builder.js`, дёргаем `buildSpec()`/`validate()`/`loadFromSim()` напрямую (рендер не нужен для логики). 23/23.