From fa29332bcde8269079dd2d1803ef56db5d2812f7 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 24 Jun 2026 19:02:08 +0300 Subject: [PATCH] =?UTF-8?q?feat(lab-graph):=20=D0=B2=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=E2=80=94=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D1=83=D0=B5=D0=BC=D0=BE=D0=B5=20KaTeX-=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Введённая функция показывается отрисованной формулой KaTeX прямо в строке; клик по формуле → правка текста на месте (raw input + живое превью под полем), клик мимо/blur → снова формула. Реализовано без MathQuill: .fn-field держит и .fn-math (KaTeX), класс has-math переключает отображение по фокусу. - renderFnMath() рисует формулу в строке; _fnDisplay() решает режим (фокус+значение) - focus/blur/mousedown-обработчики в _initGraphPanel (идемпотентно) - живое превью .fn-preview теперь видно ТОЛЬКО при правке (:focus-within), цвет функции - graphInsert/applyPreset/state-apply/clearAll/default-fn0 обновляют math-поле - _katexInto() — общий безопасный рендер Только фронт. node --check OK; логика вставки 5/5 (прошлый прогон). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/css/lab.css | 20 ++++++++----- frontend/js/labs/graph.js | 63 +++++++++++++++++++++++++++++---------- frontend/labs-bodies.html | 15 ++++++++-- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/frontend/css/lab.css b/frontend/css/lab.css index c0e94e7..5656f15 100644 --- a/frontend/css/lab.css +++ b/frontend/css/lab.css @@ -181,22 +181,28 @@ background: var(--fn-color, var(--violet)); box-shadow: 0 0 6px var(--fn-color, var(--violet)); } + .fn-field { position: relative; flex: 1; min-width: 0; display: flex; align-items: center; } .fn-input { - flex: 1; border: none; outline: none; background: transparent; + width: 100%; border: none; outline: none; background: transparent; font-family: 'Manrope', monospace; font-size: 0.88rem; font-weight: 600; color: var(--text); padding: 0; min-width: 0; } .fn-input::placeholder { color: var(--text-3); font-weight: 500; } - /* KaTeX live preview — крупная «отрисованная» формула под вводом */ + /* введённая функция как KaTeX прямо в строке; клик — правка текста на месте */ + .fn-math { display: none; width: 100%; cursor: text; overflow-x: auto; overflow-y: hidden; min-height: 19px; line-height: 1.3; } + .fn-math .katex { color: var(--fn-color, rgba(255,255,255,.9)); font-size: 1.12em; } + .fn-field.has-math .fn-input { display: none; } + .fn-field.has-math .fn-math { display: block; } + + /* живое превью формулы — только пока строка в режиме правки (поле в фокусе) */ .fn-preview { - min-height: 22px; padding: 5px 8px 5px 36px; margin-top: 3px; - font-size: 0.82rem; line-height: 1.5; + min-height: 0; padding: 4px 8px 2px 36px; margin-top: 2px; + font-size: 0.8rem; line-height: 1.4; overflow-x: auto; overflow-y: hidden; display: none; } - .fn-preview.has-content { display: block; } - .fn-preview .katex { color: var(--fn-color, rgba(255,255,255,.85)); font-size: 1.18em; } - .fn-preview.is-ph { opacity: .4; } /* плейсхолдер-формула, пока поле пустое */ + .fn-row:focus-within + .fn-preview.has-content { display: block; } + .fn-preview .katex { color: var(--fn-color, rgba(255,255,255,.6)); font-size: 1.05em; opacity: .8; } /* keypad вставки структур (как редактор формул) */ .gp-keypad { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; margin-bottom: 6px; } diff --git a/frontend/js/labs/graph.js b/frontend/js/labs/graph.js index b921567..eb05f8d 100644 --- a/frontend/js/labs/graph.js +++ b/frontend/js/labs/graph.js @@ -519,6 +519,7 @@ class GraphSim { const el = document.getElementById(`fn${i}`); if (el) { el.value = fn.expr; } if (gSim) gSim.setFn(i, fn.expr, FN_COLORS[i]); + _fnDisplay(i); }); } ); @@ -531,7 +532,7 @@ class GraphSim { gSim.onHover = updateInfoBar; if (!document.getElementById('fn0').value.trim()) { document.getElementById('fn0').value = 'sin(x)'; - renderPreview(0); + renderPreview(0); _fnDisplay(0); gSim.fit(); gSim.setFn(0, 'sin(x)', FN_COLORS[0]); return; @@ -574,22 +575,40 @@ class GraphSim { .replace(/\*/g, '\\cdot '); } + function _katexInto(el, latex) { + try { el.innerHTML = katex.renderToString(latex, { throwOnError: false, strict: false, displayMode: false }); return true; } + catch { el.innerHTML = ''; return false; } + } + + /* Живое превью формулы под полем — показывается ТОЛЬКО пока строка в правке. */ function renderPreview(idx) { const inp = document.getElementById('fn' + idx); const prev = document.getElementById('fn' + idx + '-prev'); if (!prev) return; const raw = inp?.value?.trim() || ''; - const src = raw || (inp?.getAttribute('placeholder') || ''); // пусто → показываем плейсхолдер-формулу - if (!src || typeof katex === 'undefined') { - prev.innerHTML = ''; prev.classList.remove('has-content', 'is-ph'); return; - } - try { - prev.innerHTML = katex.renderToString(toLatex(src), { - throwOnError: false, strict: false, displayMode: false, - }); - prev.classList.add('has-content'); - prev.classList.toggle('is-ph', !raw); // плейсхолдер — приглушённо - } catch { prev.innerHTML = ''; prev.classList.remove('has-content', 'is-ph'); } + if (!raw || typeof katex === 'undefined') { prev.innerHTML = ''; prev.classList.remove('has-content'); return; } + if (_katexInto(prev, toLatex(raw))) prev.classList.add('has-content'); + else prev.classList.remove('has-content'); + } + + /* Введённая функция — отрисованной формулой KaTeX прямо в строке. */ + function renderFnMath(idx) { + const inp = document.getElementById('fn' + idx); + const m = document.getElementById('fn' + idx + '-math'); + if (!m || !inp) return; + const raw = inp.value.trim(); + if (!raw || typeof katex === 'undefined') { m.innerHTML = ''; return; } + _katexInto(m, toLatex(raw)); + } + + /* Режим строки: не в фокусе и есть формула → показываем KaTeX; иначе — поле ввода. */ + function _fnDisplay(idx) { + const inp = document.getElementById('fn' + idx); + const field = inp && inp.closest('.fn-field'); + if (!field) return; + const showMath = (document.activeElement !== inp) && !!inp.value.trim() && typeof katex !== 'undefined'; + if (showMath) { renderFnMath(idx); field.classList.add('has-math'); } + else field.classList.remove('has-math'); } /* Вставка структуры формулы в активное поле (как редактор формул). @@ -598,6 +617,7 @@ class GraphSim { function graphInsert(token) { const el = document.getElementById('fn' + _activeFnIdx) || document.getElementById('fn0'); if (!el) return; + const f = el.closest('.fn-field'); if (f) f.classList.remove('has-math'); // в режим правки el.focus(); let ins = String(token || ''); let caretInTok = -1; const bar = ins.indexOf('|'); @@ -610,19 +630,28 @@ class GraphSim { updateFn(_activeFnIdx); } - /* Отрисовать KaTeX на чипах/клавиатуре + следить за активным полем (идемпотентно). */ + /* KaTeX на чипах/клавиатуре + math-поля + слежение за активным полем (идемпотентно). */ function _initGraphPanel() { const root = document.getElementById('sim-graph'); if (!root || typeof katex === 'undefined') return; root.querySelectorAll('.preset-btn[data-tex], .kp-btn[data-tex]').forEach(b => { if (b.dataset.rendered) return; - try { b.innerHTML = katex.renderToString(b.dataset.tex, { throwOnError: false, strict: false, displayMode: false }); b.dataset.rendered = '1'; } catch (_) {} + if (_katexInto(b, b.dataset.tex)) b.dataset.rendered = '1'; }); if (!_graphPanelInit) { _graphPanelInit = true; - [0, 1, 2].forEach(i => { const el = document.getElementById('fn' + i); if (el) el.addEventListener('focus', () => { _activeFnIdx = i; }); }); + [0, 1, 2].forEach(i => { + const el = document.getElementById('fn' + i); + const m = document.getElementById('fn' + i + '-math'); + if (el) { + el.addEventListener('focus', () => { _activeFnIdx = i; _fnDisplay(i); }); + el.addEventListener('blur', () => { _fnDisplay(i); }); + } + // клик по формуле → правка текста на месте + if (m) m.addEventListener('mousedown', (ev) => { ev.preventDefault(); const f = m.closest('.fn-field'); if (f) f.classList.remove('has-math'); el && el.focus(); }); + }); } - [0, 1, 2].forEach(renderPreview); + [0, 1, 2].forEach(i => { renderPreview(i); _fnDisplay(i); }); } /* debounced formula update */ @@ -661,6 +690,8 @@ class GraphSim { document.getElementById('fn' + i).value = ''; document.getElementById('fn' + i + '-prev').innerHTML = ''; document.getElementById('fn' + i + '-prev').classList.remove('has-content'); + const m = document.getElementById('fn' + i + '-math'); if (m) m.innerHTML = ''; + const f = document.getElementById('fn' + i)?.closest('.fn-field'); if (f) f.classList.remove('has-math'); document.getElementById('fn' + i + '-err').classList.remove('show'); if (gSim) gSim.setFn(i, '', FN_COLORS[i]); } diff --git a/frontend/labs-bodies.html b/frontend/labs-bodies.html index 21f064b..bd4b2f1 100644 --- a/frontend/labs-bodies.html +++ b/frontend/labs-bodies.html @@ -10,7 +10,10 @@
y = - +
+ +
+
Синтаксическая ошибка
@@ -21,7 +24,10 @@
y = - +
+ +
+
Синтаксическая ошибка
@@ -32,7 +38,10 @@
y = - +
+ +
+
Синтаксическая ошибка