Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6743dfcbce | |||
| b6f854fc77 | |||
| 69e219ae8c |
@@ -158,3 +158,30 @@ 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, тумблер легенды.
|
||||
|
||||
### SimForge improvements — P4 (UI билдера + контролы стиля) — Learnings
|
||||
|
||||
Всё в `frontend/sim-builder.html` (CSS) + `frontend/js/sim-builder.js` (логика). `_sim_engine.js`/`js/api.js`/lab.* НЕ тронуты — билдер только генерит спеку, которую движок P2/P3 уже рисует.
|
||||
|
||||
- **Контролы стиля = data-driven хелперы** (рядом с `field`/`miniField`): `colorCtl(label,attr,val,clearable)` (нативный `<input type=color>` + текст + опц.очистка), `rangeCtl` (слайдер 0..1 для opacity), `selectCtl` (lineStyle/pointStyle/marker). Блок «Стиль» в каждом объекте — `Builder.styleBlock(o)`, набор полей решает `STYLE_FOR[type]` ({opacity,line,point,glow,grad}).
|
||||
- **Цвет: текст — источник истины, не нативный пикер.** Нативный `<input type=color>` умеет только `#rrggbb`; rgba()/named он бы потерял. Поэтому пикер лишь ПИШЕТ в текстовое поле и диспатчит `input` (его ловит основной `data-of`/`data-cvf`-обработчик). `Builder.wireColorControls(row)` связывает пикер↔текст↔очистку. `toHexColor(v)` приводит #rgb/#rrggbb→#rrggbb (иначе #000000) для нативного пикера. Очистка (fill/trailColor) = пустая строка → `stripObj` выбрасывает → «нет заливки».
|
||||
- **Round-trip как чинили range в Ф4: дефолты НЕ сериализуем.** `stripObj.isDefaultStyle(k,v)` выбрасывает `hidden`, `glow:false`, `lineStyle:'solid'`, `pointStyle:'filled'`, `opacity:1`, `trail/closed:false`. Спека минимальна, а save→load→save идемпотентен (loadFromSim восстанавливает дефолты из контролов). Селекты хранят дефолтную строку в `st`, но она не уходит в спеку. Проверено vm-смоуком.
|
||||
- **Plot теперь — кривые.** UI-модель plot = `{var,range_a/b,samples,trace,legend,plotFill,plotMarker,curves:[{_uid,expr,color,label,width,lineStyle,opacity,fill(bool),fillColor,marker}]}`. `plotEditor`+`curveEditor` рисуют, `loadPlot` (spec→UI: `curves[]`→`exprs[]`→легаси `expr`; легаси plot-level width/lineStyle/opacity наследуются кривой), `normalizePlotForSpec`+`stripCurve` (UI→spec). **Одиночная «простая» кривая (только expr+color, без plot-fill/marker) → легаси `{expr,color}`**, иначе `curves:[...]` — не ломает обратную совместимость. `legend:false` эмитится только при выкл (движок включает легенду авто при label). Валидация: каждая кривая + границы range через `SimExpr.compile`.
|
||||
- **z-order / видимость / дублирование — чисто в билдере** (движок не трогали): z-order = порядок массива `st.objects`/`st.plots` (кнопки вверх/вниз свапают, крайние disabled). Видимость `hidden:true` — билдерский флаг, `buildSpec` фильтрует hidden из спеки (движок про hidden не знает). Дублирование — `JSON.parse(JSON.stringify(o))` + новый `_uid` + `id+'_copy'`, вставка после оригинала. Аналогично у plot.
|
||||
- **Новые ICON** (inline SVG `.ic`, ⛔ без эмодзи): up/down/copy/eye/eyeOff/clearX. Новые CSS-классы в ls.css-стиле; заголовок объекта `flex-wrap` + 26px-кнопки; медиа ≤920px (была) + новый ≤560px (поля/стили в один столбец).
|
||||
- **Верификация P4**: `node --check` sim-builder.js + извлечённого инлайна html — OK; эмодзи/eval/new Function — 0 (скан кодпойнтов обоих файлов); headless vm-смоук (DOM/SimExpr-стаб) 27+12+2 PASS: стили объекта в спеке, round-trip объектов ×2 идемпотентен, plot с 2 кривыми (все поля) + round-trip ×2, легаси-одиночная→легаси-форма, hidden исключён, z-order=порядок, дефолты-стрип, шаблонные легаси-plot save→load→save стабильны. Temp удалены. git status: только sim-builder.html и sim-builder.js.
|
||||
- **На P5 (прямое манипулирование + история)**: drag сейчас только x/y point/circle/label/readout/rect + конец segment/vector (`bindPreviewDrag` через `inst._toWorld`). Расширять до всех типов + snap-к-сетке + выравнивание (нужны хит-тесты/ручки в `_sim_engine.js`). Undo/redo: `this.st` сериализуем JSON → стек снапшотов в Builder, restore + `renderPanels`/`scheduleRemount`.
|
||||
|
||||
+195
-17
@@ -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: живое значение выражения как бейдж на оверлее ── */
|
||||
|
||||
+797
-76
File diff suppressed because it is too large
Load Diff
@@ -82,10 +82,42 @@
|
||||
|
||||
/* ── объект ── */
|
||||
.sbu-obj.sel { border-color: var(--violet); box-shadow: 0 0 0 2px rgba(155,93,229,0.16); }
|
||||
.sbu-obj-hdr { display: flex; align-items: center; gap: 6px; }
|
||||
.sbu-obj.is-hidden, .sbu-plot.is-hidden { opacity: .62; }
|
||||
.sbu-obj.is-hidden .sbu-obj-fields, .sbu-obj.is-hidden .sbu-obj-style { opacity: .7; }
|
||||
.sbu-obj-hdr { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
|
||||
.sbu-obj-type { font-size: .72rem; font-weight: 800; color: var(--violet); flex-shrink: 0; }
|
||||
.sbu-in-id { flex: 1; max-width: 120px; }
|
||||
.sbu-in-id { flex: 1; min-width: 64px; max-width: 110px; }
|
||||
.sbu-obj-hdr .sbu-icon-btn { width: 26px; height: 26px; }
|
||||
.sbu-icon-btn:disabled { opacity: .32; cursor: default; pointer-events: none; }
|
||||
.sbu-icon-btn.active { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,0.1); }
|
||||
.sbu-zord { color: var(--text-3); }
|
||||
.sbu-obj-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
|
||||
/* ── блок «Стиль» объекта (P4) ── */
|
||||
.sbu-obj-style { border-top: 1px dashed var(--border); padding-top: 7px; margin-top: 1px; display: flex; flex-direction: column; gap: 7px; }
|
||||
.sbu-obj-style .sbu-sub { margin-top: 0; }
|
||||
.sbu-style-row { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; align-items: end; }
|
||||
.sbu-style-row > * { min-width: 0; }
|
||||
.sbu-grad-row { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
|
||||
|
||||
/* ── color-picker контрол (нативный пикер + текст + очистка) ── */
|
||||
.sbu-color-mini { min-width: 0; }
|
||||
.sbu-color-wrap { display: flex; align-items: center; gap: 5px; }
|
||||
.sbu-color-pick { width: 30px; height: 30px; flex-shrink: 0; padding: 0; border: 1px solid var(--border); border-radius: 8px; background: #fff; cursor: pointer; }
|
||||
.sbu-color-pick::-webkit-color-swatch-wrapper { padding: 3px; }
|
||||
.sbu-color-pick::-webkit-color-swatch { border: none; border-radius: 5px; }
|
||||
.sbu-color-wrap .sbu-in-color { flex: 1; min-width: 0; }
|
||||
.sbu-color-clr { width: 26px; height: 26px; flex-shrink: 0; border: 1px solid var(--border); border-radius: 7px; background: #fff; color: var(--text-3); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.sbu-color-clr:hover { border-color: #ef4444; color: #ef4444; }
|
||||
|
||||
/* ── range (opacity) ── */
|
||||
.sbu-range-mini { min-width: 0; }
|
||||
.sbu-range-val { color: var(--violet); font-variant-numeric: tabular-nums; }
|
||||
.sbu-range { width: 100%; accent-color: var(--violet); height: 30px; box-sizing: border-box; }
|
||||
|
||||
/* ── кривые графика ── */
|
||||
.sbu-curves { display: flex; flex-direction: column; gap: 8px; }
|
||||
.sbu-curve { border: 1px solid var(--border); border-radius: 9px; padding: 8px; background: #fafbfd; display: flex; flex-direction: column; gap: 7px; }
|
||||
.sbu-curve-del { width: 24px; height: 24px; }
|
||||
.sbu-of { display: flex; flex-direction: column; gap: 2px; }
|
||||
.sbu-of-lbl { font-size: .66rem; color: var(--text-3); display: flex; align-items: center; justify-content: space-between; gap: 4px; }
|
||||
.sbu-fx { font-size: .62rem; font-weight: 800; font-style: italic; color: var(--violet); background: rgba(155,93,229,0.1); border: none; border-radius: 5px; padding: 1px 6px; cursor: pointer; }
|
||||
@@ -113,6 +145,11 @@
|
||||
.sbu-panels { width: auto; max-height: 50vh; border-right: none; border-bottom: 1px solid var(--border); }
|
||||
.sbu-preview { min-height: 320px; }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.sbu-obj-fields { grid-template-columns: 1fr; }
|
||||
.sbu-style-row, .sbu-grad-row { grid-template-columns: 1fr; }
|
||||
.sbu-row4 { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,6 +1,85 @@
|
||||
# Feature Context: Конструктор симуляций (SimForge)
|
||||
|
||||
## Current State
|
||||
- **РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) ЗАВЕРШЁН — P5 «Прямое манипулирование + история» РЕАЛИЗОВАН**
|
||||
(рабочее дерево, не закоммичено; ветка `feature/sim-builder`). Файл: ТОЛЬКО `frontend/js/sim-builder.js`.
|
||||
`_sim_engine.js` НЕ тронут — `_toWorld`/`_toPx`/`_niceStep` уже публичны на инстансе движка, хука не
|
||||
потребовалось (в IMPROVEMENTS.md P5 предполагались правки движка — не понадобились).
|
||||
- **Прямое манипулирование** (`bindPreviewDrag` переписан): «ручки» через `handlesOf(obj)` для ВСЕХ
|
||||
позиционируемых типов — point/circle/label/readout/rect (одна ручка x,y), segment/vector (origin x1,y1 +
|
||||
end x2,y2 ИЛИ origin+dx/dy), polyline/path (по ручке на числовую вершину `points`). Хит-тест `pickHandle`
|
||||
(14px, через `_toPx`); режимы pointerdown: `handle`/`place` (единств. ручка — клик ставит)/`body`
|
||||
(несколько ручек — относительный сдвиг)/`none`. Поля-выражения `blocked` (не затираются). `refreshObjFields`
|
||||
расширен на x1/y1/x2/y2/dx/dy/points.
|
||||
- **Snap-к-сетке**: тумблер в тулбаре (`_snap`, `toggleSnap`, иконка `ICON.grid`, активность — инлайн
|
||||
`SNAP_ACTIVE_CSS`); округление к `_niceStep(34)` (минорный шаг сетки; fallback 0.5). Выравнивание к чужим
|
||||
координатам не делалось (бонус; snap достаточно — отмечено как частичное).
|
||||
- **Undo/Redo**: стек `JSON.stringify(this.st)` (глубина 50), `pushHistory` (до мутации, без дублей, сброс
|
||||
redo), `snapField` (один снапшот на сессию правки поля через focusin/`_fieldSnapTaken`). Структурные
|
||||
операции — снапшот сразу; drag — один на сессию (no-op откатывается). Кнопки undo/redo (SVG `.ic`) +
|
||||
Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y (`bindKeyboardShortcuts`, игнорит фокус в полях). `loadFromSim` обнуляет
|
||||
историю; `_restoreSnapshot` → renderPanels + scheduleRemount.
|
||||
- Верификация: `node --check` OK; эмодзи/eval — 0; vm-смоук 38/38 PASS (drag всех типов + body-move; snap;
|
||||
защита выражений; undo/redo drag+add; лимит стека; round-trip идемпотентен). buildSpec/валидация не тронуты.
|
||||
git status: тронут только sim-builder.js (`_sim_engine.js` в статусе — чужой коммит параллельной сессии
|
||||
«goal/game», мной НЕ редактировался).
|
||||
- **РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md) — P4 «UI билдера + контролы стиля» РЕАЛИЗОВАН** (рабочее дерево, не
|
||||
закоммичено; ветка `feature/sim-builder`). Файлы: только `frontend/sim-builder.html` + `frontend/js/sim-builder.js`.
|
||||
`_sim_engine.js`/`js/api.js`/lab.* НЕ тронуты — билдер лишь генерит спеку, которую движок (P2/P3) уже умеет рисовать.
|
||||
- **Контролы стиля объекта** (блок «Стиль», `STYLE_FOR[type]`): слайдер непрозр.(`opacity` 0..1),
|
||||
select `lineStyle`(solid/dashed/dotted), `pointStyle`(только point), тумблер `glow`, тумблер градиент-
|
||||
заливки(circle/rect → `gradient:[c0,c1]`). Цвета — `colorCtl`: нативный `<input type=color>` + текст
|
||||
(источник истины, держит rgba/named) + очистка для fill/trailColor. Синхрон — `wireColorControls`,
|
||||
`toHexColor`→`#rrggbb`. Per-объект уже были width/color в OBJ_FIELDS — переведены на color-пикеры.
|
||||
- **Редактор кривых plot** (`plotEditor`/`curveEditor`): UI-модель `{var,range_a/b,samples,trace,legend,
|
||||
plotFill,plotMarker,curves:[{expr,color,label,width,lineStyle,opacity,fill,fillColor,marker}]}`. Список
|
||||
кривых (add/del, минимум 1), на кривую все P3-поля + fx-палитра, plot-уровневые fill/marker/легенда.
|
||||
`loadPlot` (spec→UI: curves[]→exprs[]→legacy expr; легаси plot-level width/lineStyle/opacity → в кривую),
|
||||
`normalizePlotForSpec`+`stripCurve` (UI→spec). Одиночная простая кривая → легаси `{expr,color}`, иначе
|
||||
`curves:[...]`. `legend:false` эмитится только при выкл.
|
||||
- **Список объектов/графиков**: z-order вверх/вниз (порядок массива = порядок отрисовки), видимость
|
||||
(`hidden:true` — чисто билдерский флаг, фильтруется в `buildSpec`, движок не знает), дублировать
|
||||
(deep-clone+новый `_uid`, `id+'_copy'`), удалить. Иконки — новые inline SVG `.ic` (up/down/copy/eye/eyeOff/clearX).
|
||||
- **Минимизация спеки + стабильный round-trip**: `stripObj.isDefaultStyle` выбрасывает дефолты
|
||||
(glow:false, lineStyle:'solid', pointStyle:'filled', opacity:1, trail/closed:false) и `hidden`. Save→load→
|
||||
save идемпотентен (loadFromSim восстанавливает дефолты из контролов).
|
||||
- **Дизайн/мобайл**: новые CSS-классы в ls.css-стиле (`.sbu-obj-style`/`.sbu-style-row`/`.sbu-color-*`/
|
||||
`.sbu-range`/`.sbu-curve(s)`/`.is-hidden`/`.sbu-grad-row`); заголовок объекта flex-wrap + 26px-кнопки;
|
||||
медиа ≤920px (раскладка) + новый ≤560px (поля/стили в один столбец). Пустые состояния дополнены.
|
||||
- **Безопасность**: выражения только через `SimExpr.compile`; цвета попадают лишь в спеку (canvas-стоки
|
||||
движка), DOM-style с польз.цветом не используется; eval/new Function — нет.
|
||||
- Верификация: `node --check` sim-builder.js + извлечённого инлайна html — OK; эмодзи нет (скан кодпойнтов
|
||||
обоих файлов — 0); eval/new Function — 0; headless vm-смоук (DOM/SimExpr-стаб) 27+12 PASS: стили объекта в
|
||||
спеке, round-trip объектов идемпотентен ×2, plot с 2 кривыми (label/marker/lineStyle/opacity/fill-цвет/
|
||||
range/samples) + round-trip ×2, легаси-одиночная кривая → легаси-форма + round-trip, hidden исключает из
|
||||
спеки, z-order=порядок массива, дефолты-стрип; +шаблонные легаси-plot save→load→save стабильны (2 PASS).
|
||||
Temp удалены. git status: тронуты только sim-builder.html и sim-builder.js.
|
||||
- **Следующее (P5):** прямое манипулирование на сцене (drag всех типов + snap-к-сетке + выравнивание) и
|
||||
undo/redo. Потребуются правки `_sim_engine.js` (хит-тесты/ручки) + `sim-builder.js` (стек снапшотов `this.st`).
|
||||
- **РАУНД УЛУЧШЕНИЙ (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 +104,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, и на доске.
|
||||
@@ -213,6 +290,11 @@
|
||||
- Reuse > переписывание: сначала смотреть `_fx_motion`, `_graph_panel`, `graph.js`.
|
||||
|
||||
## RESUME STATE
|
||||
- **РАУНД УЛУЧШЕНИЙ (IMPROVEMENTS.md):** P1+P2+P3 закоммичены; **P4 «UI билдера + контролы стиля»
|
||||
РЕАЛИЗОВАН** (рабочее дерево, не закоммичено — ждёт ревьюера/оркестратора). Файлы: только
|
||||
`frontend/sim-builder.html` + `frontend/js/sim-builder.js`. Дальше — независимый ревью P4, затем P5
|
||||
(прямое манипулирование на сцене для всех типов + snap/выравнивание + undo/redo; правки `_sim_engine.js`
|
||||
+ `sim-builder.js`). Контракт стилей/кривых из P2/P3-handoff полностью покрыт контролами билдера.
|
||||
- Последний коммит фичи: — (Ф0..Ф7 ВСЕ реализованы, ещё не закоммичены — ждут оркестратора)
|
||||
- Текущая фаза: Phase 7 — Доска онлайн-урока (✅ Implemented, pending commit) — ФИНАЛЬНАЯ.
|
||||
Дальше: Final Review (final-reviewer + security review) → коммит всех фаз → merge в master.
|
||||
|
||||
@@ -68,20 +68,103 @@
|
||||
кривых). Для P3 расширять `_drawPlot` — оси-делений plot, несколько кривых, заливка под кривой, маркеры
|
||||
точек (можно переиспользовать `_drawPoint`), легенда. Хелперы `_applyStroke`/`_fillStyleFor`/`_drawPoint`
|
||||
готовы к переиспользованию.
|
||||
- [ ] **P3 — Графики/диаграммы (визуал charts).** Для plot: оси с делениями/подписями, несколько кривых,
|
||||
заливка под кривой, маркеры точек, легенда; аккуратный стиль диаграмм. Файл: `_sim_engine.js` (+ билдер
|
||||
поля plot).
|
||||
- [ ] **P4 — UI билдера + контролы стиля.** Дизайн-полировка панелей/тулбара (ls.css), нативные color-
|
||||
- [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`.
|
||||
- [x] **P4 — UI билдера + контролы стиля.** Дизайн-полировка панелей/тулбара (ls.css), нативные color-
|
||||
пикеры + opacity/width/dash/линестиль на объект, z-order/дублирование/видимость объектов, пустые
|
||||
состояния, мобайл. Файлы: `frontend/sim-builder.html`, `frontend/js/sim-builder.js`.
|
||||
- [ ] **P5 — Прямое манипулирование на сцене + история.** Drag всех типов (не только point/circle),
|
||||
snap-к-сетке, выравнивание; undo/redo в билдере. Файлы: `_sim_engine.js`, `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`. Идентичность спеки между билдами уже гарантирована.
|
||||
- [x] **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 --check` OK, эмодзи/eval нет.
|
||||
|
||||
## Progress
|
||||
| Phase | Status | Review | Committed |
|
||||
|-------|--------|--------|-----------|
|
||||
| P1 Working field | Done | ✅ PASS | ✅ |
|
||||
| P2 Object graphics | Done | ✅ PASS | ✅ |
|
||||
| P3 Charts | ⬜ | ⬜ | ⬜ |
|
||||
| P4 Builder UI | ⬜ | ⬜ | ⬜ |
|
||||
| P5 Direct manip + history | ⬜ | ⬜ | ⬜ |
|
||||
| P3 Charts | Done | ✅ PASS | ✅ |
|
||||
| P4 Builder UI | Done | ✅ PASS | ✅ |
|
||||
| P5 Direct manip + history | Done | ✅ PASS | ✅ |
|
||||
|
||||
Reference in New Issue
Block a user