From 69e219ae8c2c07e7f9b326f481b3f055acfdd7dd Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 13 Jun 2026 14:26:36 +0300 Subject: [PATCH] =?UTF-8?q?feat(sim-builder):=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20P3=20=E2=80=94=20=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D1=84=D0=B8=D0=BA=D0=B8:=20=D0=BD=D0=B5=D1=81=D0=BA?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BA=D1=80=D0=B8=D0=B2=D1=8B?= =?UTF-8?q?=D1=85,=20=D0=B7=D0=B0=D0=BB=D0=B8=D0=B2=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=BA=D1=80=D0=B8=D0=B2=D0=BE=D0=B9,=20=D0=BC?= =?UTF-8?q?=D0=B0=D1=80=D0=BA=D0=B5=D1=80=D1=8B,=20=D0=BB=D0=B5=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 14 ++ frontend/js/labs/_sim_engine.js | 212 +++++++++++++++++++++++++++--- plans/sim-builder/CONTEXT.md | 28 +++- plans/sim-builder/IMPROVEMENTS.md | 30 ++++- 4 files changed, 260 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4d2b4fe..4be8185 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -158,3 +158,17 @@ git push origin master - **Палитра по умолчанию** `DEFAULT_PALETTE` (8 холодно-ярких тонов) — циклически `[i % 8]` в `_prepareObjects`, только если `color` не задан в спеке; явный color сохраняется. - **Верификация P2**: `node --check` OK; headless-смоук (vm + DOM/canvas-стаб со счётчиками вызовов и проверкой баланса save/restore + РЕАЛЬНЫЕ `_sim_expr.js`+`_sim_engine.js`) 23/23: 18-объектная спека (все типы + все новые поля) ×4 кадра без throw; **ctx не протекает** (depth=0, globalAlpha→1, shadowBlur→0, lineDash→[] после кадра); setLineDash/createLinearGradient/fill/stroke/arc вызваны; поля прочитаны; палитра+явный color; трасса накоплена; destroy чист. Эмодзи нет (скан: только пре-существующие →/─/═/∞ в комментариях); eval=0; new Function — только в комментарии стр.15. - **На P3 (графики/диаграммы)**: `_drawPlot` уже зовёт `_applyStroke`. Расширять `_drawPlot` — оси-деления plot, несколько кривых, заливка под кривой, маркеры (переиспользовать `_drawPoint`), легенда. Хелперы `_applyStroke`/`_fillStyleFor`/`_drawPoint` готовы к переиспользованию. + +### SimForge improvements — P3 (Графики/диаграммы) — Learnings + +Всё в `frontend/js/labs/_sim_engine.js`. Расширен `_drawPlot` + ветка `type==='plot'` в `_prepareObjects`. Оси/сетка/подписи уже из P1 — в P3 не дублировались. + +- **Несколько кривых.** Нормализуются в `prep.curves[]` с приоритетом источника: `curves:[{...}]` → `exprs:['sin(x)','x^2']` → одиночный `expr` (легаси, обратная совместимость). Каждой кривой свой цвет: явный `color` или `DEFAULT_PALETTE[i%8]`. `prep.exprFn` оставлен = первой кривой (нужен trace-режиму `_accumPlotTrace`). +- **Поля кривой** (`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`** — дефолт для всех кривых. +- **Заливка под кривой** — `_fillUnderCurve(ctx,pts,baseY)`: между кривой и осью `y=0` (baseY клиппится к canvas), посегментно — разрывы у не-finite точек НЕ сливаются в один полигон. `fill:true` → `_fillAlpha(color,0.18)` (#RGB/#RRGGBB→rgba; прочие форматы как есть, alpha через globalAlpha). +- **Маркеры узлов** — `_drawCurveMarkers` переиспользует `_drawPoint` (dot→filled, ring→hollow), прорежены ~28px по экрану (не сотни точек на 200 сэмплах). +- **Легенда** — `_drawLegend` на canvas (НЕ DOM): тёмная плашка (`roundRect` с фолбэком на `fillRect`) + цветной свотч (strokeStyle цвета кривой) + светлый `fillText`. Верх-право, не наезжает на бар кнопок вида. Авто при наличии `label`; `legend:false` отключает. ⛔ Пользовательский цвет — только canvas-сток; текст легенды — фикс. светлый. +- **Качество кривой** — пропуск не-finite (разрывы через `started=false`), переиспользован equidistant sampling (`samples` 200/макс 2000), `_applyStroke` даёт dash/opacity/glow/round-стыки. Каждая кривая в своём `ctx.save()/restore()`, легенда — на внешнем уровне → состояние не протекает. +- **Новые хелперы модульного уровня** (рядом с `_dashFor`/`_opacity`): `_markerStyle(v)` (none|dot|ring), `_fillAlpha(color,a)` (hex→rgba для заливки). +- **Верификация P3**: `node --check` OK; headless vm-смоук (canvas-стаб со счётчиком save/restore + РЕАЛЬНЫЕ `_sim_expr`+`_sim_engine`) 10/10: легаси одиночный expr, exprs[], curves[]+fill+marker+legend, наследование plot-уровня, не-finite (1/x, tan) без throw, legend:false, trace±range, fillUnder+markers с null-разрывами, регресс point/vector/circle/rect — все PASS; **ctx сбалансирован** (depth→0, нет restore-underflow). Эмодзи нет (только пре-существующие → в комментариях); eval=0. Temp-смоук удалён. +- **На P4 (билдер)**: дать полям контролы — список кривых (add/del, expr+color+label+width+lineStyle+opacity+fill+marker), plot-уровневые fill/marker, тумблер легенды. diff --git a/frontend/js/labs/_sim_engine.js b/frontend/js/labs/_sim_engine.js index 996d782..fdb6d6e 100644 --- a/frontend/js/labs/_sim_engine.js +++ b/frontend/js/labs/_sim_engine.js @@ -137,6 +137,26 @@ return v < 0 ? 0 : (v > 1 ? 1 : v); } + /* нормализовать стиль маркера узлов кривой plot: 'dot'|'ring'|'none' (деф. none). */ + function _markerStyle(v) { + return (v === 'dot' || v === 'ring') ? v : 'none'; + } + + /* полупрозрачная версия цвета для заливки под кривой. #RGB/#RRGGBB -> rgba(...,a); + прочие форматы (rgb()/named) оставляем как есть (canvas сам применит globalAlpha). */ + function _fillAlpha(color, a) { + if (typeof color !== 'string') return color; + var m = color.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/); + if (!m) return color; + var h = m[1], r, g, b; + if (h.length === 3) { + r = parseInt(h[0] + h[0], 16); g = parseInt(h[1] + h[1], 16); b = parseInt(h[2] + h[2], 16); + } else { + r = parseInt(h.slice(0, 2), 16); g = parseInt(h.slice(2, 4), 16); b = parseInt(h.slice(4, 6), 16); + } + return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; + } + /* Компилятор свойства: число/строка -> { ev(env) } (всегда число). */ function bind(value, dflt) { if (value === undefined || value === null) { @@ -635,13 +655,52 @@ bp('x', 0); bp('y', 0); } else if (type === 'plot') { prep.varName = (typeof o['var'] === 'string' && o['var']) ? o['var'] : 'x'; - prep.exprFn = bind(o.expr != null ? o.expr : '0', 0); var rng = Array.isArray(o.range) ? o.range : null; prep.rangeA = bind(rng ? rng[0] : null, null); prep.rangeB = bind(rng ? rng[1] : null, null); prep.hasRange = !!rng; prep.samples = Math.max(2, Math.min(2000, num(o.samples, 200) | 0)); prep.trace = !!o.trace; + // ── P3: несколько кривых на одном plot ── + // Источник кривых (приоритет): curves[] -> exprs[] -> одиночный expr (легаси). + // Каждой кривой свой нормализованный стиль; цвет — явный или из палитры по индексу. + var curveDefs = []; + if (Array.isArray(o.curves) && o.curves.length) { + curveDefs = o.curves.map(function (cv) { + return (cv && typeof cv === 'object') ? cv : { expr: cv }; + }); + } else if (Array.isArray(o.exprs) && o.exprs.length) { + curveDefs = o.exprs.map(function (ex) { return { expr: ex }; }); + } else { + curveDefs = [{ expr: o.expr != null ? o.expr : '0' }]; + } + var plotMarker = _markerStyle(o.marker); + // plot-уровневые дефолты заливки/маркера наследуются кривыми (если у кривой не задано) + prep.curves = curveDefs.map(function (cv, ci) { + cv = cv || {}; + var cFill = (cv.fill !== undefined) ? cv.fill : o.fill; + return { + exprFn: bind(cv.expr != null ? cv.expr : '0', 0), + color: cv.color || o.color || DEFAULT_PALETTE[ci % DEFAULT_PALETTE.length], + label: (cv.label != null) ? String(cv.label) : '', + width: num(cv.width, prep.width), + lineStyle: (cv.lineStyle === 'dashed' || cv.lineStyle === 'dotted') ? cv.lineStyle + : prep.lineStyle, + opacity: (cv.opacity === undefined || cv.opacity === null) ? prep.opacity : _opacity(cv.opacity), + // заливка под кривой: true -> полупрозрачный цвет кривой; строка -> явный цвет + fill: (cFill === true || (typeof cFill === 'string' && cFill)) ? cFill : false, + // маркеры узлов: none|dot|ring (наследует plot-уровень) + marker: (cv.marker !== undefined) ? _markerStyle(cv.marker) : plotMarker, + glow: prep.glow, + glowColor: prep.glowColor, + glowBlur: prep.glowBlur + }; + }); + // легаси: одиночное выражение для trace-режима (накопление по t) + prep.exprFn = prep.curves[0].exprFn; + // легенда: показывать, если есть хотя бы одна подпись (можно явно legend:false) + var anyLabel = prep.curves.some(function (c) { return !!c.label; }); + prep.legend = (o.legend === false) ? false : anyLabel; } else if (type === 'readout') { // компилируем выражение один раз: храним и fn (быстро), и ast (для evalSafe — мягкая ошибка) var rc = global.SimExpr ? global.SimExpr.compileValue(o.expr != null ? o.expr : '0') @@ -1471,11 +1530,14 @@ } }; - /* ── plot: график выражения f(var) на отрезке range (мир-координаты) ── */ + /* ── plot: график f(var) на отрезке range (мир-координаты) ── + P3: несколько кривых (o.curves[]), заливка под кривой (к оси y=0), маркеры узлов, + легенда (на canvas). Trace-режим (накопление по t) рисуется отдельно через _drawTrail. */ SimEngineInstance.prototype._drawPlot = function (ctx, o, env) { // trace без явного range — только накапливаемый след (статической кривой нет) if (o.trace && !o.hasRange) return; var vp = this._vp(); + var W = this._cw, H = this._ch; var a = o.rangeA.ev(env), b = o.rangeB.ev(env); if (!o.hasRange || !isFinite(a) || !isFinite(b)) { a = vp.xmin; b = vp.xmax; } if (a === b) return; @@ -1485,25 +1547,141 @@ var prev = env[o.varName]; var hadPrev = Object.prototype.hasOwnProperty.call(env, o.varName); - ctx.save(); - this._applyStroke(ctx, o); - ctx.strokeStyle = o.color; - ctx.beginPath(); - var started = false; - for (var i = 0; i < n; i++) { - var xv = a + step * i; - env[o.varName] = xv; - var yv = o.exprFn.ev(env); - if (typeof yv !== 'number' || !isFinite(yv)) { started = false; continue; } - var px = this._toPx(xv, yv); - if (!started) { ctx.moveTo(px[0], px[1]); started = true; } - else ctx.lineTo(px[0], px[1]); + var curves = o.curves || []; + var legendItems = []; + // y=0 в экранных px (для клипа заливки к оси X) + видимая область для клипа + var zeroPy = this._toPx(0, 0)[1]; + for (var ci = 0; ci < curves.length; ci++) { + var cv = curves[ci]; + // сэмплинг: экранные точки [px,py,worldY], разрывы как null (не-finite y -> пропуск сегмента) + var pts = []; + for (var i = 0; i < n; i++) { + var xv = a + step * i; + env[o.varName] = xv; + var yv = cv.exprFn.ev(env); + if (typeof yv !== 'number' || !isFinite(yv)) { pts.push(null); continue; } + var p = this._toPx(xv, yv); + pts.push([p[0], p[1], yv]); + } + + ctx.save(); + // заливка под кривой (между кривой и осью y=0), клиппится к видимой высоте + if (cv.fill) { + var fillCol = (cv.fill === true) ? _fillAlpha(cv.color, 0.18) : cv.fill; + ctx.save(); + ctx.globalAlpha = cv.opacity; + ctx.fillStyle = fillCol; + ctx.shadowBlur = 0; + this._fillUnderCurve(ctx, pts, _clamp(zeroPy, 0, H)); + ctx.restore(); + } + // линия кривой (через _applyStroke: dash/opacity/glow/width) + this._applyStroke(ctx, cv); + ctx.strokeStyle = cv.color; + ctx.beginPath(); + var started = false; + for (var k = 0; k < pts.length; k++) { + if (!pts[k]) { started = false; continue; } + if (!started) { ctx.moveTo(pts[k][0], pts[k][1]); started = true; } + else ctx.lineTo(pts[k][0], pts[k][1]); + } + ctx.stroke(); + // маркеры узлов (с прореживанием: не чаще ~28px по экрану) + if (cv.marker && cv.marker !== 'none') { + this._drawCurveMarkers(ctx, pts, cv); + } + ctx.restore(); + + if (o.legend && cv.label) legendItems.push({ color: cv.color, label: cv.label }); } - ctx.stroke(); - ctx.restore(); // восстановить env if (hadPrev) env[o.varName] = prev; else delete env[o.varName]; + + // легенда (поверх кривых, в углу области plot, на canvas) + if (legendItems.length) this._drawLegend(ctx, W, H, legendItems); + }; + + /* Заливка области под полилинией к базовой линии y=baseY (экранные px). Каждый + непрерывный сегмент (между разрывами null) заливается отдельным замкнутым контуром: + curve-up -> вдоль кривой -> curve-down к baseY -> закрыть. baseY клиппится к canvas. */ + SimEngineInstance.prototype._fillUnderCurve = function (ctx, pts, baseY) { + var i = 0, n = pts.length; + while (i < n) { + // найти начало непрерывного сегмента + while (i < n && !pts[i]) i++; + var startI = i; + while (i < n && pts[i]) i++; + var endI = i; // [startI, endI) + if (endI - startI < 2) continue; // сегмент из <2 точек — заливать нечего + ctx.beginPath(); + ctx.moveTo(pts[startI][0], baseY); + for (var k = startI; k < endI; k++) ctx.lineTo(pts[k][0], pts[k][1]); + ctx.lineTo(pts[endI - 1][0], baseY); + ctx.closePath(); + ctx.fill(); + } + }; + + /* Маркеры узлов кривой (dot|ring) с прореживанием по экранному расстоянию (~28px), + чтобы не рисовать сотни точек. Цвет — цвет кривой; opacity наследуется от ctx. */ + SimEngineInstance.prototype._drawCurveMarkers = function (ctx, pts, cv) { + var MIN_PX = 28; // минимальный шаг между маркерами по экрану + var r = Math.max(2.5, (cv.width || 2) + 1.5); + var marker = { color: cv.color, fillColor: cv.color, opacity: cv.opacity, glow: false, + pointStyle: (cv.marker === 'ring') ? 'hollow' : 'filled', width: cv.width || 2 }; + var lastX = -1e9, lastY = -1e9; + for (var k = 0; k < pts.length; k++) { + var p = pts[k]; + if (!p) continue; + var dx = p[0] - lastX, dy = p[1] - lastY; + if (dx * dx + dy * dy < MIN_PX * MIN_PX) continue; + this._drawPoint(ctx, marker, p[0], p[1], r); + lastX = p[0]; lastY = p[1]; + } + }; + + /* Компактная легенда в углу области plot (на canvas, без DOM): цветная метка + текст. + Позиция: верх-право, со смещением вниз, чтобы не наезжать на ось Y/подписи. */ + SimEngineInstance.prototype._drawLegend = function (ctx, W, H, items) { + if (!items.length) return; + ctx.save(); + ctx.font = '12px Manrope,system-ui,sans-serif'; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'left'; + var pad = 8, rowH = 18, swatch = 11, gap = 7; + // ширина по самой длинной подписи + var maxTxt = 0; + for (var i = 0; i < items.length; i++) { + var w = ctx.measureText(items[i].label).width; + if (w > maxTxt) maxTxt = w; + } + var boxW = pad * 2 + swatch + gap + Math.ceil(maxTxt); + var boxH = pad * 2 + items.length * rowH - (rowH - 14); + // верх-право; отступ от края, не наезжает на бар кнопок (right/bottom) и оси + var bx = W - boxW - 12, by = 12; + if (bx < 6) bx = 6; + // фон-плашка (полупрозрачная тёмная, как readout-бейджи) + ctx.globalAlpha = 1; + ctx.fillStyle = 'rgba(13,13,26,0.78)'; + ctx.strokeStyle = 'rgba(255,255,255,0.12)'; + ctx.lineWidth = 1; + if (ctx.roundRect) { ctx.beginPath(); ctx.roundRect(bx, by, boxW, boxH, 8); ctx.fill(); ctx.stroke(); } + else { ctx.fillRect(bx, by, boxW, boxH); ctx.strokeRect(bx, by, boxW, boxH); } + for (var j = 0; j < items.length; j++) { + var cy = by + pad + 7 + j * rowH; + // цветная метка (линия-свотч) + ctx.strokeStyle = items[j].color; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(bx + pad, cy); ctx.lineTo(bx + pad + swatch, cy); + ctx.stroke(); + // текст метки (светлый, без пользовательского цвета в DOM) + ctx.fillStyle = 'rgba(255,255,255,0.88)'; + ctx.fillText(items[j].label, bx + pad + swatch + gap, cy); + } + ctx.restore(); }; /* ── readout: живое значение выражения как бейдж на оверлее ── */ diff --git a/plans/sim-builder/CONTEXT.md b/plans/sim-builder/CONTEXT.md index 9e1a98c..527e1a3 100644 --- a/plans/sim-builder/CONTEXT.md +++ b/plans/sim-builder/CONTEXT.md @@ -1,6 +1,30 @@ # Feature Context: Конструктор симуляций (SimForge) ## Current State +- **РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) — P3 «Графики/диаграммы» РЕАЛИЗОВАН** (рабочее дерево, не + закоммичено; ветка `feature/sim-builder`). Файл: только `frontend/js/labs/_sim_engine.js`. Расширен + `_drawPlot` + ветка `type==='plot'` в `_prepareObjects`. Оси/сетка/подписи уже из P1 — не дублировались. + - **Несколько кривых**: нормализуются в `prep.curves[]`, приоритет источника `curves:[{...}]` → + `exprs:['sin(x)','x^2']` → одиночный `expr` (легаси, обратная совместимость сохранена). Каждой кривой + свой цвет (явный `color` или `DEFAULT_PALETTE[i%8]`). `prep.exprFn` = первой кривой (для trace-режима). + - **Поля кривой** (`curves[i]`): `expr`, `color`, `label`(→легенда), `width`, `lineStyle`, `opacity`, + `fill`(true→полупрозр. цвет / строка), `marker`(none|dot|ring). Не заданные наследуют plot-уровень. + **Plot-уровневые `fill`/`marker`** — дефолт для всех кривых. + - **Заливка под кривой** `_fillUnderCurve` (между кривой и y=0, посегментно — разрывы у не-finite не + сливаются; baseY клиппится к canvas). **Маркеры** `_drawCurveMarkers` (переиспользует `_drawPoint`, + прорежены ~28px). **Легенда** `_drawLegend` на canvas (тёмная плашка + свотч + светлый текст, верх-право, + авто при `label`, `legend:false` отключает). Новые модульные хелперы `_markerStyle`/`_fillAlpha`. + - **Безопасность**: цвета только в canvas-стоки (strokeStyle/fillStyle/fillText фикс-цвет легенды); + DOM-style с пользовательским цветом не используется; eval нет. Каждая кривая в своём save/restore, + легенда на внешнем уровне. + - Верификация: `node --check` OK; headless vm-смоук (canvas-стаб со счётчиком save/restore + РЕАЛЬНЫЕ + `_sim_expr`+`_sim_engine`) 10/10: легаси/exprs[]/curves+fill+marker+legend/наследование/не-finite + (1/x,tan)/legend:false/trace±range/fillUnder+markers с null/регресс point-vector-circle-rect — все PASS; + ctx сбалансирован (depth→0, нет underflow). Эмодзи нет (только пре-существующие → в комментариях); eval=0. + Temp-смоук удалён. git status: тронут только `_sim_engine.js`. + - **Следующее (P4):** UI билдера + контролы стиля (`sim-builder.html`/`sim-builder.js`) — дать новым полям + plot контролы: список кривых (add/del, expr+color+label+width+lineStyle+opacity+fill+marker), plot-fill/ + marker, тумблер легенды; плюс per-объект color/opacity/width/dash, z-order, дублирование, мобайл. - **РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) — P2 «Качество графики объектов» РЕАЛИЗОВАН** (рабочее дерево, не закоммичено; ветка `feature/sim-builder`). Файл: только `frontend/js/labs/_sim_engine.js`. Один движок → эффект и в билдере, и в /lab, и на доске. @@ -25,9 +49,7 @@ масштаб; все поля прочитаны; палитра применена + явный color сохранён; трасса накоплена; destroy чист. Эмодзи нет (скан кодпойнтов: только пре-существующие →/─/═/∞ в комментариях); eval=0; new Function — только в комментарии стр.15. git status: тронут только `_sim_engine.js`. - - **Следующее (P3):** графики/диаграммы (`_drawPlot`): оси-деления plot, несколько кривых, заливка под - кривой, маркеры точек (переиспользовать `_drawPoint`), легенда. Хелперы `_applyStroke`/`_fillStyleFor`/ - `_drawPoint` готовы к переиспользованию. + - **Следующее (P3):** РЕАЛИЗОВАНО (см. блок P3 выше) — несколько кривых, заливка, маркеры, легенда. - **РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) — P1 «Рабочее поле» РЕАЛИЗОВАН** (рабочее дерево, не закоммичено; ветка `feature/sim-builder`, общая с параллельной сессией materials/quota). Файл: только `frontend/js/labs/_sim_engine.js` (sim-builder.html НЕ потребовался). Один движок → эффект и в билдере, и в /lab, и на доске. diff --git a/plans/sim-builder/IMPROVEMENTS.md b/plans/sim-builder/IMPROVEMENTS.md index 8fc4891..0e2d681 100644 --- a/plans/sim-builder/IMPROVEMENTS.md +++ b/plans/sim-builder/IMPROVEMENTS.md @@ -68,9 +68,31 @@ кривых). Для P3 расширять `_drawPlot` — оси-делений plot, несколько кривых, заливка под кривой, маркеры точек (можно переиспользовать `_drawPoint`), легенда. Хелперы `_applyStroke`/`_fillStyleFor`/`_drawPoint` готовы к переиспользованию. -- [ ] **P3 — Графики/диаграммы (визуал charts).** Для plot: оси с делениями/подписями, несколько кривых, - заливка под кривой, маркеры точек, легенда; аккуратный стиль диаграмм. Файл: `_sim_engine.js` (+ билдер - поля plot). +- [x] **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`. @@ -82,6 +104,6 @@ |-------|--------|--------|-----------| | P1 Working field | Done | ✅ PASS | ✅ | | P2 Object graphics | Done | ✅ PASS | ✅ | -| P3 Charts | ⬜ | ⬜ | ⬜ | +| P3 Charts | Done | ✅ PASS | ✅ | | P4 Builder UI | ⬜ | ⬜ | ⬜ | | P5 Direct manip + history | ⬜ | ⬜ | ⬜ |