From a13c0b77fa91512792cc5e525891e9c2d4ecf694 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 13 Jun 2026 12:29:13 +0300 Subject: [PATCH] =?UTF-8?q?feat(sim-builder):=20=D1=84=D0=B0=D0=B7=D0=B0?= =?UTF-8?q?=204=20=E2=80=94=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=20=D1=81=D0=B8=D0=BC=D1=83=D0=BB=D1=8F=D1=86=D0=B8=D0=B9?= =?UTF-8?q?=20(sim-builder.html:=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D0=B8,?= =?UTF-8?q?=20=D0=B6=D0=B8=D0=B2=D0=BE=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D1=8C=D1=8E,=20save/publish)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 12 + frontend/js/sim-builder.js | 1116 +++++++++++++++++++++++ frontend/sim-builder.html | 190 ++++ js/sidebar.js | 1 + plans/sim-builder/CONTEXT.md | 32 +- plans/sim-builder/PLAN.md | 4 +- plans/sim-builder/phase-4-builder-ui.md | 84 +- 7 files changed, 1419 insertions(+), 20 deletions(-) create mode 100644 frontend/js/sim-builder.js create mode 100644 frontend/sim-builder.html diff --git a/CLAUDE.md b/CLAUDE.md index 607f547..0a03c13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,3 +93,15 @@ git push origin master - **lint:routes (baseline 0)**: `:id`-роуты прикрыты router-level `authMiddleware` (линтер видит `router.use()`); 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**: ` + + + +
+ +
+
+
+
+
+
+
+ Живое превью обновляется при правках. Включите «прицел» у объекта и кликните по сцене, чтобы задать его координаты. Кнопка «Тест» запускает анимацию. +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + diff --git a/js/sidebar.js b/js/sidebar.js index 50eacb0..4828464 100644 --- a/js/sidebar.js +++ b/js/sidebar.js @@ -87,6 +87,7 @@ ${G('practice', 'Практика и игры', ` ${L('/lab', 'atom', 'Лаборатория')} + ${L('/sim-builder', 'pencil-ruler', 'Конструктор симуляций', { cls: 'sb-teacher-only', hidden: !isTch })} ${L('/biochem', 'flask-conical', 'Биохимия')} ${L('/red-book', 'leaf', 'Красная книга')} ${L('/crossword', 'grid-3x3', 'Кроссворд')} diff --git a/plans/sim-builder/CONTEXT.md b/plans/sim-builder/CONTEXT.md index 378cd6b..b6ea7c1 100644 --- a/plans/sim-builder/CONTEXT.md +++ b/plans/sim-builder/CONTEXT.md @@ -1,6 +1,31 @@ # Feature Context: Конструктор симуляций (SimForge) ## Current State +- **Фаза 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=` → `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. @@ -59,9 +84,12 @@ - Reuse > переписывание: сначала смотреть `_fx_motion`, `_graph_panel`, `graph.js`. ## RESUME STATE -- Последний коммит фичи: — (Ф0 + Ф1 + Ф2 + Ф3 реализованы, ещё не закоммичены — ждут оркестратора) -- Текущая фаза: Phase 3 — Persistence + API (✅ Implemented, pending commit) → дальше Phase 4 — Builder UI +- Последний коммит фичи: — (Ф0 + Ф1 + Ф2 + Ф3 + Ф4 реализованы, ещё не закоммичены — ждут оркестратора) +- Текущая фаза: Phase 4 — Builder UI (✅ Implemented, pending commit) → дальше Phase 5 — Каталог (custom-sims в /lab) - Режим: 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. diff --git a/plans/sim-builder/PLAN.md b/plans/sim-builder/PLAN.md index a8a87ea..285f37f 100644 --- a/plans/sim-builder/PLAN.md +++ b/plans/sim-builder/PLAN.md @@ -43,7 +43,7 @@ - [x] Phase 1: Графики + интеракции [domain: frontend] → [subplan](./phase-1-plots-interactions.md) - [x] Phase 2: Физический интегратор [domain: frontend] → [subplan](./phase-2-physics.md) - [x] Phase 3: БД + API (custom_sims) [domain: backend] → [subplan](./phase-3-persistence-api.md) -- [ ] Phase 4: Билдер (редактор) [domain: frontend] → [subplan](./phase-4-builder-ui.md) +- [x] 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) @@ -56,7 +56,7 @@ | Phase 1: Plots & interactions | frontend | ✅ Done | ✅ | ✅ | ✅ | | Phase 2: Physics | frontend | ✅ Done | ✅ | ✅ | ✅ | | Phase 3: Persistence + API | backend | ✅ Done | ✅ | ✅ | ✅ | -| Phase 4: Builder UI | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 4: Builder UI | frontend | ✅ Done | ✅ | ✅ | ✅ | | Phase 5: Catalog | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ | | Phase 6: Sharing | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ | | Phase 7: Classroom | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ | diff --git a/plans/sim-builder/phase-4-builder-ui.md b/plans/sim-builder/phase-4-builder-ui.md index 5f0206c..ec991d1 100644 --- a/plans/sim-builder/phase-4-builder-ui.md +++ b/plans/sim-builder/phase-4-builder-ui.md @@ -1,6 +1,6 @@ # Phase 4: Билдер (редактор) -**Status:** ⬜ Not Started +**Status:** ✅ Implemented (в рабочем дереве, не закоммичено — коммит за оркестратором) **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** frontend @@ -9,16 +9,16 @@ После фазы учитель собирает рабочую симуляцию с нуля в 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). -- [ ] Валидация на клиенте (понятные ошибки до сохранения) + показ ошибок выражений. +- [x] Страница `frontend/sim-builder.html` (доступ teacher/admin; сайдбар как у других страниц). +- [x] Левая панель-аккордеоны + центр-превью (встроенный `SimEngine` инстанс, перемонтаж при правках, debounce 280мс). +- [x] Панель **Параметры**: добавить/удалить параметр (имя, min, max, step, начальное, единица) → слайдер в превью. +- [x] Панель **Объекты**: добавить объект (тип из whitelist), редактор свойств; числовые поля + принимают число ИЛИ выражение; палитра-помощник функций/параметров/объектов; подпись с LaTeX-превью. +- [x] Панель **Графики/Физика**: добавить plot (expr/var/range/trace); тумблер физики + её поля (Ф2) + стены/пружины. +- [x] Размещение объектов мышью на превью (кнопка-«прицел» у объекта → клик-поставить, drag-двигать) с синхроном в свойства. +- [x] Мета: заголовок, описание, предмет, класс, категория + viewport (xmin/xmax/ymin/ymax, сетка/оси, автозапуск/цикл). +- [x] Save/Load через `LS.customSims*` (Ф3): новый / редактировать существующий (?id=); кнопки «Сохранить»/«Опубликовать»/«Тест»/«Сброс». +- [x] Валидация на клиенте (понятные ошибки до сохранения, модалка-список) + inline-ошибки выражений у полей. ## Files to Modify/Create - `frontend/sim-builder.html` (new) — страница + инлайн-логика редактора @@ -37,10 +37,62 @@ - Большой файл — держать логику модульной (можно вынести в `frontend/js/sim-builder.js`). ## Review Checklist -- [ ] Все задачи выполнены -- [ ] Полный цикл build→save→reload→edit работает -- [ ] Доступ только teacher/admin -- [ ] Нет эмодзи; дизайн-система соблюдена +- [x] Все задачи выполнены +- [x] Полный цикл build→save→reload→edit работает (headless-смоук 23/23) +- [x] Доступ только teacher/admin (LS.initPage gate + редирект /dashboard) +- [x] Нет эмодзи; дизайн-система соблюдена (ls.css переменные/классы) ## Handoff to Next Phase - + +### Что реализовано (Phase 4) +- **Страница `frontend/sim-builder.html`** (URL `/sim-builder`, гейт teacher/admin через + `LS.initPage()` → редирект `/dashboard`). Раскладка: `.app-layout > .sidebar(#app-sidebar) + + .sb-content`; внутри `.sbu-wrap` = тулбар + `.sbu-body` (панели-аккордеоны слева 360px + + превью справа). Подключает движок `/js/labs/_sim_expr.js` + `/js/labs/_sim_engine.js` (тем + же путём, что lab.html — `/js` мапится на корневой `js/`, а `labs/*` проваливается на + `express.static(frontend)`), KaTeX CDN, и логику `/js/sim-builder.js`. +- **Логика `frontend/js/sim-builder.js`** → `window.SimBuilder.create({host, previewHost, + panelHost, toolbarHost}) -> Builder`. Состояние `Builder.st` (meta/subject/grade/cat/ + viewport/time/params[]/objects[]/plots[]/physics{}); `_uid` на объектах/стенах/пружинах — + только UI-метка, вырезается при сборке. +- **Генерация спеки** `Builder.buildSpec()` → чистый JSON v1: `{ specVersion:1, meta:{title, + desc}, viewport, time, params[], objects[](+merged plots), physics? }`. `stripObj()` убирает + `_uid`/пустые поля; **plot** материализуется отдельно (`normalizePlotForSpec`: UI-поля + `range_a/range_b` → `range:[a,b]`, границы могут быть числом ИЛИ выражением xmin/xmax). Физика + собирается только при `physics.enabled` (gravity{x,y}, friction, restitution клампится 0..1, + walls[], springs[]; конец пружины «id» или «x,y» парсится `parseEnd`). Числовые поля объектов + хранятся как введено (число/строка-выражение) — движок `SimExpr.compileValue` ест оба. +- **Живое превью**: `scheduleRemount(debounce 280мс)` → `remount()` уничтожает старый инстанс, + монтирует `SimEngine.mount(previewHost, buildSpec())`, сохраняет play-состояние. Ошибка сборки + показывается в превью, не роняет редактор. +- **Drag-on-preview**: кнопка-«прицел» у объекта выбирает его (`_selObjId`); pointerdown/move по + `inst.canvas` конвертит px→мир через `inst._toWorld()` и пишет x/y (или x2/y2 для segment/ + vector) в свойства объекта + обновляет поля панели. Работает только на паузе движка. +- **Палитра выражений** (`openPalette`): модалка с чипами — параметры, ссылки `id.x/id.y` на + объекты с id, `t/w/h`, константы (`SimExpr.CONSTANTS`), функции (`SimExpr.FUNCTIONS`); клик + вставляет имя в активное поле (функции — с `()`). +- **Валидация (клиент, до запроса)** `Builder.validate()` зеркалит серверную (Ф3): обязателен + title; params ≤50, имя-идентификатор, **запрет `e`/служебных (pi/t/w/h/E/PI/tau)**, без дублей, + min≤max; objects+plots ≤200; каждое выражение `SimExpr.compile` → ошибка показывается у поля И + в списке-модалке; expr ≤500 симв.; walls ≤20, springs ≤50; restitution 0..1; размер JSON ≤200КБ + (через Blob). Ошибки — дружелюбная модалка-список. +- **Save/Load**: «Сохранить»→`customSimCreate`/`customSimUpdate`(если есть simId); «Опубликовать» + добавляет `status:'published'`. После create — `history.replaceState('/sim-builder?id=')`, + чтобы повторное сохранение делало update. Загрузка `?id=` → `customSimGet` → `loadFromSim` + раскладывает спеку обратно по панелям (объекты vs plots разделяются по `type==='plot'`). +- **Сайдбар**: добавлен пункт `/sim-builder` «Конструктор симуляций» (icon `pencil-ruler`, + teacher-only) в группу «Практика и игры» сразу после «Лаборатория» — минимальная аддитивная + правка `js/sidebar.js` (тот же паттерн `cls:'sb-teacher-only', hidden:!isTch`, активная + подсветка через `isActive`). + +### Что осталось / на Ф5 (каталог) +- Превью-картинка симуляции (опц., упомянута в задаче) НЕ делалась — `custom_sims` не имеет поля + превью; визуальная карточка каталога — забота Ф5 (можно рендерить мини-`SimEngine` как превью). +- На Ф5: custom-sims из `LS.customSimsList()` (published + свои) должны попасть в каталог `/lab` + через `window.registerSpecSim(spec)` / `SimAdapter` (Ф0). Кнопка «Открыть в конструкторе» из + каталога → `/sim-builder?id=` уже работает (страница грузит по `?id`). Билдер пишет спеку + ровно в формате, который ест `SimEngine.mount` и серверная `validateSpec` (escaped-текст + приходит обратно при GET — для KaTeX/canvas безопасно). +- Drag-on-preview пишет только x/y (или конец segment/vector). Перетаскивание точек polyline, + origin вектора в форме origin+dx/dy, и хэндлов physics-тел — НЕ сделано (числами/выражениями + редактируется). Это осознанный прагматичный минимум (см. Notes плана).