feat(geom11 ch1 wave2): §2 «Цилиндр» + 3D-конструктор + сечения

This commit is contained in:
Maxim Dolgolyov
2026-05-29 14:12:55 +03:00
parent b6bb1d9f48
commit 6acdb72b39
+420 -3
View File
@@ -392,7 +392,7 @@ function buildParaSelector(){
}
const BUILT=new Set();
const BUILDERS = { p1:()=>buildP1(), p2:()=>buildStub('p2'), final1:()=>buildStub('final1') };
const BUILDERS = { p1:()=>buildP1(), p2:()=>buildP2(), final1:()=>buildStub('final1') };
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
function goTo(id){
STATE.current=id; ensureBuilt(id);
@@ -408,13 +408,13 @@ function goTo(id){
const SIDEBARS = {
p1:{title:"Шпаргалка § 1", rows:[["Тема","Призма"],["Прямая","$S_{бок}=P_{осн}\\\\cdot h$"],["Наклонная","$S_{бок}=P_{пер}\\\\cdot l$"],["Объём","$V=S_{осн}\\\\cdot h$"],["Диагональ пар.","$d=\\\\sqrt{a^2+b^2+c^2}$"]]},
p2:{title:"Шпаргалка § 2", rows:[["Тема", "Цилиндр"],["Формула","$S_{бок}=2\\\\pi Rh$, $V=\\\\pi R^2h$"]]},
p2:{title:"Шпаргалка § 2", rows:[["Тема", "Цилиндр"],["$S_{осн}$","$\\\\pi R^2$"],["$S_{бок}$","$2\\\\pi Rh$"],["$S_{полн}$","$2\\\\pi R(R+h)$"],["$V$","$\\\\pi R^2 h$"],["Развёртка","прямоуг. $2\\\\pi R \\\\times h$"],["Осевое сеч.","прямоуг. $2R \\\\times h$"],["Наклон. сеч.","эллипс, $a=R/\\\\cos\\\\alpha$, $b=R$"]]},
final1:{title:"Финал раздела 1", rows:[["§ 1–§ 2","теория раздела 1"],["Награда","+50 XP"]]}
};
const TIPS=[
{sec:'p1',html:"§ 1 «Призма» — крути 3D-модель в интерактиве 1, проверь формулы в калькуляторе. Главное: $V=S_{осн}\\\\cdot h$, $S_{бок}=P_{осн}\\\\cdot h$ (для прямой)."},
{sec:'p2',html:"§ 2 «Цилиндр» — содержание в разработке. $S_{бок}=2\\\\\\\\pi Rh$, $V=\\\\\\\\pi R^2h$"},
{sec:'p2',html:"§ 2 «Цилиндр» — крути 3D-модель в интерактиве 1, разбирай сечения в IV2 (круг/прямоугольник/эллипс). Главное: $S_{бок}=2\\\\\\\\pi Rh$, $V=\\\\\\\\pi R^2 h$, развёртка боковой поверхности — прямоугольник $2\\\\\\\\pi R \\\\\\\\times h$."},
{sec:'final1',html:"Финал раздела 1 — интегрированные задачи по разделу."}
];
@@ -989,6 +989,423 @@ function buildP1(){
wireReadBtn('p1');
}
/* ===== §2 «Цилиндр» — Wave 2 ===== */
function buildP2(){
const box = document.getElementById('p2-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Определение и элементы', '§ 2.1',
'<p><b>Цилиндр</b> (точнее — <b>прямой круговой цилиндр</b>) — тело, полученное вращением прямоугольника вокруг одной из его сторон.</p>'
+ '<p><b>Элементы цилиндра:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Основания</b> — два равных круга радиуса $R$, лежащих в параллельных плоскостях.</li>'
+ '<li><b>Ось</b> — отрезок, соединяющий центры оснований.</li>'
+ '<li><b>Образующие</b> — отрезки длины $h$, параллельные оси и соединяющие соответствующие точки окружностей оснований. Все образующие равны и $\\perp$ основаниям.</li>'
+ '<li><b>Боковая поверхность</b> — объединение всех образующих.</li>'
+ '<li><b>Высота</b> $h$ — расстояние между основаниями (= длина образующей).</li>'
+ '<li><b>Радиус</b> $R$ — радиус основания.</li>'
+ '</ul>'
+ '<p>Цилиндр можно рассматривать как <b>предельный случай правильной $n$-угольной призмы</b>: если число сторон основания $n \\to \\infty$, призма «выпрямляется» в цилиндр, а её боковая поверхность — в боковую поверхность цилиндра.</p>');
html += makeCard('rule', 'Площадь поверхности и объём', '§ 2.2',
'<p>Основные формулы цилиндра радиуса $R$ и высоты $h$:</p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{осн}=\\pi R^2 \\qquad S_{бок}=2\\pi R h$$</p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{полн}=2\\pi R(R+h) \\qquad V=\\pi R^2 h$$</p>'
+ '<p><b>Развёртка боковой поверхности</b> — прямоугольник со сторонами $2\\pi R$ (длина окружности основания) и $h$ (высота). Отсюда $S_{бок}=2\\pi R \\cdot h$.</p>'
+ '<details class="spoiler"><summary>Пример: $R=3$, $h=5$</summary><div class="spoiler-body">'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>$S_{осн}=9\\pi \\approx 28{,}3$</li>'
+ '<li>$S_{бок}=2\\pi\\cdot 3\\cdot 5=30\\pi \\approx 94{,}2$</li>'
+ '<li>$S_{полн}=2\\pi\\cdot 3(3+5)=48\\pi \\approx 150{,}8$</li>'
+ '<li>$V=\\pi\\cdot 9\\cdot 5=45\\pi \\approx 141{,}4$</li>'
+ '</ul>'
+ '</div></details>');
html += makeCard('example', 'Сечения цилиндра', '§ 2.3',
'<p>Какие фигуры получаются в сечении цилиндра плоскостью:</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Перпендикулярное оси</b> (= параллельное основанию): <b>круг</b> радиуса $R$.</li>'
+ '<li><b>Осевое</b> (через ось): <b>прямоугольник</b> со сторонами $2R$ и $h$.</li>'
+ '<li><b>Параллельное оси, не осевое</b> (на расстоянии $d<R$ от оси): <b>прямоугольник</b> со сторонами $2\\sqrt{R^2-d^2}$ и $h$.</li>'
+ '<li><b>Наклонное</b> (плоскость пересекает все образующие, угол с основанием $\\alpha$): <b>эллипс</b> с полуосями $b=R$ (малая) и $a=R/\\cos\\alpha$ (большая).</li>'
+ '</ul>'
+ '<p><b>Касательная плоскость</b> к цилиндру проходит через одну образующую и не пересекает цилиндр в других точках. Через любую образующую можно провести <b>ровно одну</b> касательную плоскость; она перпендикулярна осевому сечению, проходящему через эту образующую.</p>'
+ '<details class="spoiler"><summary>Почему наклонное сечение — эллипс?</summary><div class="spoiler-body">'
+ '<p>Пусть плоскость $\\pi$ наклонена к основанию под углом $\\alpha$. Спроецируем сечение на плоскость основания вдоль оси цилиндра. Проекция — круг радиуса $R$ (граница цилиндра, видимая сверху).</p>'
+ '<p>В направлении линии пересечения с основанием размеры сохраняются ($b=R$), а в перпендикулярном направлении сечение «растянуто» в $1/\\cos\\alpha$ раз — получается $a=R/\\cos\\alpha$. Это и есть эллипс с полуосями $R$ и $R/\\cos\\alpha$.</p>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — 3D-конструктор === */
html += '<div class="wg" id="p2-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · конструктор</span><div class="wg-title">Цилиндр $R \\times h$</div></div>'
+ '<div class="wg-help">Меняй радиус $R$ и высоту $h$. Вращай мышью или выбирай вид. После <b>4 разных конфигураций</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>$R$ (радиус):<b id="p2-iv1-R-v">2.0</b><input type="range" id="p2-iv1-R" min="1" max="4" step="0.1" value="2"></label>'
+ '<label>$h$ (высота):<b id="p2-iv1-h-v">3.0</b><input type="range" id="p2-iv1-h" min="1" max="8" step="0.1" value="3"></label>'
+ '</div>'
+ '<div class="g3d-tools">'
+ '<button class="btn" data-view="iso">Изо</button>'
+ '<button class="btn" data-view="front">Спереди</button>'
+ '<button class="btn" data-view="top">Сверху</button>'
+ '<button class="btn" data-view="side">Сбоку</button>'
+ '</div>'
+ '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:8px;text-align:center"><svg id="p2-iv1-svg" viewBox="0 0 480 400" width="100%" style="max-width:480px;height:auto"></svg></div>'
+ '<div class="score-display" style="margin-top:10px;flex-wrap:wrap">'
+ '<span>$P_{осн}=$<b id="p2-iv1-P">—</b></span>'
+ '<span>$S_{осн}=$<b id="p2-iv1-Sb">—</b></span>'
+ '<span>$S_{бок}=$<b id="p2-iv1-Ss">—</b></span>'
+ '<span>$S_{полн}=$<b id="p2-iv1-St">—</b></span>'
+ '<span>$V=$<b id="p2-iv1-V">—</b></span>'
+ '</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Конфигураций изучено: <b id="p2-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Сечения === */
html += '<div class="wg" id="p2-iv2">'
+ '<div class="wg-header"><span class="wg-badge">визуализатор</span><div class="wg-title">4 типа сечений цилиндра</div></div>'
+ '<div class="wg-help">Выбери тип сечения и крути ползунки. После просмотра всех <b>4 типов</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>Тип сечения:<b id="p2-iv2-t-v">1</b><input type="range" id="p2-iv2-t" min="1" max="4" step="1" value="1"></label>'
+ '<label>$R$:<b id="p2-iv2-R-v">2.0</b><input type="range" id="p2-iv2-R" min="1" max="4" step="0.1" value="2"></label>'
+ '<label>$h$:<b id="p2-iv2-h-v">3.0</b><input type="range" id="p2-iv2-h" min="1" max="8" step="0.1" value="3"></label>'
+ '<label id="p2-iv2-d-wrap" style="display:none">$d$:<b id="p2-iv2-d-v">0.5</b><input type="range" id="p2-iv2-d" min="0" max="3.9" step="0.1" value="0.5"></label>'
+ '<label id="p2-iv2-a-wrap" style="display:none">$\\alpha$ (°):<b id="p2-iv2-a-v">30</b><input type="range" id="p2-iv2-a" min="0" max="80" step="1" value="30"></label>'
+ '</div>'
+ '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:8px;text-align:center"><svg id="p2-iv2-svg" viewBox="0 0 380 280" width="100%" style="max-width:380px;height:auto"></svg></div>'
+ '<div id="p2-iv2-info" style="margin-top:10px;padding:10px 14px;background:var(--card);border:1px solid var(--border);border-radius:9px;font-size:.92rem;line-height:1.7"></div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Типов сечений просмотрено: <b id="p2-iv2-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Квикфайр === */
html += '<div class="wg" id="p2-iv3">'
+ '<div class="wg-header"><span class="wg-badge">квикфайр · 6 вопросов</span><div class="wg-title">Какая фигура в сечении?</div></div>'
+ '<div class="wg-help">Прочитай описание секущей плоскости и выбери фигуру. После 6 верных — +15 XP.</div>'
+ '<div id="p2-iv3-area"></div>'
+ '<div class="score-display" style="margin-top:10px">Верно: <b id="p2-iv3-score">0</b> / 6</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 4 — Тренажёр === */
html += '<div class="wg" id="p2-iv4">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 6 задач</span><div class="wg-title">$V$ и $S$ цилиндра</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$. Используй $\\pi \\approx 3{,}14$.</div>'
+ '<div id="p2-iv4-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p2-iv4-score">0</b> / 6</div>'
+ '</div>';
html += secNavFor('p2');
html += readButton('p2');
box.innerHTML = html;
renderMath(box);
/* ===== JS-логика интерактивов ===== */
/* IV1 — 3D конструктор */
(function(){
if(!window.G3D) return;
const svg = document.getElementById('p2-iv1-svg');
const elR = document.getElementById('p2-iv1-R');
const elH = document.getElementById('p2-iv1-h');
const vR = document.getElementById('p2-iv1-R-v');
const vH = document.getElementById('p2-iv1-h-v');
const oP = document.getElementById('p2-iv1-P');
const oSb = document.getElementById('p2-iv1-Sb');
const oSs = document.getElementById('p2-iv1-Ss');
const oSt = document.getElementById('p2-iv1-St');
const oV = document.getElementById('p2-iv1-V');
const oCnt = document.getElementById('p2-iv1-cnt');
if(!svg) return;
const scene = G3D.createScene({W:480, H:400, scale:50, camDist:8, rotX:-0.35, rotY:0.7});
const seen = new Set();
let xpGiven = false;
function draw(){
const R = +elR.value, h = +elH.value;
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
const mesh = G3D.cylinderMesh(R, h, 32);
const M = G3D.buildRotMatrix(scene);
svg.innerHTML = G3D.renderMesh(mesh, M, scene, {
fillBase:'rgba(252,231,243,.55)',
fillSide:'rgba(219,234,254,.55)',
strokeVisible:'#0f172a',
strokeHidden:'#94a3b8'
});
const P = 2*Math.PI*R;
const Sb = Math.PI*R*R;
const Ss = 2*Math.PI*R*h;
const St = 2*Math.PI*R*(R+h);
const V = Math.PI*R*R*h;
oP.textContent = P.toFixed(2);
oSb.textContent = Sb.toFixed(2);
oSs.textContent = Ss.toFixed(2);
oSt.textContent = St.toFixed(2);
oV.textContent = V.toFixed(2);
const key = R.toFixed(1)+'|'+h.toFixed(1);
seen.add(key);
oCnt.textContent = Math.min(seen.size, 4);
if(seen.size >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p2-iv1');
bumpProgress('p2', 15);
const note = document.createElement('div');
note.className = 'feedback ok';
note.innerHTML = '&#10003; +10 XP за изучение 4 разных цилиндров!';
note.style.cssText = 'display:block;margin-top:8px';
const host = document.getElementById('p2-iv1');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
G3D.attachOrbit(svg, scene, draw);
[elR, elH].forEach(function(el){ el.addEventListener('input', draw); });
document.querySelectorAll('#p2-iv1 .g3d-tools .btn').forEach(function(b){
b.addEventListener('click', function(){ G3D.presetView(scene, b.dataset.view, draw); });
});
})();
/* IV2 — Сечения */
(function(){
const svg = document.getElementById('p2-iv2-svg');
const info = document.getElementById('p2-iv2-info');
const elT = document.getElementById('p2-iv2-t');
const elR = document.getElementById('p2-iv2-R');
const elH = document.getElementById('p2-iv2-h');
const elD = document.getElementById('p2-iv2-d');
const elA = document.getElementById('p2-iv2-a');
const vT = document.getElementById('p2-iv2-t-v');
const vR = document.getElementById('p2-iv2-R-v');
const vH = document.getElementById('p2-iv2-h-v');
const vD = document.getElementById('p2-iv2-d-v');
const vA = document.getElementById('p2-iv2-a-v');
const wrapD = document.getElementById('p2-iv2-d-wrap');
const wrapA = document.getElementById('p2-iv2-a-wrap');
const oCnt = document.getElementById('p2-iv2-cnt');
if(!svg) return;
const TYPE_NAMES = {
1:'Параллельное основанию',
2:'Осевое',
3:'Параллельное оси (на расстоянии $d$ от оси)',
4:'Наклонное (под углом $\\alpha$ к основанию)'
};
const seenTypes = new Set();
let xpGiven = false;
function draw(){
const t = +elT.value;
const R = +elR.value, h = +elH.value;
let d = +elD.value, a = +elA.value;
vT.textContent = t;
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
vD.textContent = d.toFixed(1);
vA.textContent = a;
/* Динамически ограничиваем d ≤ R - 0.1 */
elD.max = (R - 0.1).toFixed(1);
if(d > +elD.max){ d = +elD.max; elD.value = d; vD.textContent = d.toFixed(1); }
wrapD.style.display = (t === 3) ? '' : 'none';
wrapA.style.display = (t === 4) ? '' : 'none';
/* Масштаб: используем единичный масштаб, чтобы вписать в 380x280 */
const W = 380, H = 280;
const cx = W/2, cy = H/2;
let body = '';
let infoHtml = '<p><b>Тип '+t+': '+TYPE_NAMES[t]+'</b></p>';
if(t === 1){
/* Круг радиуса R */
const sc = Math.min((W-60)/(2*R), (H-60)/(2*R));
const rPx = R*sc;
body += '<circle cx="'+cx+'" cy="'+cy+'" r="'+rPx.toFixed(2)+'" fill="rgba(252,231,243,.55)" stroke="#db2777" stroke-width="2"/>';
/* радиус */
body += '<line x1="'+cx+'" y1="'+cy+'" x2="'+(cx+rPx).toFixed(2)+'" y2="'+cy+'" stroke="#0f172a" stroke-width="1.4" stroke-dasharray="4 3"/>';
body += '<circle cx="'+cx+'" cy="'+cy+'" r="3" fill="#0f172a"/>';
body += '<text x="'+(cx + rPx/2).toFixed(2)+'" y="'+(cy-6)+'" font-size="13" fill="#0f172a" text-anchor="middle" font-weight="700">R = '+R.toFixed(1)+'</text>';
infoHtml += '<p>Фигура — <b>круг</b> радиуса $R='+R.toFixed(1)+'$.</p>';
infoHtml += '<p>Площадь сечения: $S=\\pi R^2 \\approx '+(Math.PI*R*R).toFixed(2)+'$.</p>';
} else if(t === 2){
/* Осевое — прямоугольник 2R × h */
const sc = Math.min((W-60)/(2*R), (H-60)/h);
const wPx = 2*R*sc, hPx = h*sc;
const x0 = cx - wPx/2, y0 = cy - hPx/2;
body += '<rect x="'+x0.toFixed(2)+'" y="'+y0.toFixed(2)+'" width="'+wPx.toFixed(2)+'" height="'+hPx.toFixed(2)+'" fill="rgba(219,234,254,.55)" stroke="#0891b2" stroke-width="2"/>';
/* ось */
body += '<line x1="'+cx+'" y1="'+y0.toFixed(2)+'" x2="'+cx+'" y2="'+(y0+hPx).toFixed(2)+'" stroke="#db2777" stroke-width="1.4" stroke-dasharray="4 3"/>';
body += '<text x="'+cx+'" y="'+(y0-6)+'" font-size="12" fill="#db2777" text-anchor="middle" font-weight="700">ось</text>';
body += '<text x="'+cx+'" y="'+(y0+hPx+16).toFixed(2)+'" font-size="13" fill="#0f172a" text-anchor="middle" font-weight="700">2R = '+(2*R).toFixed(1)+'</text>';
body += '<text x="'+(x0-6).toFixed(2)+'" y="'+(cy+4)+'" font-size="13" fill="#0f172a" text-anchor="end" font-weight="700">h = '+h.toFixed(1)+'</text>';
infoHtml += '<p>Фигура — <b>прямоугольник</b> со сторонами $2R='+(2*R).toFixed(1)+'$ и $h='+h.toFixed(1)+'$.</p>';
infoHtml += '<p>Площадь сечения: $S=2Rh = '+(2*R*h).toFixed(2)+'$.</p>';
} else if(t === 3){
/* Параллельное оси, не осевое — прямоугольник со сторонами 2√(R²-d²) и h */
const w = 2*Math.sqrt(Math.max(0, R*R - d*d));
const sc = Math.min((W-60)/(2*R), (H-60)/h);
const wPx = w*sc, hPx = h*sc;
/* нарисуем «пунктиром» полный осевой прямоугольник 2R×h для сравнения и закрасим нашу полосу */
const fullW = 2*R*sc;
const fx0 = cx - fullW/2, fy0 = cy - hPx/2;
body += '<rect x="'+fx0.toFixed(2)+'" y="'+fy0.toFixed(2)+'" width="'+fullW.toFixed(2)+'" height="'+hPx.toFixed(2)+'" fill="none" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 3"/>';
const x0 = cx - wPx/2, y0 = cy - hPx/2;
body += '<rect x="'+x0.toFixed(2)+'" y="'+y0.toFixed(2)+'" width="'+wPx.toFixed(2)+'" height="'+hPx.toFixed(2)+'" fill="rgba(219,234,254,.55)" stroke="#0891b2" stroke-width="2"/>';
body += '<text x="'+cx+'" y="'+(y0+hPx+16).toFixed(2)+'" font-size="12" fill="#0f172a" text-anchor="middle" font-weight="700">2√(R²−d²) = '+w.toFixed(2)+'</text>';
body += '<text x="'+(x0-6).toFixed(2)+'" y="'+(cy+4)+'" font-size="13" fill="#0f172a" text-anchor="end" font-weight="700">h = '+h.toFixed(1)+'</text>';
infoHtml += '<p>Фигура — <b>прямоугольник</b> со сторонами $2\\sqrt{R^2-d^2}\\approx '+w.toFixed(2)+'$ и $h='+h.toFixed(1)+'$.</p>';
infoHtml += '<p>При $d='+d.toFixed(1)+'$, $R='+R.toFixed(1)+'$: ширина = $2\\sqrt{'+(R*R).toFixed(2)+'-'+(d*d).toFixed(2)+'}='+w.toFixed(2)+'$.</p>';
infoHtml += '<p>Площадь сечения: $S='+w.toFixed(2)+' \\cdot '+h.toFixed(1)+' = '+(w*h).toFixed(2)+'$.</p>';
} else {
/* Наклонное — эллипс с полуосями R и R/cos(α) */
const alpha = a * Math.PI/180;
const aMaj = R / Math.cos(alpha); /* большая полуось */
const sc = Math.min((W-60)/(2*aMaj), (H-60)/(2*R));
const aPx = aMaj*sc, bPx = R*sc;
body += '<ellipse cx="'+cx+'" cy="'+cy+'" rx="'+aPx.toFixed(2)+'" ry="'+bPx.toFixed(2)+'" fill="rgba(254,243,199,.55)" stroke="#d97706" stroke-width="2"/>';
/* большая ось */
body += '<line x1="'+(cx-aPx).toFixed(2)+'" y1="'+cy+'" x2="'+(cx+aPx).toFixed(2)+'" y2="'+cy+'" stroke="#0f172a" stroke-width="1.2" stroke-dasharray="4 3"/>';
/* малая ось */
body += '<line x1="'+cx+'" y1="'+(cy-bPx).toFixed(2)+'" x2="'+cx+'" y2="'+(cy+bPx).toFixed(2)+'" stroke="#0f172a" stroke-width="1.2" stroke-dasharray="4 3"/>';
body += '<text x="'+(cx + aPx/2).toFixed(2)+'" y="'+(cy-6)+'" font-size="12" fill="#0f172a" text-anchor="middle" font-weight="700">a = '+aMaj.toFixed(2)+'</text>';
body += '<text x="'+(cx+6)+'" y="'+(cy - bPx/2).toFixed(2)+'" font-size="12" fill="#0f172a" font-weight="700">b = '+R.toFixed(1)+'</text>';
infoHtml += '<p>Фигура — <b>эллипс</b> с полуосями $b=R='+R.toFixed(1)+'$ и $a=R/\\cos\\alpha = '+R.toFixed(1)+'/\\cos '+a+'° \\approx '+aMaj.toFixed(2)+'$.</p>';
infoHtml += '<p>Площадь эллипса: $S=\\pi ab \\approx '+(Math.PI*R*aMaj).toFixed(2)+'$.</p>';
}
svg.innerHTML = body;
info.innerHTML = infoHtml;
try{ renderMath(info); }catch(e){}
seenTypes.add(t);
oCnt.textContent = seenTypes.size;
if(seenTypes.size >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p2-iv2');
bumpProgress('p2', 15);
const note = document.createElement('div');
note.className = 'feedback ok';
note.innerHTML = '&#10003; +10 XP за разбор всех 4 типов сечений!';
note.style.cssText = 'display:block;margin-top:8px';
const host = document.getElementById('p2-iv2');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
[elT, elR, elH, elD, elA].forEach(function(el){ el.addEventListener('input', draw); });
})();
/* IV3 — Квикфайр */
(function(){
const tasks = [
{ q:'Сечение цилиндра плоскостью, <b>параллельной основанию</b>:', a:'circle' },
{ q:'Сечение цилиндра <b>осевой</b> плоскостью:', a:'rect' },
{ q:'Сечение цилиндра плоскостью, <b>перпендикулярной оси</b>:', a:'circle' },
{ q:'Сечение цилиндра плоскостью, <b>параллельной оси, но не проходящей через ось</b>:', a:'rect' },
{ q:'Сечение цилиндра плоскостью, <b>наклонной к основанию под углом $60°$</b> (пересекает все образующие):', a:'ellipse' },
{ q:'Сечение цилиндра плоскостью, проходящей <b>через ось и наклонённой к основанию</b>:', a:'rect' }
];
const LABELS = {circle:'Круг', rect:'Прямоугольник', ellipse:'Эллипс'};
const area = document.getElementById('p2-iv3-area');
const scoreEl = document.getElementById('p2-iv3-score');
const solved = new Set();
let xpGiven = false;
area.innerHTML = tasks.map(function(t, i){
return '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:8px" id="p2-iv3-q-'+i+'">'
+ '<div style="margin-bottom:6px"><b>Вопрос '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap">'
+ '<button class="btn" data-i="'+i+'" data-v="circle">Круг</button>'
+ '<button class="btn" data-i="'+i+'" data-v="rect">Прямоугольник</button>'
+ '<button class="btn" data-i="'+i+'" data-v="ellipse">Эллипс</button>'
+ '</div>'
+ '<div class="feedback" id="p2-iv3-fb-'+i+'"></div>'
+ '</div>';
}).join('');
renderMath(area);
area.querySelectorAll('button[data-i]').forEach(function(b){
b.addEventListener('click', function(){
const i = +b.dataset.i, v = b.dataset.v, t = tasks[i];
const fb = document.getElementById('p2-iv3-fb-'+i);
if(v === t.a){
feedback(fb, true, '&#10003; Верно — '+LABELS[t.a]+'!');
if(!solved.has(i)){
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p2-iv3');
bumpProgress('p2', 25);
}
}
} else {
feedback(fb, false, '&#10007; Неверно. Подсказка: подумай, как плоскость пересекает образующие.');
}
});
});
})();
/* IV4 — Тренажёр */
(function(){
const PI = 3.14;
const tasks = [
{ q:'Цилиндр $R=2$, $h=5$. Найди $V$ (используй $\\pi\\approx 3{,}14$).', a:+(PI*4*5).toFixed(2), tol:0.05 }, /* 62.80 */
{ q:'Цилиндр $R=3$, $h=4$. Найди $S_{бок}$ ($\\pi\\approx 3{,}14$).', a:+(2*PI*3*4).toFixed(2), tol:0.05 }, /* 75.36 */
{ q:'Цилиндр $R=5$, $h=2$. Найди $S_{полн}$ ($\\pi\\approx 3{,}14$).', a:+(2*PI*5*7).toFixed(2), tol:0.05 }, /* 219.80 */
{ q:'$V$ цилиндра равен $100\\pi$, высота $h=4$. Найди радиус $R$.', a:5, tol:0.05 },
{ q:'Цилиндр $R=2$, $h=3$. Найди длину окружности основания ($\\pi\\approx 3{,}14$).', a:+(2*PI*2).toFixed(2), tol:0.05 }, /* 12.56 */
{ q:'Цилиндр $R=4$ пересечён плоскостью под углом $60°$ к основанию. Найди <b>большую полуось</b> эллипса в сечении.', a:8, tol:0.05 }
];
const list = document.getElementById('p2-iv4-list');
const scoreEl = document.getElementById('p2-iv4-score');
const solved = new Set();
let xpGiven = false;
list.innerHTML = tasks.map(function(t, i){
return '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:8px">'
+ '<div style="margin-bottom:6px"><b>Задача '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
+ '<input type="text" class="tinp" id="p2-iv4-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p2-iv4-fb-'+i+'"></div>'
+ '</div>';
}).join('');
renderMath(list);
list.querySelectorAll('button[data-i]').forEach(function(b){
b.addEventListener('click', function(){
const i = +b.dataset.i, t = tasks[i];
const inp = document.getElementById('p2-iv4-inp-'+i);
const fb = document.getElementById('p2-iv4-fb-'+i);
const raw = (inp.value || '').replace(',', '.').trim();
const val = parseFloat(raw);
if(!isFinite(val)){ feedback(fb, false, '&#10007; Введи число'); return; }
if(Math.abs(val - t.a) <= t.tol){
feedback(fb, true, '&#10003; Верно!');
if(!solved.has(i)){
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p2-iv4');
bumpProgress('p2', 25);
setTimeout(function(){ achievement('p2_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно ($\\pi\\approx 3{,}14$).');
}
});
});
})();
wireReadBtn('p2');
}
/* ===== STUB BUILDER — единый для всех параграфов раздела (Phase 0) ===== */
function buildStub(id){