feat(geom11 ch4 wave2): §10 «Координаты и векторы 3D» + 3D-визуализатор

This commit is contained in:
Maxim Dolgolyov
2026-05-29 15:00:13 +03:00
parent 3cc52e21b0
commit 788d612716
+417 -1
View File
@@ -400,7 +400,7 @@ function buildParaSelector(){
}
const BUILT=new Set();
const BUILDERS = { p8:()=>buildP8(), p9:()=>buildP9(), p10:()=>buildStub('p10'), p11:()=>buildStub('p11'), final4:()=>buildStub('final4') };
const BUILDERS = { p8:()=>buildP8(), p9:()=>buildP9(), p10:()=>buildP10(), p11:()=>buildStub('p11'), final4:()=>buildStub('final4') };
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);
@@ -1191,6 +1191,422 @@ function buildP9(){
wireReadBtn('p9');
}
/* ===== §10 «Координаты и векторы» ===== */
function buildP10(){
const box = document.getElementById('p10-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Координаты в пространстве и длина вектора', '§ 10.1',
'<p><b>Декартова система координат.</b> Три взаимно перпендикулярные оси $Ox$, $Oy$, $Oz$ с общим началом $O$. Каждая точка пространства задаётся тройкой координат $(x;\\, y;\\, z)$.</p>'
+ '<p><b>Вектор</b> $\\vec{v}$ в пространстве — упорядоченная тройка координат:</p>'
+ '<p style="text-align:center;margin:6px 0">$\\vec{v} = (x;\\, y;\\, z)$</p>'
+ '<p><b>Длина вектора:</b></p>'
+ '<p style="text-align:center;margin:6px 0">$|\\vec{v}| = \\sqrt{x^2 + y^2 + z^2}$</p>'
+ '<p><b>Расстояние между точками</b> $A(x_1, y_1, z_1)$ и $B(x_2, y_2, z_2)$:</p>'
+ '<p style="text-align:center;margin:6px 0">$|AB| = \\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2 + (z_2 - z_1)^2}$</p>'
+ '<p><b>Координаты середины отрезка</b> $AB$:</p>'
+ '<p style="text-align:center;margin:6px 0">$M = \\left(\\dfrac{x_1+x_2}{2};\\, \\dfrac{y_1+y_2}{2};\\, \\dfrac{z_1+z_2}{2}\\right)$</p>'
+ '<details class="spoiler"><summary>Пример: $A(1, 2, 3)$, $B(4, 6, 7)$</summary><div class="spoiler-body">'
+ '<p>$|AB| = \\sqrt{(4-1)^2 + (6-2)^2 + (7-3)^2} = \\sqrt{9 + 16 + 16} = \\sqrt{41} \\approx 6{,}40$.</p>'
+ '<p>Середина: $M = \\left(\\dfrac{5}{2};\\, 4;\\, 5\\right) = (2{,}5;\\, 4;\\, 5)$.</p>'
+ '</div></details>');
html += makeCard('rule', 'Скалярное произведение и угол между векторами', '§ 10.2',
'<p><b>Операции с векторами</b> (поэлементно):</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>Сумма: $\\vec{a} + \\vec{b} = (a_1 + b_1;\\, a_2 + b_2;\\, a_3 + b_3)$.</li>'
+ '<li>Разность: $\\vec{a} - \\vec{b} = (a_1 - b_1;\\, a_2 - b_2;\\, a_3 - b_3)$.</li>'
+ '<li>Умножение на число $k$: $k\\vec{a} = (ka_1;\\, ka_2;\\, ka_3)$.</li>'
+ '</ul>'
+ '<p><b>Скалярное произведение</b> (две эквивалентные формулы):</p>'
+ '<p style="text-align:center;margin:6px 0">$\\vec{a} \\cdot \\vec{b} = a_1 b_1 + a_2 b_2 + a_3 b_3 = |\\vec{a}| \\cdot |\\vec{b}| \\cdot \\cos\\alpha$</p>'
+ '<p><b>Косинус угла</b> между векторами:</p>'
+ '<p style="text-align:center;margin:6px 0">$\\cos\\alpha = \\dfrac{\\vec{a} \\cdot \\vec{b}}{|\\vec{a}| \\cdot |\\vec{b}|}$</p>'
+ '<p><b>Условие перпендикулярности:</b> $\\vec{a} \\perp \\vec{b} \\Leftrightarrow \\vec{a} \\cdot \\vec{b} = 0$.</p>'
+ '<details class="spoiler"><summary>Пример: $\\vec{a} = (1, 2, 2)$, $\\vec{b} = (2, -1, 2)$</summary><div class="spoiler-body">'
+ '<p>$\\vec{a} \\cdot \\vec{b} = 1\\cdot 2 + 2\\cdot(-1) + 2\\cdot 2 = 2 - 2 + 4 = 4$.</p>'
+ '<p>$|\\vec{a}| = \\sqrt{1 + 4 + 4} = 3$, $|\\vec{b}| = \\sqrt{4 + 1 + 4} = 3$.</p>'
+ '<p>$\\cos\\alpha = \\dfrac{4}{3 \\cdot 3} = \\dfrac{4}{9} \\approx 0{,}444$, откуда $\\alpha \\approx 63{,}6°$.</p>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — 3D-визуализатор векторов === */
html += '<div class="wg" id="p10-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · визуализатор</span><div class="wg-title">Векторы $\\vec{a}$ и $\\vec{b}$ в пространстве</div></div>'
+ '<div class="wg-help">Перетащи мышью, чтобы вращать сцену. Меняй координаты слайдерами. После <b>4 разных конфигураций</b> — +10 XP.</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-radius:9px;padding:10px;text-align:center;margin-top:6px"><svg id="p10-iv1-svg" viewBox="0 0 480 400" width="100%" style="max-width:480px;height:auto;display:block;margin:0 auto"></svg></div>'
+ '<div class="sliders" style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:14px">'
+ '<div style="background:rgba(234,88,12,0.08);border:1px solid rgba(234,88,12,0.3);border-radius:9px;padding:8px 10px">'
+ '<div style="font-weight:700;color:#ea580c;margin-bottom:6px">$\\vec{a} = (a_1;\\, a_2;\\, a_3)$</div>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$a_1$: <input type="range" id="p10-iv1-a1" min="-3" max="3" step="0.5" value="2" style="width:55%;vertical-align:middle"> <span id="p10-iv1-a1-v" class="kx">2</span></label>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$a_2$: <input type="range" id="p10-iv1-a2" min="-3" max="3" step="0.5" value="1" style="width:55%;vertical-align:middle"> <span id="p10-iv1-a2-v" class="kx">1</span></label>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$a_3$: <input type="range" id="p10-iv1-a3" min="-3" max="3" step="0.5" value="0" style="width:55%;vertical-align:middle"> <span id="p10-iv1-a3-v" class="kx">0</span></label>'
+ '</div>'
+ '<div style="background:rgba(147,51,234,0.08);border:1px solid rgba(147,51,234,0.3);border-radius:9px;padding:8px 10px">'
+ '<div style="font-weight:700;color:#9333ea;margin-bottom:6px">$\\vec{b} = (b_1;\\, b_2;\\, b_3)$</div>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$b_1$: <input type="range" id="p10-iv1-b1" min="-3" max="3" step="0.5" value="0" style="width:55%;vertical-align:middle"> <span id="p10-iv1-b1-v" class="kx">0</span></label>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$b_2$: <input type="range" id="p10-iv1-b2" min="-3" max="3" step="0.5" value="2" style="width:55%;vertical-align:middle"> <span id="p10-iv1-b2-v" class="kx">2</span></label>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$b_3$: <input type="range" id="p10-iv1-b3" min="-3" max="3" step="0.5" value="1.5" style="width:55%;vertical-align:middle"> <span id="p10-iv1-b3-v" class="kx">1.5</span></label>'
+ '</div>'
+ '</div>'
+ '<div id="p10-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--card);border:1px solid var(--border);border-radius:9px;font-size:.92rem;line-height:1.75"></div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Изучено конфигураций: <b id="p10-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Калькулятор векторов === */
html += '<div class="wg" id="p10-iv2">'
+ '<div class="wg-header"><span class="wg-badge">калькулятор</span><div class="wg-title">Операции над векторами</div></div>'
+ '<div class="wg-help">Введи координаты, выбери операцию. После <b>4 операций</b> — +15 XP.</div>'
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:8px">'
+ '<div style="background:rgba(234,88,12,0.08);border:1px solid rgba(234,88,12,0.3);border-radius:9px;padding:8px 10px">'
+ '<div style="font-weight:700;color:#ea580c;margin-bottom:6px">$\\vec{a}$</div>'
+ '<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">'
+ '<span class="kx">$a_1$</span>=<input type="text" class="tinp" id="p10-iv2-a1" value="1" style="width:55px">'
+ '<span class="kx">$a_2$</span>=<input type="text" class="tinp" id="p10-iv2-a2" value="2" style="width:55px">'
+ '<span class="kx">$a_3$</span>=<input type="text" class="tinp" id="p10-iv2-a3" value="2" style="width:55px">'
+ '</div>'
+ '</div>'
+ '<div style="background:rgba(147,51,234,0.08);border:1px solid rgba(147,51,234,0.3);border-radius:9px;padding:8px 10px">'
+ '<div style="font-weight:700;color:#9333ea;margin-bottom:6px">$\\vec{b}$</div>'
+ '<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">'
+ '<span class="kx">$b_1$</span>=<input type="text" class="tinp" id="p10-iv2-b1" value="2" style="width:55px">'
+ '<span class="kx">$b_2$</span>=<input type="text" class="tinp" id="p10-iv2-b2" value="-1" style="width:55px">'
+ '<span class="kx">$b_3$</span>=<input type="text" class="tinp" id="p10-iv2-b3" value="2" style="width:55px">'
+ '</div>'
+ '</div>'
+ '</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:10px">'
+ '<button class="btn primary" data-op="sum">Сумма $\\vec{a}+\\vec{b}$</button>'
+ '<button class="btn primary" data-op="diff">Разность $\\vec{a}-\\vec{b}$</button>'
+ '<button class="btn primary" data-op="dot">Скалярное произв.</button>'
+ '<button class="btn primary" data-op="cos">Угол ($\\cos\\alpha$, $\\alpha°$)</button>'
+ '<button class="btn primary" data-op="len">Длина $|\\vec{a}|$</button>'
+ '</div>'
+ '<div id="p10-iv2-out" style="margin-top:10px;padding:10px 14px;background:var(--card);border:1px solid var(--border);border-radius:9px;font-size:.92rem;line-height:1.75;min-height:40px">Выбери операцию.</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Операций использовано: <b id="p10-iv2-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Тренажёр векторов === */
html += '<div class="wg" id="p10-iv3">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 5 задач</span><div class="wg-title">Векторы и координаты</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$. После всех — +15 XP.</div>'
+ '<div id="p10-iv3-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p10-iv3-score">0</b> / 5</div>'
+ '</div>';
html += secNavFor('p10');
html += readButton('p10');
box.innerHTML = html;
renderMath(box);
/* === IV1 — 3D-визуализатор === */
(function(){
const svg = document.getElementById('p10-iv1-svg');
if(!svg) return;
/* Если G3D недоступен — graceful fallback */
if(!window.G3D){
svg.innerHTML = '<text x="240" y="200" text-anchor="middle" fill="#888">G3D engine not loaded</text>';
return;
}
const scene = G3D.createScene({W:480, H:400, scale:38, camDist:9, rotX:-0.42, rotY:0.78});
const AX_COL = { X:'#dc2626', Y:'#10b981', Z:'#2563eb' };
const VA_COL = '#ea580c'; /* оранжевый */
const VB_COL = '#9333ea'; /* фиолетовый */
const AX_LEN = 4;
const GRID_LEN = 4;
let a = { x:2, y:1, z:0 };
let b = { x:0, y:2, z:1.5 };
const seen = new Set();
let xpGiven = false;
/* Проекция 3D-точки на 2D-плоскость SVG */
function P(v, M){
const r = G3D.vApply(M, v);
return G3D.projectPersp(r, scene.camDist, scene.cx, scene.cy, scene.scale);
}
/* Рендер одной координатной оси (двусторонняя линия + стрелка + подпись) */
function renderAxis(M, dir, col, label){
const len = AX_LEN;
const pPos = P({x:dir.x*len, y:dir.y*len, z:dir.z*len}, M);
const pNeg = P({x:-dir.x*len*0.6, y:-dir.y*len*0.6, z:-dir.z*len*0.6}, M);
const pO = P({x:0,y:0,z:0}, M);
if(!pPos || !pNeg || !pO) return '';
let s = '';
/* отрицательная часть — тонко, пунктир */
s += '<line x1="'+pNeg.x.toFixed(1)+'" y1="'+pNeg.y.toFixed(1)+'" x2="'+pO.x.toFixed(1)+'" y2="'+pO.y.toFixed(1)+'" stroke="'+col+'" stroke-width="1" stroke-dasharray="3,3" opacity=".55"/>';
/* положительная часть — толсто */
s += '<line x1="'+pO.x.toFixed(1)+'" y1="'+pO.y.toFixed(1)+'" x2="'+pPos.x.toFixed(1)+'" y2="'+pPos.y.toFixed(1)+'" stroke="'+col+'" stroke-width="2" opacity=".9"/>';
/* стрелка-треугольник на конце положительной части */
s += renderArrowHead(M, {x:0,y:0,z:0}, {x:dir.x*len, y:dir.y*len, z:dir.z*len}, col, 0.18);
/* подпись */
s += '<text x="'+(pPos.x + dir.x*8).toFixed(1)+'" y="'+(pPos.y - dir.y*10 + 4).toFixed(1)+'" fill="'+col+'" font-size="14" font-weight="700">'+label+'</text>';
return s;
}
/* Стрелка-треугольник в конце вектора */
function renderArrowHead(M, tail, head, col, size){
size = size || 0.22;
/* направление в 3D */
const dx = head.x - tail.x, dy = head.y - tail.y, dz = head.z - tail.z;
const L = Math.sqrt(dx*dx + dy*dy + dz*dz) || 1;
const ux = dx/L, uy = dy/L, uz = dz/L;
/* выбираем «вспомогательную» ось, не параллельную направлению */
let helper = (Math.abs(uy) < 0.9) ? {x:0,y:1,z:0} : {x:1,y:0,z:0};
/* perp1 = u x helper */
const px = uy*helper.z - uz*helper.y;
const py = uz*helper.x - ux*helper.z;
const pz = ux*helper.y - uy*helper.x;
const pl = Math.sqrt(px*px+py*py+pz*pz) || 1;
const npx = px/pl, npy = py/pl, npz = pz/pl;
/* основание стрелки = head - u*size */
const baseX = head.x - ux*size, baseY = head.y - uy*size, baseZ = head.z - uz*size;
/* две точки сбоку */
const sx1 = baseX + npx*size*0.55, sy1 = baseY + npy*size*0.55, sz1 = baseZ + npz*size*0.55;
const sx2 = baseX - npx*size*0.55, sy2 = baseY - npy*size*0.55, sz2 = baseZ - npz*size*0.55;
const pHead = P(head, M);
const pS1 = P({x:sx1,y:sy1,z:sz1}, M);
const pS2 = P({x:sx2,y:sy2,z:sz2}, M);
if(!pHead || !pS1 || !pS2) return '';
return '<polygon points="'+pHead.x.toFixed(1)+','+pHead.y.toFixed(1)+' '+pS1.x.toFixed(1)+','+pS1.y.toFixed(1)+' '+pS2.x.toFixed(1)+','+pS2.y.toFixed(1)+'" fill="'+col+'" stroke="'+col+'" stroke-width="1"/>';
}
/* Рендер вектора-стрелки от O до v */
function renderVector(M, v, col, label){
const L = Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
if(L < 0.01) return '';
const pO = P({x:0,y:0,z:0}, M);
const pV = P(v, M);
if(!pO || !pV) return '';
let s = '';
s += '<line x1="'+pO.x.toFixed(1)+'" y1="'+pO.y.toFixed(1)+'" x2="'+pV.x.toFixed(1)+'" y2="'+pV.y.toFixed(1)+'" stroke="'+col+'" stroke-width="3" stroke-linecap="round"/>';
s += renderArrowHead(M, {x:0,y:0,z:0}, v, col, 0.28);
/* подпись чуть в стороне от наконечника */
s += '<text x="'+(pV.x + 10).toFixed(1)+'" y="'+(pV.y - 6).toFixed(1)+'" fill="'+col+'" font-size="13" font-weight="700" font-style="italic">'+label+'</text>';
return s;
}
/* Рендер сетки на плоскости XOZ (пол) */
function renderGrid(M){
let s = '';
const step = 1, N = GRID_LEN;
for(let i = -N; i <= N; i++){
const p1 = P({x:i,y:-0.001,z:-N}, M);
const p2 = P({x:i,y:-0.001,z: N}, M);
const q1 = P({x:-N,y:-0.001,z:i}, M);
const q2 = P({x: N,y:-0.001,z:i}, M);
if(p1 && p2) s += '<line x1="'+p1.x.toFixed(1)+'" y1="'+p1.y.toFixed(1)+'" x2="'+p2.x.toFixed(1)+'" y2="'+p2.y.toFixed(1)+'" stroke="#94a3b8" stroke-width=".5" opacity=".25"/>';
if(q1 && q2) s += '<line x1="'+q1.x.toFixed(1)+'" y1="'+q1.y.toFixed(1)+'" x2="'+q2.x.toFixed(1)+'" y2="'+q2.y.toFixed(1)+'" stroke="#94a3b8" stroke-width=".5" opacity=".25"/>';
}
return s;
}
function draw(){
const M = G3D.buildRotMatrix(scene);
let s = '';
s += renderGrid(M);
s += renderAxis(M, {x:1,y:0,z:0}, AX_COL.X, 'X');
s += renderAxis(M, {x:0,y:1,z:0}, AX_COL.Y, 'Y');
s += renderAxis(M, {x:0,y:0,z:1}, AX_COL.Z, 'Z');
s += renderVector(M, a, VA_COL, 'a⃗');
s += renderVector(M, b, VB_COL, 'b⃗');
svg.innerHTML = s;
}
function updateOut(){
const out = document.getElementById('p10-iv1-out');
const la = Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z);
const lb = Math.sqrt(b.x*b.x + b.y*b.y + b.z*b.z);
const dot = a.x*b.x + a.y*b.y + a.z*b.z;
const denom = la * lb;
const cosA = denom > 1e-9 ? dot / denom : null;
const ang = cosA !== null ? Math.acos(Math.max(-1, Math.min(1, cosA))) * 180 / Math.PI : null;
const perp = Math.abs(dot) < 1e-6;
let h = '';
h += '<p><b style="color:'+VA_COL+'">$\\vec{a}$</b> = ('+fmt(a.x)+'; '+fmt(a.y)+'; '+fmt(a.z)+'), &nbsp; <b style="color:'+VB_COL+'">$\\vec{b}$</b> = ('+fmt(b.x)+'; '+fmt(b.y)+'; '+fmt(b.z)+')</p>';
h += '<p>$|\\vec{a}| = \\sqrt{'+fmt(a.x*a.x)+'+'+fmt(a.y*a.y)+'+'+fmt(a.z*a.z)+'} \\approx '+la.toFixed(3)+'$, &nbsp; $|\\vec{b}| \\approx '+lb.toFixed(3)+'$</p>';
h += '<p>$\\vec{a} \\cdot \\vec{b} = '+fmt(a.x)+'\\cdot '+fmt(b.x)+' + '+fmt(a.y)+'\\cdot '+fmt(b.y)+' + '+fmt(a.z)+'\\cdot '+fmt(b.z)+' = '+dot.toFixed(3)+'$</p>';
if(cosA !== null){
h += '<p>$\\cos\\alpha = \\dfrac{'+dot.toFixed(3)+'}{'+la.toFixed(3)+'\\cdot '+lb.toFixed(3)+'} \\approx '+cosA.toFixed(3)+'$, &nbsp; $\\alpha \\approx '+ang.toFixed(1)+'°$</p>';
} else {
h += '<p style="color:var(--muted)">Один из векторов нулевой — угол не определён.</p>';
}
if(perp && la > 1e-6 && lb > 1e-6){
h += '<p style="color:#10b981;font-weight:700">$\\vec{a} \\perp \\vec{b}$ &#10003; (скалярное произведение равно 0)</p>';
}
out.innerHTML = h;
renderMath(out);
}
function bumpSeen(){
const key = [a.x,a.y,a.z,b.x,b.y,b.z].map(v=>v.toFixed(1)).join('|');
seen.add(key);
const cnt = document.getElementById('p10-iv1-cnt');
if(cnt) cnt.textContent = Math.min(seen.size, 4);
if(seen.size >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p10-iv1');
bumpProgress('p10', 20);
}
}
function bindSlider(id, axis, key){
const el = document.getElementById('p10-iv1-'+id);
const lbl = document.getElementById('p10-iv1-'+id+'-v');
if(!el || !lbl) return;
el.addEventListener('input', function(){
const v = parseFloat(el.value);
lbl.textContent = (v === Math.round(v)) ? String(v) : v.toFixed(1);
if(axis === 'a') a[key] = v; else b[key] = v;
draw(); updateOut(); bumpSeen();
});
}
bindSlider('a1','a','x'); bindSlider('a2','a','y'); bindSlider('a3','a','z');
bindSlider('b1','b','x'); bindSlider('b2','b','y'); bindSlider('b3','b','z');
draw(); updateOut();
G3D.attachOrbit(svg, scene, draw);
document.querySelectorAll('#p10-iv1 .g3d-tools .btn').forEach(function(btn){
btn.addEventListener('click', function(){
G3D.presetView(scene, btn.dataset.view, draw);
});
});
})();
/* === IV2 — Калькулятор векторов === */
(function(){
const out = document.getElementById('p10-iv2-out');
const cntEl = document.getElementById('p10-iv2-cnt');
const used = new Set();
let xpGiven = false;
function rd(id){
const raw = (document.getElementById('p10-iv2-'+id).value || '').replace(',', '.').trim();
return parseFloat(raw);
}
function readVec(p){
return { x:rd(p+'1'), y:rd(p+'2'), z:rd(p+'3') };
}
function vecOk(v){ return isFinite(v.x) && isFinite(v.y) && isFinite(v.z); }
function vecStr(v){ return '('+fmt(v.x)+';\\, '+fmt(v.y)+';\\, '+fmt(v.z)+')'; }
document.querySelectorAll('#p10-iv2 button[data-op]').forEach(function(btn){
btn.addEventListener('click', function(){
const a = readVec('a'), b = readVec('b');
if(!vecOk(a) || !vecOk(b)){
out.innerHTML = '<span style="color:var(--bad,#ef4444)">Введи числовые координаты во все поля.</span>';
return;
}
const op = btn.dataset.op;
let h = '';
if(op === 'sum'){
const r = { x:a.x+b.x, y:a.y+b.y, z:a.z+b.z };
h = '<p>$\\vec{a} + \\vec{b} = ('+fmt(a.x)+'+'+fmt(b.x)+';\\, '+fmt(a.y)+'+'+fmt(b.y)+';\\, '+fmt(a.z)+'+'+fmt(b.z)+') = '+vecStr(r)+'$</p>';
} else if(op === 'diff'){
const r = { x:a.x-b.x, y:a.y-b.y, z:a.z-b.z };
h = '<p>$\\vec{a} - \\vec{b} = ('+fmt(a.x)+'-('+fmt(b.x)+');\\, '+fmt(a.y)+'-('+fmt(b.y)+');\\, '+fmt(a.z)+'-('+fmt(b.z)+')) = '+vecStr(r)+'$</p>';
} else if(op === 'dot'){
const d = a.x*b.x + a.y*b.y + a.z*b.z;
h = '<p>$\\vec{a} \\cdot \\vec{b} = '+fmt(a.x)+'\\cdot '+fmt(b.x)+' + '+fmt(a.y)+'\\cdot '+fmt(b.y)+' + '+fmt(a.z)+'\\cdot '+fmt(b.z)+' = '+d.toFixed(3)+'$</p>';
if(Math.abs(d) < 1e-6) h += '<p style="color:#10b981;font-weight:700">$\\vec{a} \\perp \\vec{b}$ &#10003;</p>';
} else if(op === 'cos'){
const d = a.x*b.x + a.y*b.y + a.z*b.z;
const la = Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z);
const lb = Math.sqrt(b.x*b.x + b.y*b.y + b.z*b.z);
if(la < 1e-9 || lb < 1e-9){ h = '<p style="color:var(--bad,#ef4444)">Один из векторов нулевой — угол не определён.</p>'; }
else {
const c = d/(la*lb);
const ang = Math.acos(Math.max(-1,Math.min(1,c))) * 180/Math.PI;
h = '<p>$|\\vec{a}| \\approx '+la.toFixed(3)+'$, &nbsp; $|\\vec{b}| \\approx '+lb.toFixed(3)+'$, &nbsp; $\\vec{a}\\cdot\\vec{b} = '+d.toFixed(3)+'$</p>'
+ '<p>$\\cos\\alpha = \\dfrac{'+d.toFixed(3)+'}{'+la.toFixed(3)+'\\cdot '+lb.toFixed(3)+'} \\approx '+c.toFixed(3)+'$, &nbsp; $\\alpha \\approx '+ang.toFixed(2)+'°$</p>';
}
} else if(op === 'len'){
const la = Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z);
h = '<p>$|\\vec{a}| = \\sqrt{'+fmt(a.x*a.x)+'+'+fmt(a.y*a.y)+'+'+fmt(a.z*a.z)+'} = \\sqrt{'+(a.x*a.x+a.y*a.y+a.z*a.z).toFixed(3)+'} \\approx '+la.toFixed(3)+'$</p>';
}
out.innerHTML = h;
renderMath(out);
used.add(op);
cntEl.textContent = Math.min(used.size, 4);
if(used.size >= 4 && !xpGiven){
xpGiven = true;
addXp(15, 'p10-iv2');
bumpProgress('p10', 30);
}
});
});
})();
/* === IV3 — Тренажёр === */
(function(){
const tasks = [
{ q:'Найди длину вектора $\\vec{a} = (2,\\, 3,\\, 6)$.', a:7, tol:0.05 },
{ q:'Скалярное произведение $\\vec{a} = (1,\\, 2,\\, 3)$ и $\\vec{b} = (4,\\, 5,\\, 6)$.', a:32, tol:0.05 },
{ q:'Расстояние между точками $A(1, 2, 3)$ и $B(4, 6, 7)$.', a:6.40, tol:0.05 },
{ q:'Если $\\vec{a} = (3,\\, 0,\\, 4)$, найди $|\\vec{a}|$.', a:5, tol:0.05 },
{ q:'Найди $\\cos\\alpha$ между $\\vec{a} = (1,\\, 0,\\, 0)$ и $\\vec{b} = (1,\\, 1,\\, 0)$.', a:0.71, tol:0.05 }
];
const list = document.getElementById('p10-iv3-list');
const scoreEl = document.getElementById('p10-iv3-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="p10-iv3-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p10-iv3-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('p10-iv3-inp-'+i);
const fb = document.getElementById('p10-iv3-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, 'p10-iv3');
bumpProgress('p10', 30);
setTimeout(function(){ achievement('p10_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
wireReadBtn('p10');
}
/* ===== Search ===== */
const SEARCH_INDEX = (function(){
const arr=[];