# Phase 4: Билдер (редактор)

**Status:** ✅ Implemented (в рабочем дереве, не закоммичено — коммит за оркестратором)
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend

## Objective
Учительский редактор: страница с живым превью и панелями для сборки спеки без кода.
После фазы учитель собирает рабочую симуляцию с нуля в UI и сохраняет в БД (Ф3).

## Tasks
- [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) — страница + инлайн-логика редактора
- `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
- [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=<id>')`,
  чтобы повторное сохранение делало update. Загрузка `?id=<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>` уже работает (страница грузит по `?id`). Билдер пишет спеку
  ровно в формате, который ест `SimEngine.mount` и серверная `validateSpec` (escaped-текст
  приходит обратно при GET — для KaTeX/canvas безопасно).
- Drag-on-preview пишет только x/y (или конец segment/vector). Перетаскивание точек polyline,
  origin вектора в форме origin+dx/dy, и хэндлов physics-тел — НЕ сделано (числами/выражениями
  редактируется). Это осознанный прагматичный минимум (см. Notes плана).
