10 KiB
10 KiB
Phase 4: Билдер (редактор)
Status: ✅ Implemented (в рабочем дереве, не закоммичено — коммит за оркестратором) Parent plan: PLAN.md Domain: frontend
Objective
Учительский редактор: страница с живым превью и панелями для сборки спеки без кода. После фазы учитель собирает рабочую симуляцию с нуля в UI и сохраняет в БД (Ф3).
Tasks
- Страница
frontend/sim-builder.html(доступ teacher/admin; сайдбар как у других страниц). - Левая панель-аккордеоны + центр-превью (встроенный
SimEngineинстанс, перемонтаж при правках, debounce 280мс). - Панель Параметры: добавить/удалить параметр (имя, min, max, step, начальное, единица) → слайдер в превью.
- Панель Объекты: добавить объект (тип из whitelist), редактор свойств; числовые поля принимают число ИЛИ выражение; палитра-помощник функций/параметров/объектов; подпись с LaTeX-превью.
- Панель Графики/Физика: добавить plot (expr/var/range/trace); тумблер физики + её поля (Ф2) + стены/пружины.
- Размещение объектов мышью на превью (кнопка-«прицел» у объекта → клик-поставить, drag-двигать) с синхроном в свойства.
- Мета: заголовок, описание, предмет, класс, категория + viewport (xmin/xmax/ymin/ymax, сетка/оси, автозапуск/цикл).
- Save/Load через
LS.customSims*(Ф3): новый / редактировать существующий (?id=); кнопки «Сохранить»/«Опубликовать»/«Тест»/«Сброс». - Валидация на клиенте (понятные ошибки до сохранения, модалка-список) + 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 работает (headless-смоук 23/23)
- Доступ только teacher/admin (LS.initPage gate + редирект /dashboard)
- Нет эмодзи; дизайн-система соблюдена (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=<id>'), чтобы повторное сохранение делало update. Загрузка?id=<id>→customSimGet→loadFromSimраскладывает спеку обратно по панелям (объекты vs plots разделяются поtype==='plot'). - Сайдбар: добавлен пункт
/sim-builder«Конструктор симуляций» (iconpencil-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>уже работает (страница грузит по?id). Билдер пишет спеку ровно в формате, который естSimEngine.mountи сервернаяvalidateSpec(escaped-текст приходит обратно при GET — для KaTeX/canvas безопасно). - Drag-on-preview пишет только x/y (или конец segment/vector). Перетаскивание точек polyline, origin вектора в форме origin+dx/dy, и хэндлов physics-тел — НЕ сделано (числами/выражениями редактируется). Это осознанный прагматичный минимум (см. Notes плана).