21 KiB
SimForge — раунд улучшений (визуал / графика / рабочее поле)
Branch: feature/sim-builder · Mode: Automated · Execution: Orchestrator · Strategy: Incremental
Started: 2026-06-13
Полировка конструктора симуляций по всем направлениям. Каждая фаза — реализатор + независимый ревьюер, коммит поимённо. ⛔ Эмодзи нет (SVG .ic); ast-index/Read; общая ветка с параллельной сессией — править свои файлы движка/билдера, чужое (materials/quota) не трогать.
Контекст бага «съехало вправо»
_sim_engine.js._build рисует фикс-панель контролов width:260px СЛЕВА + сцену справа. У пустой/новой
симуляции панель всё равно 260px → сцена и сетка визуально смещены вправо (правые ~70% хоста). _fit
(DPR, центрирование по stage) корректен. Фикс — в раскладке (Фаза 1).
Фазы
-
P1 — Рабочее поле (fix смещения + основа сцены). Контролы из фикс-260px-колонки → плавающая/ нижняя ненавязчивая панель (collapsible); canvas-сцена центрирована и во всю ширину. Сетка major/minor + числовые подписи осей + маркер origin. Zoom (колесо к курсору) + pan (drag пустого места) + кнопки fit/reset-view. Работает и в билдере, и в /lab, и на доске (один движок). Подтвердить DPR-резкость. Файлы:
frontend/js/labs/_sim_engine.js(sim-builder.html НЕ потребовался — старый CSS превью.sbu-preview .sim-spec-root{position:absolute;inset:0}уже корректно растягивает новый full-bleed root).Handoff (P1 → P2):
- Раскладка:
_buildтеперь делаетroot(position:relative) → внутриstage(position:absolute; inset:0, canvas+labels на всю площадь) + плавающаяpanel(position:absolute;left/top:10px;z-index:5; pointer-events:auto, сворачивается кнопкой_togglePanel, есть только при наличии params) + бар кнопок вида (right/bottom:10px). Смещение вправо устранено: панель больше не отжимает сцену. - Transform-модель:
_fit()считает БАЗУ_baseScale/_baseOffX/_baseOffY(центрированный fit) и ЭФФЕКТИВНЫЙ_scale/_offX/_offY._zoom— пользовательский множитель к базе,_viewLocked— был ли zoom/pan (ресайз тогда сохраняет мир-центр и zoom, не сбрасывает вид)._toPx/_toWorld— без изменений сигнатур. - API вида (новое, публичное):
inst.fitView()/inst.resetView()(оба → центрированный viewport). Внутреннее:_zoomAt(lx,ly,factor)(зум к экранной точке, инвариант мир-точки),_setupZoomPan(),_pickHandleAt(lx,ly)(вынесен из_setupDrag, общий хит-тест — pan стартует только если вернул null → приоритет ручек/тел сохранён),_visibleWorld(W,H). - Сетка/оси:
_niceStep(targetPx)теперь завязан на_scale(адаптивен к zoom, шаги 1/2/5·10^n);_drawGridрисует minor(~34px) + major(×5) через всю видимую область (_visibleWorld), линии на .5px (резкость, без «ступенек»);_drawAxesрисует оси (прижимаются к краю если 0 вне вида) + числовые подписи делений (светлый текст + тень для тёмного фона, хелперы_axisNum/_stepDecimals) + маркер origin (0,0). - destroy: снимает wheel + pan-листенеры (
_onWheel/_onPanDown/_onPanMove/_onPanUp) и ResizeObserver. - На P2: качество графики ОБЪЕКТОВ (lineJoin/cap, стрелки векторов, dash/opacity/градиент/glow, стили
точек, затухающие трассы) — это
_drawObject/_drawTrail/_arrowHead/_drawPlot/_prepareObjectsв том же файле. Поля стилей объектов уже читаются в_prepareObjects(color/fill/width) — расширять там.
- Раскладка:
-
P2 — Качество графики объектов. Скругление/сглаживание линий (lineJoin/cap), красивые стрелки векторов, стили линий (solid/dashed/dotted), opacity, градиент-заливки, опц. тень/glow, стили точек (filled/hollow/cross/ring), затухающие трассы; приятная дефолтная палитра. Файл:
_sim_engine.js.Handoff (P2 → P3/P4): новые поля стиля спеки (контракт для контролов билдера в P4). Все рендерятся ТОЛЬКО на canvas (
fillStyle/strokeStyle/createLinearGradient/shadowColor) — XSS нет, мусорный цвет игнорится canvas. Читаются в_prepareObjects, применяются в_drawObjectчерез хелперы_applyStroke(alpha/lineWidth/join/cap/dash/glow) и_fillStyleFor(градиент или сплошная заливка):opacity— число0..1(деф. 1) →globalAlphaна время отрисовки объекта (восстанавливается).lineStyle—'solid'|'dashed'|'dotted'(деф. solid) →setLineDash(паттерн масштабируется отwidth).width— толщина штриха (деф. 2); для circle/rectwidth:0отключает обводку (только заливка).fill/fillColor— цвет заливки (circle/rect/закрытый path).gradient:[c0,c1]— линейный градиент (вертикальный по bbox), приоритетнееfill. Полигон-заливка только приclosed:true.glow:trueИЛИshadow:'#color'ИЛИshadow:{blur}— свечение (shadowColor/shadowBlur); деф. ВЫКЛ (производительность).glowColor/glowBlur— точечная настройка (деф. цвет объекта / blur 12).pointStyle(point) —'filled'|'hollow'|'cross'|'ring'(деф. filled: заполненный кружок + тонкая белая обводка). hollow — только обводка, ring — толстое кольцо, cross — крестик.trailFade(деф. true) — затухающая трасса (старые сегменты прозрачнее, посегментно alpha 0.08→0.68);trailWidth(деф. 1.6),trailLen(деф. 2000, макс 5000) — толщина/длина следа.trailColor— как было.- Палитра по умолчанию: если
colorне задан — циклическиDEFAULT_PALETTE[i % 8](cyan/violet/pink/ emerald/amber/blue/rose/green) вместо единого#06D6E0. Явныйcolorвсегда сохраняется. - Стрелки векторов (
_arrowHead/_arrowHeadLen): заполненный «барбед»-треугольник (вырез у основания), длина =max(9, width*3.2)px; тело линии укорочено на длину головы (не торчит сквозь остриё). - На P3 (графики/диаграммы):
_drawPlotуже использует_applyStroke(dash/opacity/glow работают на кривых). Для P3 расширять_drawPlot— оси-делений plot, несколько кривых, заливка под кривой, маркеры точек (можно переиспользовать_drawPoint), легенда. Хелперы_applyStroke/_fillStyleFor/_drawPointготовы к переиспользованию.
-
P3 — Графики/диаграммы (визуал charts). Для plot: несколько кривых, заливка под кривой, маркеры точек, легенда; аккуратный стиль диаграмм (оси/сетка/подписи — уже из P1). Файл:
_sim_engine.js.Handoff (P3 → P4): новые поля plot-объекта (контракт для контролов билдера в P4). Все читаются в
_prepareObjects(веткаtype==='plot'), рендерятся ТОЛЬКО на canvas (без DOM-style/eval). Старый одиночныйexpr/var/range/samples/traceработает как раньше (обратная совместимость):- Несколько кривых. Источник (приоритет):
curves:[{...}]→exprs:['sin(x)','x^2']→expr(легаси). Нормализуются вprep.curves[]. Каждой кривой свой цвет: явныйcolorилиDEFAULT_PALETTE[i%8].prep.exprFn= первая кривая (для trace-режима). - Поля кривой (
curves[i]):expr(строка),color,label(строка → легенда),width,lineStyle(solid|dashed|dotted),opacity(0..1),fill(true→ полупрозр. цвет кривой / строка цвета),marker(none|dot|ring). Не заданные наследуются от plot-уровня (width/lineStyle/opacity) или дефолтов. - Plot-уровневые
fillиmarker— дефолт для всех кривых (если у кривой не задано). - Заливка под кривой — между кривой и осью
y=0, посегментно (разрывы у не-finite точек не сливаются),_fillUnderCurve. Прозрачность через_fillAlpha(color, 0.18)дляfill:true. - Маркеры узлов —
_drawCurveMarkers(переиспользует_drawPoint), прорежены ~28px по экрану (не рисуем сотни точек).dot→ filled,ring→ hollow. - Легенда —
_drawLegend(на canvas: тёмная плашка + цветной свотч + светлый текст), верх-право, не наезжает на бар кнопок вида. Включается авто при наличииlabel;legend:falseотключает. - Качество кривой — пропуск не-finite (разрывы), переиспользован существующий equidistant sampling
(
samples, деф. 200, макс 2000),_applyStroke(dash/opacity/glow/lineJoin/cap). - На P4 (билдер): дать этим полям контролы — список кривых (добавить/удалить, expr + color-picker +
label + width + lineStyle + opacity + fill toggle/color + marker select), plot-уровневые fill/marker,
тумблер легенды. Хелпер
_markerStyle/_fillAlpha— модульного уровня, рядом с_dashFor/_opacity.
- Несколько кривых. Источник (приоритет):
-
P4 — UI билдера + контролы стиля. Дизайн-полировка панелей/тулбара (ls.css), нативные color- пикеры + opacity/width/dash/линестиль на объект, z-order/дублирование/видимость объектов, пустые состояния, мобайл. Файлы:
frontend/sim-builder.html,frontend/js/sim-builder.js.Handoff (P4 → P5):
- Контролы стиля объекта (блок «Стиль» в каждом редакторе,
STYLE_FOR[type]решает набор):rangeCtlнепрозр. (слайдер 0..1 →opacity),selectCtlлиния (lineStylesolid/dashed/dotted), стиль точки (pointStyle, только point), тумблерglow, тумблер «Градиент-заливка» (circle/rect →gradient:[c0,c1], две пары color-инпутов). Цвета — новыйcolorCtl: нативный<input type=color>- текстовое поле (источник истины, поддерживает rgba/named) + кнопка очистки для fill/trailColor
(«нет заливки»). Синхрон пикер↔текст —
Builder.wireColorControls(row)(текст диспатчитinput, основнойdata-of/data-cvfобработчик ловит).toHexColorприводит к#rrggbbдля нативного пикера.
- текстовое поле (источник истины, поддерживает rgba/named) + кнопка очистки для fill/trailColor
(«нет заливки»). Синхрон пикер↔текст —
- Редактор кривых plot (
plotEditor/curveEditor): UI-модель plot ={var, range_a/b, samples, trace, legend, plotFill, plotMarker, curves:[...]}. Кривая ={_uid, expr, color, label, width, lineStyle, opacity, fill(bool), fillColor, marker}. Список кривых (добавить[data-curveadd]/ удалить[data-curvedel], минимум 1), на кривую — expr+fx, color, label, width, lineStyle, marker, opacity, fill+цвет. Plot-уровневыеplotFill/plotMarker/легенда.loadPlotнормализует spec→UI (curves[]→exprs[]→legacy expr; легаси plot-level width/lineStyle/opacity наследуются кривой),normalizePlotForSpec+stripCurveсобирают обратно: одиночная «простая» кривая (только expr+color, нет plot-fill/marker) → легаси-форма{expr,color}; иначеcurves:[...].legend:falseэмитится только при выключенной легенде. - Список объектов: в шапке каждого — z-order вверх/вниз (
[data-oup]/[data-odown], порядок в массиве = порядок отрисовки; крайние disabled), видимость ([data-ohide]→o.hidden=true), дублировать ([data-odup], deep-clone + новый_uid,id+'_copy'), удалить. Аналогично у plot. - hidden — чисто на стороне билдера (движок не трогали):
buildSpecфильтрует объекты/plot сhidden;stripObj.isDefaultStyleгарантирует, чтоhidden/дефолты стиля (glow:false, lineStyle: 'solid', pointStyle:'filled', opacity:1, trail/closed:false) НЕ попадают в спеку → спека минимальна, round-trip save→load→save идемпотентен (проверено vm-смоуком 27+12+2 PASS). - На P5 (прямое манипулирование + история): в билдере сейчас есть только drag x/y point/circle/label/
readout/rect и конца segment/vector (
bindPreviewDragчерезinst._toWorld). Расширять до всех типов- snap-к-сетке + выравнивание (нужны правки
_sim_engine.js— хит-тесты/ручки). Undo/redo: состояние =this.st(сериализуемо JSON); снимать снапшот приonAdd/удалении/правке (debounce) — стек в Builder, перерисовкаrenderPanels+scheduleRemount. Идентичность спеки между билдами уже гарантирована.
- snap-к-сетке + выравнивание (нужны правки
- Контролы стиля объекта (блок «Стиль» в каждом редакторе,
-
P5 — Прямое манипулирование на сцене + история. Drag всех типов (не только point/circle), snap-к-сетке; undo/redo в билдере. Файл:
frontend/js/sim-builder.js(движок НЕ тронут —_toWorld/_toPx/_niceStepуже публичны на инстансе, хука не потребовалось).Итог / Handoff (P5 — финал раунда):
- Прямое манипулирование (
bindPreviewDrag, переписан). «Ручки» объекта строитhandlesOf(obj): точка/окружность/подпись/показатель/прямоугольник → одна ручкаpos(x,y); отрезок/вектор → две ручкиorigin(x1,y1)+end(x2,y2 ИЛИ origin+dx/dy — определяется по наличию полей); ломаная/путь → по ручке на каждую числовую вершинуpoints. Каждая ручка несётset(x,y)и флагblocked. Хит-тестpickHandle(допуск 14px черезinst._toPx) выбирает ближайшую ручку. Режимы pointerdown:handle(попали в ручку — двигаем её),place(единственная ручка, клик ставит точку — сохранён исходный смысл «клик ставит»),body(несколько ручек — двигаем всё тело относительным сдвигом от стартовой мир-точки),none(нет двигаемых ручек). Поля-ВЫРАЖЕНИЯ не трогаются:numFieldвернётnullдля нечислового значения → ручкаblocked(не двигается, не сериализуется молча). - Snap-к-сетке. Тумблер в тулбаре (иконка
ICON.grid, флагthis._snap, переключательtoggleSnap, активное состояние — инлайн-стильSNAP_ACTIVE_CSS, без зависимости от CSS-класса). При включённом drag округляет мир-координаты к шагуinst._niceStep(34)(минорный шаг сетки движка; fallback 0.5). Выключенный —round2. - Выравнивание — реализован минимум (snap-к-сетке движка). Прилипание к координатам других объектов НЕ делалось (бонус; достаточно snap для зачёта). Зафиксировано как частичное.
- Undo/Redo. Стек снапшотов
JSON.stringify(this.st)(глубина_undoMax=50).pushHistoryснимает снапшот ПЕРЕД мутацией (без дублей верхушки; сбрасывает redo).snapField— один снапшот на сессию правки поля (focusin сбрасывает флаг_fieldSnapTaken, первый input/change снимает) → Ctrl+Z откатывает значение целиком, а не посимвольно. Структурные операции (add/delete/z-order/duplicate/hide/toggle, включая plot/curve/wall/spring и физ-тумблер) — снапшот сразу. Drag — один снапшот на сессию (пустые no-op-снапшоты откатываются вend()). Кнопки undo/redo в тулбаре (SVG.ic), горячие клавиши Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y (bindKeyboardShortcuts, вешается один раз, игнорит фокус в полях ввода).loadFromSimобнуляет историю._restoreSnapshot→renderPanels+scheduleRemount. - Совместимость.
buildSpec/round-trip/валидация не тронуты; идемпотентность спеки сохранена.refreshObjFieldsрасширен на x1/y1/x2/y2/dx/dy/points. Проверено vm-смоуком: 38/38 PASS (drag point/circle/segment-оба-конца/vector-dx,dy/polyline-vertex + body-move polyline/segment; snap к 0.5; выражение не затирается; undo/redo drag и add; стек ограничен; round-trip идемпотентен; no-op drag не плодит историю).node --checkOK, эмодзи/eval нет.
- Прямое манипулирование (
Progress
| Phase | Status | Review | Committed |
|---|---|---|---|
| P1 Working field | Done | ✅ PASS | ✅ |
| P2 Object graphics | Done | ✅ PASS | ✅ |
| P3 Charts | Done | ✅ PASS | ✅ |
| P4 Builder UI | Done | ✅ PASS | ✅ |
| P5 Direct manip + history | Done | ✅ PASS | ✅ |