diff --git a/backend/scripts/redesign_p8_ch1_2.cjs b/backend/scripts/redesign_p8_ch1_2.cjs
new file mode 100644
index 0000000..7bc90bf
--- /dev/null
+++ b/backend/scripts/redesign_p8_ch1_2.cjs
@@ -0,0 +1,439 @@
+// Phase 1.2 — Заменяет IV-6 stubs в §3, §6, §8 на полноценные интерактивы:
+// §3 Heat Conductor Bench — drag-стержни разных материалов
+// §6 Heat Mixer — drag двух ёмкостей с T1, T2 → T_итог
+// §8 Phase Diagram T(t) — анимированный график плавления льда
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_8_ch1.html');
+let h = fs.readFileSync(DST, 'utf8');
+
+// === Замена stub'а для одного § на реальный виджет + init ===
+function replaceStub(pid, widgetHtml, initFn) {
+ const stubMatch = h.match(new RegExp(
+ `\\/\\* IV6 — flagship интерактив[^\\n]*\\)[^\\n]*\\*\\/[\\s\\S]*?\\+'';\\s*\\n\\s*box\\.innerHTML = h \\+ secNavFor\\('${pid}'\\)`
+ ));
+ if (!stubMatch) { console.warn(`${pid}: stub not found`); return false; }
+ const stubText = stubMatch[0];
+ // Replace stub HTML portion (everything before the box.innerHTML line)
+ const lastBox = stubText.lastIndexOf("box.innerHTML");
+ const stubHtmlPart = stubText.slice(0, lastBox).trimEnd();
+ const newStubHtml = widgetHtml.trim() + '\n\n ';
+ h = h.replace(stubHtmlPart, newStubHtml);
+ // Add init call after wireReadBtn
+ h = h.replace(`wireReadBtn('${pid}');`, `wireReadBtn('${pid}');\n _init${pid.toUpperCase()}_iv6();`);
+ // Append init function after build_pN
+ const fnStart = h.indexOf(`function build_${pid}()`);
+ const fnEnd = h.indexOf('\n}\n', fnStart);
+ h = h.slice(0, fnEnd + 3) + '\n' + initFn.trim() + '\n' + h.slice(fnEnd + 3);
+ console.log(`${pid}: replaced stub with real IV-6`);
+ return true;
+}
+
+// =====================================================================
+// §3 — Heat Conductor Bench
+// =====================================================================
+const P3_HTML = `
+ /* IV6 — Heat Conductor Bench (Phase 1.2) */
+ h += '
'
+ +''
+ +'
Перетащи один из стержней (медь, дерево, стекло, серебро) на горелку. Цветовая карта покажет, как тепло движется по стержню. Чем больше λ — тем быстрее.
'
+ +'
'
+ +'
'
+ +'
Материал —
'
+ +'
λ — Вт/(м·К)
'
+ +'
T дальнего конца — °C
'
+ +'
'
+ +'
';
+`;
+
+const P3_INIT = `
+function _initP3_iv6(){
+ const sb = document.getElementById('p3-iv6-sandbox');
+ if (!sb || !window.P8Helpers || !window.P8Drag || !window.P8Anim) return;
+ const svg = P8Helpers.svg.create(560, 300);
+ svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
+ sb.appendChild(svg);
+ /* Горелка (drop zone) */
+ const burner = P8Helpers.svg.el('g', { transform: 'translate(80, 240)' });
+ burner.appendChild(P8Helpers.svg.el('rect', { x:-32, y:-8, width:64, height:32, rx:4, fill:'#475569' }));
+ burner.appendChild(P8Helpers.svg.el('rect', { x:-26, y:-22, width:52, height:14, rx:7, fill:'#dc2626' }));
+ burner.appendChild(P8Helpers.svg.el('text', { x:0, y:48, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#475569', 'text-anchor':'middle', text:'Горелка (drop)' }));
+ svg.appendChild(burner);
+ /* Палитра 4 стержней */
+ const rods = [
+ { name:'Медь', lam:400, color:'#b45309', x:200, y:50 },
+ { name:'Серебро', lam:430, color:'#9ca3af', x:300, y:50 },
+ { name:'Стекло', lam:0.8, color:'#bae6fd', x:400, y:50 },
+ { name:'Дерево', lam:0.15,color:'#a16207', x:500, y:50 }
+ ];
+ const rodEls = [];
+ rods.forEach((rod, i) => {
+ const g = P8Helpers.svg.el('g', { transform: 'translate('+rod.x+','+rod.y+')' });
+ /* Sections of rod, each will be colored by T gradient when active */
+ const segments = 12;
+ const segs = [];
+ for (let s = 0; s < segments; s++) {
+ const r = P8Helpers.svg.el('rect', {
+ x: -55 + s * (110/segments), y: -10, width: 110/segments, height: 20,
+ fill: rod.color, stroke: 'none'
+ });
+ g.appendChild(r);
+ segs.push(r);
+ }
+ /* Frame */
+ g.appendChild(P8Helpers.svg.el('rect', { x:-55, y:-10, width:110, height:20, rx:3, fill:'none', stroke:'#0f172a', 'stroke-width':1.5 }));
+ g.appendChild(P8Helpers.svg.el('text', { x:0, y:-18, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: rod.name }));
+ g.appendChild(P8Helpers.svg.el('text', { x:0, y:30, 'font-family':"'JetBrains Mono',monospace", 'font-size':9, 'font-weight':600, fill:'var(--p8-muted, #64748b)', 'text-anchor':'middle', text: 'λ='+rod.lam }));
+ svg.appendChild(g);
+ rodEls.push({ rod, g, segs, x: rod.x, y: rod.y });
+ });
+ /* Active sim state */
+ let activeIdx = -1;
+ let simLoop = null;
+ let simTime = 0;
+ const matEl = document.getElementById('p3-iv6-mat');
+ const lamEl = document.getElementById('p3-iv6-lam');
+ const tendEl = document.getElementById('p3-iv6-tend');
+ function resetColors(rodObj){
+ rodObj.segs.forEach(s => s.setAttribute('fill', rodObj.rod.color));
+ }
+ function startSim(rodObj){
+ if (simLoop) simLoop.stop();
+ simTime = 0;
+ /* λ нормализованный 0..1: log scale (15 -> 430) */
+ const lamNorm = Math.min(1, Math.log10(rodObj.rod.lam + 1) / Math.log10(500));
+ simLoop = P8Anim.raf((dt, t) => {
+ simTime += dt;
+ /* Diffusion-like: каждый сегмент i прогревается со скоростью lamNorm */
+ const speed = lamNorm * 0.8 + 0.04;
+ rodObj.segs.forEach((seg, i) => {
+ const pos = i / (rodObj.segs.length - 1);
+ const wave = speed * simTime;
+ const heat = Math.max(0, Math.min(1, wave - pos));
+ seg.setAttribute('fill', P8Helpers.thermal.tempColor(heat * 0.85 + 0.1));
+ });
+ /* T-end value */
+ const endHeat = Math.max(0, Math.min(1, speed * simTime - 0.95));
+ const tEnd = Math.round(20 + endHeat * 80);
+ if (tendEl) tendEl.textContent = tEnd;
+ if (simTime > 30) simLoop.stop();
+ });
+ simLoop.start();
+ if (matEl) matEl.textContent = rodObj.rod.name;
+ if (lamEl) lamEl.textContent = rodObj.rod.lam;
+ }
+ /* Attach drag to each rod */
+ rodEls.forEach((rodObj, i) => {
+ P8Drag.attach(rodObj.g, {
+ container: svg,
+ onMove: (ev, pos) => {
+ rodObj.x = pos.x;
+ rodObj.y = pos.y;
+ rodObj.g.setAttribute('transform', 'translate('+rodObj.x+','+rodObj.y+')');
+ },
+ onEnd: (ev, pos) => {
+ /* Check if dropped near burner */
+ if (Math.abs(pos.x - 80) < 70 && Math.abs(pos.y - 240) < 50) {
+ /* Snap to position above burner */
+ P8Anim.tween({
+ from: 0, to: 1, duration: 320, easing: 'cubicOut',
+ onUpdate: k => {
+ rodObj.x = pos.x + (80 + 55 - pos.x) * k;
+ rodObj.y = pos.y + (220 - pos.y) * k;
+ rodObj.g.setAttribute('transform', 'translate('+rodObj.x+','+rodObj.y+')');
+ }
+ });
+ /* Reset other rods to original */
+ rodEls.forEach((other, j) => {
+ if (j === i) return;
+ resetColors(other);
+ });
+ activeIdx = i;
+ startSim(rodObj);
+ if (window.addXp) addXp(10, 'p3-iv6-conduct');
+ }
+ }
+ });
+ });
+ /* Help text */
+ svg.appendChild(P8Helpers.svg.el('text', {
+ x: 280, y: 290,
+ 'font-family': "'Inter', sans-serif", 'font-size': 10,
+ fill: 'var(--p8-muted, #64748b)', 'text-anchor': 'middle',
+ text: 'Перетащи стержень на горелку • Чем выше λ — тем быстрее цвет дойдёт до конца'
+ }));
+}
+`;
+replaceStub('p3', P3_HTML, P3_INIT);
+
+// =====================================================================
+// §6 — Heat Mixer (Q = cmΔT)
+// =====================================================================
+const P6_HTML = `
+ /* IV6 — Heat Mixer (Phase 1.2) */
+ h += ''
+ +''
+ +'
Перетащи ёмкости друг к другу — они смешаются. Масса и начальная температура каждой — на скрубберах ниже. Конечная температура считается по уравнению теплового баланса $c m_1 (T_1 - T) = c m_2 (T - T_2)$.
'
+ +'
'
+ +'
'
+ +'
'
+ +'
T_итог — °C
'
+ +'
Смешать '
+ +'
Сброс '
+ +'
'
+ +'
';
+`;
+
+const P6_INIT = `
+function _initP6_iv6(){
+ const sb = document.getElementById('p6-iv6-sandbox');
+ if (!sb || !window.P8Helpers || !window.P8Anim) return;
+ const svg = P8Helpers.svg.create(560, 240);
+ svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
+ sb.appendChild(svg);
+ /* Vessel positions (will animate to centre on mix) */
+ const v1 = { x: 140, y: 130, m: 0.5, T: 80, color: '#fb923c' };
+ const v2 = { x: 420, y: 130, m: 1.0, T: 20, color: '#7dd3fc' };
+ const finalState = { active: false, T: 50, fillFraction: 0.5 };
+ function drawVessel(x, y, m, T, color){
+ const g = P8Helpers.svg.el('g', { transform: 'translate('+x+','+y+')' });
+ const h = 30 + m * 50;
+ const w = 70;
+ /* Glass */
+ g.appendChild(P8Helpers.svg.el('rect', { x:-w/2, y:-h, width:w, height:h, rx:6, fill:'rgba(255,255,255,.6)', stroke:'#0f172a', 'stroke-width':1.5 }));
+ /* Liquid */
+ g.appendChild(P8Helpers.svg.el('rect', { x:-w/2+3, y:-h+5, width:w-6, height:h-8, rx:4, fill: P8Helpers.thermal.tempColor(T/100) }));
+ /* Label */
+ g.appendChild(P8Helpers.svg.el('text', { x:0, y:18, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'm='+m.toFixed(1)+' кг' }));
+ g.appendChild(P8Helpers.svg.el('text', { x:0, y:32, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'T='+Math.round(T)+'°C' }));
+ return g;
+ }
+ let v1G = drawVessel(v1.x, v1.y, v1.m, v1.T, v1.color);
+ let v2G = drawVessel(v2.x, v2.y, v2.m, v2.T, v2.color);
+ svg.appendChild(v1G); svg.appendChild(v2G);
+ function redraw(){
+ svg.innerHTML = '';
+ if (!finalState.active) {
+ v1G = drawVessel(v1.x, v1.y, v1.m, v1.T, v1.color);
+ v2G = drawVessel(v2.x, v2.y, v2.m, v2.T, v2.color);
+ svg.appendChild(v1G); svg.appendChild(v2G);
+ } else {
+ /* Single combined vessel */
+ const cv = drawVessel(280, 130, v1.m + v2.m, finalState.T, P8Helpers.thermal.tempColor(finalState.T/100));
+ svg.appendChild(cv);
+ /* Result label */
+ svg.appendChild(P8Helpers.svg.el('text', { x:280, y:60, 'font-family':"'Unbounded',sans-serif", 'font-size':14, 'font-weight':800, fill:'var(--th-mid,#f97316)', 'text-anchor':'middle', text: 'T_итог = '+Math.round(finalState.T)+' °C' }));
+ }
+ }
+ /* Hook up scrubbers */
+ function bindScrub(inputId, valId, obj, prop){
+ const input = document.getElementById(inputId);
+ const lab = document.getElementById(valId);
+ if (!input || !lab) return;
+ input.addEventListener('input', () => {
+ const v = parseFloat(input.value);
+ obj[prop] = v;
+ lab.textContent = v.toFixed(prop === 'm' ? 1 : 0);
+ if (finalState.active) {
+ finalState.active = false;
+ document.getElementById('p6-iv6-tf').textContent = '—';
+ }
+ redraw();
+ });
+ }
+ bindScrub('p6-iv6-m1', 'p6-iv6-m1-val', v1, 'm');
+ bindScrub('p6-iv6-t1', 'p6-iv6-t1-val', v1, 'T');
+ bindScrub('p6-iv6-m2', 'p6-iv6-m2-val', v2, 'm');
+ bindScrub('p6-iv6-t2', 'p6-iv6-t2-val', v2, 'T');
+ /* Mix button */
+ document.getElementById('p6-iv6-mix').onclick = () => {
+ const T = (v1.m * v1.T + v2.m * v2.T) / (v1.m + v2.m);
+ finalState.active = true;
+ finalState.T = T;
+ P8Anim.tween({
+ from: v1.T, to: T, duration: 1200, easing: 'cubicInOut',
+ onUpdate: t => {
+ finalState.T = t;
+ redraw();
+ document.getElementById('p6-iv6-tf').textContent = Math.round(t);
+ }
+ });
+ if (window.addXp) addXp(10, 'p6-iv6-mix');
+ };
+ document.getElementById('p6-iv6-reset').onclick = () => {
+ finalState.active = false;
+ document.getElementById('p6-iv6-tf').textContent = '—';
+ redraw();
+ };
+ redraw();
+}
+`;
+replaceStub('p6', P6_HTML, P6_INIT);
+
+// =====================================================================
+// §8 — Phase Diagram T(t)
+// =====================================================================
+const P8_HTML = `
+ /* IV6 — Phase Diagram T(t) (Phase 1.2) */
+ h += ''
+ +''
+ +'
Запусти нагрев льда и наблюдай за графиком T(t). При плавлении энергия идёт на разрушение кристаллической решётки — T держится постоянной (плато при 0°C). Двигай ползунок мощности нагревателя — крутизна меняется.
'
+ +'
'
+ +'
'
+ +'
Мощность 500 Вт
'
+ +'
Фаза лёд
'
+ +'
T -20 °C
'
+ +'
Старт '
+ +'
Сброс '
+ +'
'
+ +'
';
+`;
+
+const P8_INIT = `
+function _initP8_iv6(){
+ const sb = document.getElementById('p8-iv6-sandbox');
+ if (!sb || !window.P8Helpers || !window.P8Anim) return;
+ const W = 560, H = 280;
+ const svg = P8Helpers.svg.create(W, H);
+ svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
+ sb.appendChild(svg);
+ /* Sim state */
+ const m = 0.5; // kg ice
+ const c_ice = 2100;
+ const c_water = 4200;
+ const lambda = 330000;
+ const r_vap = 2300000;
+ let power = 500;
+ let energyAccumulated = 0;
+ let running = false;
+ let raf = null;
+ let points = [{ t: 0, T: -20 }];
+ /* Axes */
+ const pad = { l: 50, r: 18, t: 22, b: 32 };
+ const plotW = W - pad.l - pad.r;
+ const plotH = H - pad.t - pad.b;
+ /* Background */
+ svg.appendChild(P8Helpers.svg.el('rect', { x: pad.l, y: pad.t, width: plotW, height: plotH, fill: '#fafafa', stroke: '#e5e7eb' }));
+ /* Y axis: -20 to 120 °C */
+ const yMin = -20, yMax = 120;
+ function yToPx(T) { return pad.t + plotH * (1 - (T - yMin) / (yMax - yMin)); }
+ function tToPx(t) { return pad.l + plotW * Math.min(1, t / 300); } // 300 s scale
+ /* Y ticks */
+ [-20, 0, 20, 40, 60, 80, 100, 120].forEach(t => {
+ const y = yToPx(t);
+ svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: y, x2: pad.l + plotW, y2: y, stroke: '#e5e7eb' }));
+ svg.appendChild(P8Helpers.svg.el('text', { x: pad.l - 6, y: y + 3, 'font-family':"'JetBrains Mono',monospace", 'font-size': 10, fill: 'var(--p8-muted,#64748b)', 'text-anchor':'end', text: t+'°' }));
+ });
+ /* Phase regions overlays (transparent) */
+ const phaseRegions = [
+ { from: -20, to: 0, fill: '#bfdbfe', name: 'лёд' },
+ { from: 0, to: 100, fill: '#7dd3fc', name: 'вода' },
+ { from: 100, to: 120, fill: '#fde68a', name: 'пар' }
+ ];
+ phaseRegions.forEach(r => {
+ const y1 = yToPx(r.from), y2 = yToPx(r.to);
+ svg.appendChild(P8Helpers.svg.el('rect', { x: pad.l, y: y2, width: plotW, height: y1 - y2, fill: r.fill, opacity: 0.18 }));
+ svg.appendChild(P8Helpers.svg.el('text', { x: pad.l + plotW - 6, y: (y1 + y2) / 2 + 3, 'font-family':"'Inter',sans-serif", 'font-size': 10, 'font-weight': 700, fill: 'var(--p8-text)', 'text-anchor': 'end', text: r.name }));
+ });
+ /* Phase lines (0 and 100) */
+ svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: yToPx(0), x2: pad.l + plotW, y2: yToPx(0), stroke: '#0f172a', 'stroke-width': 1, 'stroke-dasharray': '3 3' }));
+ svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: yToPx(100), x2: pad.l + plotW, y2: yToPx(100), stroke: '#0f172a', 'stroke-width': 1, 'stroke-dasharray': '3 3' }));
+ /* X axis */
+ svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: pad.t + plotH, x2: pad.l + plotW, y2: pad.t + plotH, stroke: '#0f172a' }));
+ svg.appendChild(P8Helpers.svg.el('text', { x: pad.l + plotW / 2, y: H - 6, 'font-family':"'Inter',sans-serif", 'font-size': 11, 'font-weight': 700, fill: 'var(--p8-text)', 'text-anchor': 'middle', text: 'Время, с' }));
+ /* Curve path (will be updated) */
+ const path = P8Helpers.svg.el('path', { d: '', fill: 'none', stroke: 'var(--th-mid, #f97316)', 'stroke-width': 3, 'stroke-linejoin': 'round', 'stroke-linecap': 'round' });
+ svg.appendChild(path);
+ function updatePath(){
+ if (!points.length) return;
+ const d = points.map((p, i) => (i === 0 ? 'M' : 'L') + tToPx(p.t).toFixed(1) + ',' + yToPx(p.T).toFixed(1)).join(' ');
+ path.setAttribute('d', d);
+ }
+ function currentT(){ return points[points.length-1].T; }
+ function currentPhase(T){
+ if (T < 0) return 'лёд';
+ if (T < 100) return T === 0 ? 'плавление' : 'вода';
+ if (T === 100) return 'кипение';
+ return 'пар';
+ }
+ function tick(dt){
+ if (!running) return;
+ const energy = power * dt; // J
+ let T = currentT();
+ let newT = T;
+ if (T < 0) {
+ /* heating ice */
+ const dT = energy / (c_ice * m);
+ newT = T + dT;
+ if (newT > 0) newT = 0;
+ } else if (T < 0.001 && energyAccumulated < lambda * m) {
+ /* phase transition (melting) */
+ energyAccumulated += energy;
+ newT = 0;
+ if (energyAccumulated >= lambda * m) {
+ newT = 0.001;
+ }
+ } else if (T < 100) {
+ /* heating water */
+ const dT = energy / (c_water * m);
+ newT = T + dT;
+ if (newT > 100) newT = 100;
+ } else if (T < 100.001 && energyAccumulated < (lambda + r_vap) * m) {
+ /* phase transition (boiling) */
+ energyAccumulated += energy;
+ newT = 100;
+ if (energyAccumulated >= (lambda + r_vap) * m) {
+ newT = 100.001;
+ }
+ } else if (T < 120) {
+ const dT = energy / (c_water * m); // simplified for steam
+ newT = T + dT;
+ if (newT > 120) newT = 120;
+ } else {
+ running = false;
+ }
+ const lastP = points[points.length-1];
+ points.push({ t: lastP.t + dt, T: newT });
+ if (points.length > 600) points.shift();
+ updatePath();
+ document.getElementById('p8-iv6-temp').textContent = Math.round(newT);
+ document.getElementById('p8-iv6-phase').textContent = currentPhase(newT);
+ if (lastP.t > 300) running = false;
+ }
+ raf = P8Anim.raf(dt => tick(Math.min(dt * 4, 0.5))); // accelerate 4x for demo
+ /* Bind controls */
+ const pwrInp = document.getElementById('p8-iv6-pwr');
+ const pwrLab = document.getElementById('p8-iv6-pwr-val');
+ pwrInp.oninput = () => { power = +pwrInp.value; pwrLab.textContent = power; };
+ document.getElementById('p8-iv6-play').onclick = () => {
+ if (!running) { running = true; raf.start(); if (window.addXp) addXp(10, 'p8-iv6-melt'); }
+ };
+ document.getElementById('p8-iv6-reset').onclick = () => {
+ running = false; raf.stop();
+ energyAccumulated = 0;
+ points = [{ t: 0, T: -20 }];
+ updatePath();
+ document.getElementById('p8-iv6-temp').textContent = '-20';
+ document.getElementById('p8-iv6-phase').textContent = 'лёд';
+ };
+ updatePath();
+}
+`;
+replaceStub('p8', P8_HTML, P8_INIT);
+
+fs.writeFileSync(DST, h);
+console.log('ch1 size:', h.length);
+
+const scripts = [...h.matchAll(/