Files

21 KiB
Raw Permalink Blame History

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/rect width: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 линия (lineStyle solid/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 для нативного пикера.
    • Редактор кривых 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. Идентичность спеки между билдами уже гарантирована.
  • 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 обнуляет историю. _restoreSnapshotrenderPanels+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 --check OK, эмодзи/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