Files
Maxim Dolgolyov 8786cf5e20 fix(textbooks): убраны лишние слэши в LaTeX-формулах (over-escaping)
Формулы в JS-литералах имели \\\\dfrac / \\\\\\\\dfrac (4/8 слэшей) вместо
\\dfrac (2). После JS-анескейпа KaTeX получал \\dfrac, трактовал \\ как
перенос строки и печатал dfrac/cdot/sqrt/pi как текст (карточка пирамиды и
конуса в geometry_11_ch2, и др.).

Схлопнуты прогоны слэшей кратные 4 перед LaTeX-командой -> 2. Прогоны из
3 слэшей (\\ перенос строки + \cmd в \begin{cases}) и перед x/цифрой не
тронуты. 150 правок в 7 файлах (algebra_11_ch1/ch2/ch3, geometry_11_ch1..ch4).

БД чиста: questions (1398) text/explanation/correct_text + options (5187) -
0 багов. Скрипт: backend/scripts/fix_overescaped_latex.js (идемпотентный,
dry-run по умолчанию, --apply, с KaTeX-валидацией).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:53:17 +03:00

2528 lines
161 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Геометрия 11 · Раздел 3 · «Сфера и шар»</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/g3d.js" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#fafafa; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
--pri:#7c3aed; --pri2:#6d28d9; --pri-soft:#ede9fe;
--acc:#a78bfa; --acc2:#7c3aed; --acc-soft:#f3e8ff;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0e0521; --card:#1a0a30; --card-soft:#220c3d; --text:#ede9fe; --ink:#ede9fe; --muted:#c4b5fd; --border:#3a1d5e}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
button,input,select,textarea{font-family:inherit;font-size:inherit}
button{cursor:pointer;border:0;background:transparent;color:inherit}
a{color:inherit;text-decoration:none}
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
.hdr{position:relative;background:linear-gradient(110deg,#3b0764 0%,#7c3aed 55%,#a78bfa 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(167,139,250,.2);min-height:130px}
.hdr::before{content:'РАЗДЕЛ 3';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.12);line-height:1;pointer-events:none;user-select:none;z-index:0}
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
.col-main{min-width:0}
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.hero::before{content:'\25C7';position:absolute;right:0;top:-30px;font-size:clamp(2rem,12vw,8rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
.psel{margin-bottom:24px}
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
.psel-card.final{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft))}
.psel-card.final .psel-num{color:var(--warn)}
.sec[id="sec-p5"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-p6"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-p7"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-final3"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec{display:none;position:relative;animation:fadeIn .35s ease}
.sec.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec::before{content:attr(data-watermark);position:absolute;right:-20px;top:10%;font-family:'Unbounded',sans-serif;font-size:clamp(6rem,18vw,14rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px var(--sec-acc-soft,var(--pri-soft));line-height:1;pointer-events:none;user-select:none;z-index:0;opacity:.35}
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.04);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(0,0,0,.08)}
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}
.card-icon .ic{width:18px;height:18px}
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
.card-body{font-size:.94rem;line-height:1.65}
.card-body p{margin-bottom:8px}
.card-body p:last-child{margin-bottom:0}
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.btn:active{transform:scale(.96)}
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
.wg{background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)));border:1.5px solid var(--sec-acc,var(--pri));border-radius:14px;padding:18px 20px;margin-bottom:18px;box-shadow:var(--sh2);position:relative;z-index:1}
.wg-header{display:flex;align-items:center;gap:8px;margin-bottom:14px}
.wg-badge{padding:4px 9px;background:var(--sec-acc,var(--pri));color:#fff;border-radius:6px;font-family:'Unbounded',sans-serif;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
.wg-title{font-family:'Unbounded',sans-serif;font-size:1.05rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));flex:1}
.wg-help{font-size:.88rem;color:var(--text);margin-bottom:12px;line-height:1.55;background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--sec-acc-soft,var(--pri-soft)));border-left:4px solid var(--warn,#f59e0b);padding:9px 14px;border-radius:9px}
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);transition:border-color .15s;font-family:'JetBrains Mono',monospace}
.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.sliders{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:10px}
.sliders label{display:block;font-size:.92rem;color:var(--muted);background:var(--card);padding:8px 12px;border-radius:8px;border:1px solid var(--border);line-height:1.5}
.sliders label b{font-family:'JetBrains Mono',monospace;font-size:1.05rem;color:var(--sec-acc-d,var(--pri2));margin-left:4px}
.sliders label input[type="range"]{display:block;width:100%;margin-top:6px;accent-color:var(--sec-acc,var(--pri))}
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-bottom:12px}
.score-display b{color:var(--sec-acc-d,var(--pri2));font-size:1.15rem}
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
.spoiler summary{padding:8px 14px;background:var(--sec-acc-soft,var(--pri-soft));font-weight:700;cursor:pointer;font-size:.88rem;color:var(--sec-acc-d,var(--pri2));list-style:none;display:flex;align-items:center;gap:8px}
.spoiler summary::-webkit-details-marker{display:none}
.spoiler summary::before{content:'+';font-size:1.2rem;font-weight:900;color:var(--sec-acc,var(--pri));width:18px}
.spoiler[open] summary::before{content:'\2212'}
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px;transition:border-color .18s,background .18s}
.dnd-pool.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
.dnd-pool.col{flex-direction:column;align-items:stretch}
.dnd-pool.col .dnd-chip{width:auto}
.dnd-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--card);border:1.5px solid var(--border);border-radius:10px;cursor:grab;user-select:none;font-size:.92rem;line-height:1.4;transition:transform .12s,box-shadow .12s,border-color .12s;touch-action:none;max-width:100%}
.dnd-chip:hover{transform:translateY(-1px);border-color:var(--sec-acc,var(--pri));box-shadow:var(--sh)}
.dnd-chip:active{cursor:grabbing}
.dnd-chip.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px #ede9fe;transform:translateY(-1px)}
.dnd-chip.dragging{opacity:.28}
.dnd-chip.placed{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.dnd-chip .dnd-x{padding:0 5px;color:var(--muted);font-weight:700;font-size:1.05rem;border-radius:4px;cursor:pointer}
.dnd-chip .dnd-x:hover{color:var(--bad,var(--fail));background:var(--fail-bg)}
.drop-box{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px;transition:border-color .15s,background .15s}
.drop-box:hover{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em}
.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
.dnd-hint{font-size:.78rem;color:var(--muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
.dnd-hint svg{width:14px;height:14px;flex-shrink:0}
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
.sidecard-row b{color:var(--pri);font-weight:700}
.sidecard-row:last-child{margin-bottom:0}
@media(max-width:980px){.col-side{position:static;max-height:none}}
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(0,0,0,.10);border-radius:6px;overflow:hidden;margin:7px 0}
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
.col-side-backdrop.show{display:block}
@media(min-width:981px){#sidebar-btn{display:none}.col-side-backdrop.show{display:none}}
@media(max-width:980px){
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
.col-side.open{transform:none}
}
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
.search-modal.show{display:flex}
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
.search-results{flex:1;overflow-y:auto;padding:6px 0}
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
/* === GEOM11 POLISH === */
@keyframes wgFadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec.active .wg{animation:wgFadeIn .35s cubic-bezier(.16,1,.3,1) backwards}
.sec.active .wg:nth-of-type(1){animation-delay:.02s}
.sec.active .wg:nth-of-type(2){animation-delay:.08s}
.sec.active .wg:nth-of-type(3){animation-delay:.14s}
.sec.active .wg:nth-of-type(4){animation-delay:.20s}
.sec.active .wg:nth-of-type(5){animation-delay:.26s}
.sec.active .wg:nth-of-type(6){animation-delay:.32s}
.wg svg{transition:filter .25s ease}
.wg:hover svg{filter:drop-shadow(0 4px 16px rgba(0,0,0,.10))}
input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
.wg input[type=range]{cursor:ew-resize}
.score-display b{transition:transform .22s cubic-bezier(.16,1,.3,1),color .22s;display:inline-block;transform-origin:center}
.score-display b.bump{transform:scale(1.28);color:var(--pri)}
.katex{transition:color .2s}
.wg-help .katex:hover,.card-body .katex:hover{color:var(--pri2,var(--pri));cursor:help}
.hp-fill,.psel-prog-fill,.xp-fill,[id$="-overall-fill"]{transition:width .6s cubic-bezier(.16,1,.3,1)!important}
.boss-card,.btn.primary,.btn-primary{position:relative;overflow:hidden}
.btn.primary::after,.btn-primary::after{content:'';position:absolute;inset:0;background:radial-gradient(circle at center,rgba(255,255,255,.42) 0%,transparent 60%);opacity:0;transition:opacity .25s;pointer-events:none}
.btn.primary:hover::after,.btn-primary:hover::after{opacity:1}
.psel-card{position:relative}
.psel-card .psel-done{position:absolute;top:6px;right:6px;width:18px;height:18px;border-radius:50%;background:#10b981;display:none;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(16,185,129,.45);z-index:2}
.psel-card .psel-done svg{width:11px;height:11px;stroke:#fff;fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.psel-card.done .psel-done{display:flex}
.sec{transition:opacity .25s}
/* g3d toolbar */
.g3d-tools{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
.g3d-tools .btn{padding:5px 10px;font-size:.78rem}
.stub-note{padding:18px 22px;background:linear-gradient(135deg,var(--pri-soft),var(--sec-acc-soft));border:1.5px dashed var(--pri);border-radius:13px;text-align:center;color:var(--text);margin-bottom:14px}
.stub-note h3{font-family:'Unbounded',sans-serif;color:var(--pri2);margin-bottom:8px;font-size:1.05rem}
.stub-note p{color:var(--muted);font-size:.9rem;line-height:1.55}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 11 · Раздел 3</h1>
<div class="hdr-sub">Сфера и её уравнение · шар, сегменты · 5 платоновых тел</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-11" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К геометрии 11</a>
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero">
<h2>Сфера, шар, правильные многогранники</h2>
<p>Сфера, шар, пять платоновых тел. Уравнение сферы в координатах, шаровые сегменты, вписанные и описанные многогранники.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p5')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 5</button>
<div class="hero-progress">
<span class="hp-label">Прогресс по разделу</span>
<div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div>
<span id="hero-hp-text" class="hp-text">0%</span>
</div>
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
</div>
</section>
<section class="psel">
<div class="psel-title">Параграфы раздела</div>
<div id="psel-grid" class="psel-grid"></div>
</section>
<section id="sec-p5" class="sec" data-watermark="S^2"><div class="sec-header"><span class="sec-num">§ 5</span><h2 class="sec-h">Сфера</h2></div><div id="p5-body"></div></section>
<section id="sec-p6" class="sec" data-watermark="V"><div class="sec-header"><span class="sec-num">§ 6</span><h2 class="sec-h">Шар</h2></div><div id="p6-body"></div></section>
<section id="sec-p7" class="sec" data-watermark="\star"><div class="sec-header"><span class="sec-num">§ 7</span><h2 class="sec-h">Правильные многогранники</h2></div><div id="p7-body"></div></section>
<section id="sec-final3" class="sec" data-watermark="&#9733;"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#7c3aed,#a78bfa)">&#9733;</span><h2 class="sec-h">Финал раздела</h2></div><div id="final3-body"></div></section>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot">Интерактивный учебник «Геометрия 11» · Раздел 3 · «Сфера и шар» · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="search-modal" class="search-modal" role="dialog">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>↑↓</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
const STATE = { current:'p5', progress:{}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 4;
const _TB_SLUG = 'geometry-11-ch3';
const PARAS = [
{ id:'p5', num:'§ 5', name:"Сфера", sub:'$(x-a)^2+(y-b)^2+(z-c)^2=R^2$' },
{ id:'p6', num:'§ 6', name:"Шар", sub:'$S=4\\pi R^2$, $V=\\frac{4}{3}\\pi R^3$' },
{ id:'p7', num:'§ 7', name:"Правильные многогранники", sub:'5 платоновых тел' },
{ id:'final3', num:'&#9733;', name:"Финал раздела", sub:'Итоги · боссы раздела 3', final:true }
];
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
const ACH_LABELS = {
p5_done:"Сфера освоено!",
p6_done:"Шар освоено!",
p7_done:"Правильные многогранники освоено!",
start:"Начало раздела 3!",
ch3_done:"Раздел 3 пройден!"
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry11_ch3_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry11_ch3_achievements');
if(a){ const p=JSON.parse(a); if(Array.isArray(p)) p.forEach(id=>STATE.achievements.set(id, ACH_LABELS[id]||id)); else if(p&&typeof p==='object'){ for(const[id,t] of Object.entries(p)) STATE.achievements.set(id,(t&&t!==id)?t:(ACH_LABELS[id]||id)); } }
STATE.xp=+(localStorage.getItem('geometry11_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry11_ch3_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry11_ch3_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry11_xp', String(STATE.xp));
}catch(e){}
}
function bumpProgress(key, delta){
STATE.progress[key]=Math.max(0,Math.min(100,(STATE.progress[key]||0)+delta));
saveProgress(); refreshProgressUI();
if(STATE.progress[key]>=50) markParaRead(key);
}
const _markedRead=new Set();
let _pendingProgressBody=null, _progressTimer=null;
function _flushProgress(){
const body=_pendingProgressBody; _pendingProgressBody=null; if(!body) return;
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
fetch('/api/textbooks/'+_TB_SLUG+'/progress',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tok},body:JSON.stringify(body),keepalive:true}).catch(()=>{});
}
function _queueProgress(patch){ _pendingProgressBody=Object.assign(_pendingProgressBody||{},patch); if(_progressTimer) clearTimeout(_progressTimer); _progressTimer=setTimeout(_flushProgress, 600); }
function markLastPara(id){ _queueProgress({last_para:id}); }
function markParaRead(id){ if(_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({mark_read:id}); }
window.addEventListener('beforeunload', _flushProgress);
function loadServerReadState(){
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
fetch('/api/textbooks/'+_TB_SLUG,{headers:{'Authorization':'Bearer '+tok}}).then(r=>r.ok?r.json():null).then(d=>{
if(!d||!d.progress) return;
(d.progress.read||[]).forEach(k=>{_markedRead.add(k); if((STATE.progress[k]||0)<50) STATE.progress[k]=100;});
saveProgress(); refreshProgressUI();
}).catch(()=>{});
}
function addXp(n,src){
if(!n) return;
const prev=STATE.level; STATE.xp=Math.max(0,(STATE.xp||0)+n); STATE.level=calcLevel(STATE.xp);
saveProgress(); refreshProgressUI();
if(window.LS&&window.LS.xp) window.LS.xp.add(n,'geometry11-ch3-'+(src||'misc'));
if(STATE.level>prev){
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
}
}
function refreshProgressUI(){
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
const xpBadge=document.getElementById('hero-xp-badge');
if(xpBadge){ xpBadge.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
}
function achievement(id,text){
if(STATE.achievements.has(id)) return;
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
addXp(20,'ach-'+id);
}
function buildParaSelector(){
const g=document.getElementById('psel-grid'); g.innerHTML='';
PARAS.forEach(p=>{
const card=document.createElement('div');
card.className='psel-card'+(p.final?' final':'');
card.dataset.id=p.id; card.dataset.progCard=p.id;
card.innerHTML='<div class="psel-num">'+p.num+'</div><div class="psel-name">'+p.name+'</div><div class="psel-prog"><div class="psel-prog-fill"></div></div>';
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
}
const BUILT=new Set();
const BUILDERS = { p5:buildP5, p6:buildP6, p7:buildP7, final3:buildFinal3 };
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);
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
const el=document.getElementById('sec-'+id); if(el) el.classList.add('active');
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id===id));
buildSidebar(id);
window.scrollTo({top:0,behavior:'smooth'});
if((STATE.progress[id]||0)<10) bumpProgress(id, 10);
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
markLastPara(id);
}
const SIDEBARS = {
p5:{title:"Шпаргалка § 5", rows:[
["Тема", "Сфера"],
["Сфера", "множество точек, $|OM|=R$"],
["Шар", "множество точек, $|OM|\\le R$"],
["Уравнение", "$(x-a)^2+(y-b)^2+(z-c)^2=R^2$"],
["Касательная", "плоскость $\\perp$ радиусу $OM$"],
["Сечение", "окружность $r=\\sqrt{R^2-d^2}$"],
["Большой круг", "$d=0$, $r=R$"],
["Площадь", "$S=4\\pi R^2$"],
["Объём шара", "$V=\\tfrac{4}{3}\\pi R^3$"]
]},
p6:{title:"Шпаргалка § 6", rows:[
["Тема", "Шар"],
["Шар", "тело, $|OM|\\le R$"],
["Площадь сферы", "$S=4\\pi R^2$"],
["Объём шара", "$V=\\tfrac{4}{3}\\pi R^3$"],
["Сегмент $V$", "$\\tfrac{\\pi h^2(3R-h)}{3}$"],
["Сегмент $S$", "$2\\pi R h$"],
["Сектор $V$", "$\\tfrac{2}{3}\\pi R^2 h$"],
["Слой $V$", "$\\tfrac{\\pi h}{6}(3r_1^2+3r_2^2+h^2)$"],
["Куб впис. шар", "$r=a/2$"],
["Куб опис. шар", "$R=\\tfrac{a\\sqrt{3}}{2}$"]
]},
p7:{title:"Шпаргалка § 7", rows:[
["Тема", "Правильные многогранники"],
["Условие", "грани = равные правильные $n$-угольники"],
["Тел всего", "ровно $5$ (платоновых)"],
["Тетраэдр", "$F{=}4,\\\\ V{=}4,\\\\ E{=}6$"],
["Куб", "$F{=}6,\\\\ V{=}8,\\\\ E{=}12$"],
["Октаэдр", "$F{=}8,\\\\ V{=}6,\\\\ E{=}12$"],
["Додекаэдр", "$F{=}12,\\\\ V{=}20,\\\\ E{=}30$"],
["Икосаэдр", "$F{=}20,\\\\ V{=}12,\\\\ E{=}30$"],
["Эйлер", "$V-E+F=2$"],
["Куб$\\leftrightarrow$Окт.", "двойственные"],
["Дод.$\\leftrightarrow$Икос.", "двойственные"]
]},
final3:{title:"Финал раздела 3", rows:[["§ 5–§ 7","теория раздела 3"],["Награда","+50 XP"]]}
};
const TIPS=[
{sec:'p5',html:"Сфера: $|OM|=R$. Уравнение $(x-a)^2+(y-b)^2+(z-c)^2=R^2$. Сечение плоскостью — окружность $r=\\sqrt{R^2-d^2}$."},
{sec:'p6',html:"Шар: $S=4\\pi R^2$, $V=\\tfrac{4}{3}\\pi R^3$. Сегмент: $V=\\tfrac{\\pi h^2(3R-h)}{3}$, $S=2\\pi R h$. Куб впис. шар: $r=a/2$; куб опис. шар: $R=\\tfrac{a\\sqrt 3}{2}$."},
{sec:'p7',html:"Платоновых тел ровно $5$: тетраэдр, куб, октаэдр, додекаэдр, икосаэдр. Формула Эйлера: $V-E+F=2$. Двойственные пары: куб$\\leftrightarrow$октаэдр, додекаэдр$\\leftrightarrow$икосаэдр; тетраэдр — сам себе двойственный."},
{sec:'final3',html:"Финал раздела 3 — интегрированные задачи по разделу."}
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
let html='';
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
html+='</div>';
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
if(tip){
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
}
if(STATE.achievements.size>0){
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">&#10003; '+text+'</div>'; });
html+='</div>';
}
box.innerHTML=html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
function initTheme(){
const t=localStorage.getItem('geometry11_ch3_theme')||'light';
if(t==='dark') document.documentElement.classList.add('dark');
document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';
document.getElementById('theme-btn').addEventListener('click', ()=>{
document.documentElement.classList.toggle('dark');
const dark=document.documentElement.classList.contains('dark');
localStorage.setItem('geometry11_ch3_theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
}
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'&#10003; Верно!':'&#10007; Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
function ipow(base, exp){ let r=1; for(let i=0;i<Math.abs(exp);i++) r*=base; return exp<0 ? 1/r : r; }
function gcd(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ const t=b; b=a%b; a=t; } return a||1; }
function makeCard(kind, title, num, body){
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
}
function setupSorter(cfg){
const placed = {}; const pool = document.getElementById(cfg.poolId); const scope = document.querySelector(cfg.scopeSelector);
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
let armed = null;
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
function attach(elm,itId){ let ghost=null,dragging=false,sx=0,sy=0; elm.addEventListener('pointerdown',ev=>{ if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault(); if(ev.target.classList&&ev.target.classList.contains('dnd-x')){ ev.stopPropagation(); if(placed[itId]){delete placed[itId];render();}else if(armed===itId){armed=null;render();} return; } sx=ev.clientX;sy=ev.clientY; const r=elm.getBoundingClientRect(); const ox=ev.clientX-r.left,oy=ev.clientY-r.top; try{elm.setPointerCapture(ev.pointerId);}catch(e){} function onMove(e){ const dx=e.clientX-sx,dy=e.clientY-sy; if(!dragging&&Math.hypot(dx,dy)>8){ dragging=true; ghost=elm.cloneNode(true); ghost.classList.remove('armed'); ghost.style.cssText='position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:'+r.width+'px;left:'+(e.clientX-ox)+'px;top:'+(e.clientY-oy)+'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if(dragging&&ghost){ ghost.style.left=(e.clientX-ox)+'px';ghost.style.top=(e.clientY-oy)+'px'; const under=document.elementsFromPoint(e.clientX,e.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); const tgt=under.find(n=>n.classList&&(n.classList.contains('drop-box')||n.classList.contains('dnd-pool'))); if(tgt)tgt.classList.add('over'); } } function onUp(e){ elm.removeEventListener('pointermove',onMove);elm.removeEventListener('pointerup',onUp);elm.removeEventListener('pointercancel',onUp);elm.classList.remove('dragging'); if(ghost){ghost.remove();ghost=null;} scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); if(dragging){ const under=document.elementsFromPoint(e.clientX,e.clientY); const box=under.find(n=>n.classList&&n.classList.contains('drop-box')); const pl=under.find(n=>n.classList&&n.classList.contains('dnd-pool')); if(box){const di=box.querySelector('[data-cat]');if(di){placed[itId]=di.dataset.cat;armed=null;render();return;}}else if(pl){delete placed[itId];armed=null;render();return;} }else{ if(placed[itId]){delete placed[itId];armed=null;render();}else{armed=(armed===itId)?null:itId;render();} } dragging=false; } elm.addEventListener('pointermove',onMove);elm.addEventListener('pointerup',onUp);elm.addEventListener('pointercancel',onUp); }); }
function attachBoxTaps(){ scope.querySelectorAll('.drop-box').forEach(box=>{ box.addEventListener('click',ev=>{ if(!armed)return; if(ev.target.closest('.dnd-chip'))return; const di=box.querySelector('[data-cat]'); if(di){placed[armed]=di.dataset.cat;armed=null;render();} }); }); }
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); cfg.cats.forEach(cat=>{const box=scope.querySelector('.drop-items[data-cat="'+cat+'"]');if(!box)return;box.innerHTML='';cfg.items.forEach(it=>{if(placed[it.id]!==cat)return;box.appendChild(buildChip(it,true));});}); if(window.renderMathInElement)try{renderMath(scope);}catch(_){} }
attachBoxTaps(); render();
return {placed,render,reset(){ for(const k in placed)delete placed[k];armed=null;render(); }};
}
/* === SVG-хелперы 2D (axes, plotFunc, pointWithDrop, asymptote, snapToValue, геом.) === */
function axes2D(W, H, pad, xmin, xmax, ymin, ymax){
const ux = (W - 2*pad) / (xmax - xmin);
const uy = (H - 2*pad) / (ymax - ymin);
const toX = v => pad + (v - xmin) * ux;
const toY = v => H - pad - (v - ymin) * uy;
let g = '';
g += '<g stroke="#e5e7eb" stroke-width="1">';
for (let x = Math.ceil(xmin); x <= xmax; x++){
g += '<line x1="'+toX(x)+'" y1="'+pad+'" x2="'+toX(x)+'" y2="'+(H-pad)+'"/>';
}
for (let y = Math.ceil(ymin); y <= ymax; y++){
g += '<line x1="'+pad+'" y1="'+toY(y)+'" x2="'+(W-pad)+'" y2="'+toY(y)+'"/>';
}
g += '</g>';
const y0 = toY(0), x0 = toX(0);
g += '<line x1="'+pad+'" y1="'+y0+'" x2="'+(W-pad)+'" y2="'+y0+'" stroke="#0f172a" stroke-width="1.5"/>';
g += '<line x1="'+x0+'" y1="'+pad+'" x2="'+x0+'" y2="'+(H-pad)+'" stroke="#0f172a" stroke-width="1.5"/>';
g += '<text x="'+(W-pad+2)+'" y="'+(y0-4)+'" font-size="11" fill="#0f172a">x</text>';
g += '<text x="'+(x0+4)+'" y="'+(pad-2)+'" font-size="11" fill="#0f172a">y</text>';
g += '<g font-size="10" fill="#64748b">';
for (let x = Math.ceil(xmin); x <= xmax; x++){
if (x !== 0) g += '<text x="'+(toX(x)-3)+'" y="'+(y0+12)+'">'+x+'</text>';
}
for (let y = Math.ceil(ymin); y <= ymax; y++){
if (y !== 0) g += '<text x="'+(x0+4)+'" y="'+(toY(y)+3)+'">'+y+'</text>';
}
g += '<text x="'+(x0+4)+'" y="'+(y0+12)+'">0</text>';
g += '</g>';
return { content: g, toX, toY, ux, uy };
}
function plotFunc(f, xmin, xmax, toX, toY, color, N){
N = N || 200;
let d = '';
let prevValid = false;
for (let i = 0; i <= N; i++){
const x = xmin + (xmax - xmin) * i / N;
let y;
try { y = f(x); } catch(e){ y = NaN; }
if (!isFinite(y) || isNaN(y) || y < -1e4 || y > 1e4){ prevValid = false; continue; }
d += (prevValid ? ' L' : ' M') + toX(x).toFixed(2) + ',' + toY(y).toFixed(2);
prevValid = true;
}
return '<path d="'+d+'" stroke="'+color+'" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>';
}
function pointWithDrop(x, fx, toX, toY, color, label){
const px = toX(x), py = toY(fx);
let s = '';
s += '<line x1="'+px+'" y1="'+py+'" x2="'+px+'" y2="'+toY(0)+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
s += '<line x1="'+px+'" y1="'+py+'" x2="'+toX(0)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
s += '<circle cx="'+px+'" cy="'+py+'" r="4.5" fill="'+color+'" stroke="#fff" stroke-width="2"/>';
if (label){
s += '<text x="'+(px+8)+'" y="'+(py-8)+'" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="'+color+'">'+label+'</text>';
}
return s;
}
function asymptote(orientation, value, toX, toY, xmin, xmax, ymin, ymax, color){
color = color || '#94a3b8';
if (orientation === 'h'){
const y = toY(value);
return '<line x1="'+toX(xmin)+'" y1="'+y+'" x2="'+toX(xmax)+'" y2="'+y+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
} else {
const x = toX(value);
return '<line x1="'+x+'" y1="'+toY(ymin)+'" x2="'+x+'" y2="'+toY(ymax)+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
}
}
function snapToValue(value, snapPoints, tolerance){
tolerance = tolerance || 0.1;
for (const sp of snapPoints){
if (Math.abs(value - sp) < tolerance) return sp;
}
return value;
}
function rightAngleMark(V, uIn, wIn, s){
s = s || 9;
const p1 = {x: V.x + s*uIn.x, y: V.y + s*uIn.y};
const c = {x: p1.x + s*wIn.x, y: p1.y + s*wIn.y};
const p2 = {x: V.x + s*wIn.x, y: V.y + s*wIn.y};
return p1.x+','+p1.y+' '+c.x+','+c.y+' '+p2.x+','+p2.y;
}
function angleArcAuto(V, uA, uB, R){
const sA = {x: V.x + R*uA.x, y: V.y + R*uA.y};
const eB = {x: V.x + R*uB.x, y: V.y + R*uB.y};
const cross = uA.x*uB.y - uA.y*uB.x;
const sweep = cross > 0 ? 1 : 0;
return 'M'+sA.x+','+sA.y+' A'+R+','+R+' 0 0,'+sweep+' '+eB.x+','+eB.y;
}
function unitVec(p1, p2){
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const len = Math.sqrt(dx*dx + dy*dy) || 1;
return {x: dx/len, y: dy/len};
}
function deg2rad(d){ return d * Math.PI / 180; }
const ICONS = {
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
};
function secNavFor(curId){
const idx = PARAS.findIndex(p => p.id === curId);
const prev = idx > 0 ? PARAS[idx-1].id : null;
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
return secNav(prev, next);
}
function secNav(prev, next){
const NAMES = {p5:'\xA75',p6:'\xA76',p7:'\xA77',final3:'Финал'};
let h='<div class="sec-nav">';
h+=prev?'<button class="btn" onclick="goTo(\''+prev+'\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> '+NAMES[prev]+'</button>':'<span></span>';
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+NAMES[next]+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
h+='</div>'; return h;
}
function readButton(paraId){
const p = PARAS.find(x => x.id === paraId);
const labelTail = p && p.final ? 'финал' : (p ? p.num : '\xA7?');
return '<div style="margin-top:18px;display:flex;justify-content:center">'
+'<button class="btn primary" id="'+paraId+'-read-btn">'
+'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
+' Я прочитал — '+labelTail+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
btn.addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
const aId = paraId+'_done';
if(ACH_LABELS[aId]) achievement(aId);
});
}
/* ===== § 5 «Сфера» — Wave 1 ===== */
function buildP5(){
const box = document.getElementById('p5-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Определение и элементы', '§ 5.1',
'<p><b>Сфера</b> — множество всех точек пространства, равноудалённых от заданной точки $O$ (<b>центра</b>).</p>'
+ '<p><b>Шар</b> — множество точек, для которых $|OM|\\le R$, где $O$ — центр, $R$ — радиус. Шар ограничен сферой; сфера — поверхность шара.</p>'
+ '<p><b>Элементы:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Центр</b> $O$ — точка, от которой все точки сферы равноудалены.</li>'
+ '<li><b>Радиус</b> $R=|OM|$ для любой точки $M$ сферы.</li>'
+ '<li><b>Диаметр</b> — отрезок через центр между двумя точками сферы. Длина $d=2R$.</li>'
+ '<li><b>Хорда</b> — отрезок между двумя точками сферы (не обязательно через центр).</li>'
+ '</ul>'
+ '<p><b>Уравнение сферы</b> в декартовой системе координат. Для центра $C(a,b,c)$ и радиуса $R$:</p>'
+ '<p style="text-align:center;margin:8px 0">$$(x-a)^2+(y-b)^2+(z-c)^2=R^2$$</p>'
+ '<p>Если центр в начале координат:</p>'
+ '<p style="text-align:center;margin:8px 0">$$x^2+y^2+z^2=R^2$$</p>'
+ '<p>Уравнение выражает то, что квадрат расстояния от точки $(x,y,z)$ до центра $C$ равен $R^2$.</p>');
html += makeCard('rule', 'Касательная плоскость', '§ 5.2',
'<p><b>Касательная плоскость к сфере</b> — плоскость, имеющая со сферой <b>ровно одну</b> общую точку. Эту точку называют <b>точкой касания</b>.</p>'
+ '<p><b>Признак касания</b> (необходимый и достаточный):</p>'
+ '<p style="background:var(--sec-acc-soft,var(--pri-soft));border-left:4px solid var(--sec-acc,var(--pri));padding:8px 12px;border-radius:6px;margin:8px 0">Плоскость касается сферы в точке $M$ тогда и только тогда, когда она <b>перпендикулярна радиусу $OM$</b>, проведённому в точку касания.</p>'
+ '<p>Через каждую точку сферы можно провести <b>единственную</b> касательную плоскость.</p>'
+ '<p>Расстояние от центра сферы до касательной плоскости равно радиусу $R$.</p>'
+ '<details class="spoiler"><summary>Пример: касательная к сфере $x^2+y^2+z^2=25$ в точке $M(3;4;0)$</summary><div class="spoiler-body">'
+ '<p>Центр $O(0;0;0)$, радиус-вектор $\\overrightarrow{OM}=(3;4;0)$ направлен из центра в точку касания.</p>'
+ '<p>Значит нормаль касательной плоскости $\\vec{n}=(3;4;0)$. Уравнение плоскости в точке $M$:</p>'
+ '<p>$3(x-3)+4(y-4)+0\\cdot(z-0)=0$, то есть $3x+4y=25$.</p>'
+ '</div></details>');
html += makeCard('example', 'Сечения сферы и большой круг', '§ 5.3',
'<p><b>Сечение сферы плоскостью</b>, пересекающей её — всегда <b>окружность</b>. Это следует из того, что точки сечения равноудалены от проекции центра сферы на эту плоскость.</p>'
+ '<p>Связь радиуса сечения $r$, радиуса сферы $R$ и расстояния $d$ от центра сферы до секущей плоскости:</p>'
+ '<p style="text-align:center;margin:8px 0">$$r=\\sqrt{R^2-d^2}$$</p>'
+ '<p><b>Три случая:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>$d<R$ — пересечение есть, сечение — окружность радиуса $r$.</li>'
+ '<li>$d=R$ — плоскость касается сферы; «сечение» вырождается в точку.</li>'
+ '<li>$d>R$ — общих точек нет.</li>'
+ '</ul>'
+ '<p><b>Большой круг</b> — сечение, проходящее через центр сферы ($d=0$). Радиус большого круга максимален и равен $R$.</p>'
+ '<p>Любые два больших круга пересекаются по диаметру сферы.</p>'
+ '<details class="spoiler"><summary>Пример: $R=5$, $d=3$</summary><div class="spoiler-body">'
+ '<p>$r=\\sqrt{R^2-d^2}=\\sqrt{25-9}=\\sqrt{16}=4$.</p>'
+ '<p>Сечение — окружность радиуса $4$ в секущей плоскости.</p>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — 3D-визуализатор сферы === */
html += '<div class="wg" id="p5-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · сфера</span><div class="wg-title">Визуализатор сферы (каркас)</div></div>'
+ '<div class="wg-help">Меняй радиус $R$ ползунком, вращай мышью или выбирай вид. После <b>4 разных значений $R$</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>$R$ (радиус):<b id="p5-iv1-R-v">2.0</b><input type="range" id="p5-iv1-R" min="1" max="4" step="0.1" value="2"></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="p5-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>$R=$<b id="p5-iv1-R-o">—</b></span>'
+ '<span>$d=2R=$<b id="p5-iv1-d-o">—</b></span>'
+ '<span>$S=4\\pi R^2\\approx$<b id="p5-iv1-S-o">—</b></span>'
+ '<span>$V=\\tfrac{4}{3}\\pi R^3\\approx$<b id="p5-iv1-V-o">—</b></span>'
+ '</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Разных $R$ изучено: <b id="p5-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Уравнение сферы и проверка точки === */
html += '<div class="wg" id="p5-iv2">'
+ '<div class="wg-header"><span class="wg-badge">уравнение</span><div class="wg-title">Уравнение сферы и проверка точки</div></div>'
+ '<div class="wg-help">Введи центр $C(a;b;c)$ и радиус $R$ — получи уравнение. Затем введи точку $M(x_0;y_0;z_0)$ — узнай, лежит ли она на сфере, внутри шара или вне.</div>'
+ '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:10px">'
+ '<p style="font-weight:700;margin-bottom:6px">Параметры сферы</p>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:8px">'
+ '<span>$a=$</span><input type="text" class="tinp" id="p5-iv2-a" value="0" style="width:60px">'
+ '<span>$b=$</span><input type="text" class="tinp" id="p5-iv2-b" value="0" style="width:60px">'
+ '<span>$c=$</span><input type="text" class="tinp" id="p5-iv2-c" value="0" style="width:60px">'
+ '<span>$R=$</span><input type="text" class="tinp" id="p5-iv2-R" value="5" style="width:60px">'
+ '<button class="btn primary" id="p5-iv2-show">Показать уравнение</button>'
+ '</div>'
+ '<div id="p5-iv2-eq" style="font-size:1rem;line-height:1.7;margin-top:6px"></div>'
+ '</div>'
+ '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px">'
+ '<p style="font-weight:700;margin-bottom:6px">Проверка точки $M(x_0;y_0;z_0)$</p>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:8px">'
+ '<span>$x_0=$</span><input type="text" class="tinp" id="p5-iv2-x0" value="3" style="width:60px">'
+ '<span>$y_0=$</span><input type="text" class="tinp" id="p5-iv2-y0" value="4" style="width:60px">'
+ '<span>$z_0=$</span><input type="text" class="tinp" id="p5-iv2-z0" value="0" style="width:60px">'
+ '<button class="btn primary" id="p5-iv2-check">Проверить точку</button>'
+ '</div>'
+ '<div id="p5-iv2-pt" style="font-size:.94rem;line-height:1.65;margin-top:6px"></div>'
+ '</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Сечение сферы (квикфайр 6) === */
html += '<div class="wg" id="p5-iv3">'
+ '<div class="wg-header"><span class="wg-badge">квикфайр · 6 заданий</span><div class="wg-title">Сечение сферы плоскостью</div></div>'
+ '<div class="wg-help">Сравни $d$ и $R$. Выбери, что получится при пересечении: окружность, точка (касание) или ничего.</div>'
+ '<div id="p5-iv3-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Верно: <b id="p5-iv3-score">0</b> / 6</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 4 — Тренажёр === */
html += '<div class="wg" id="p5-iv4">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 6 задач</span><div class="wg-title">Сечения, расстояния и уравнения</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$ для дробных значений.</div>'
+ '<div id="p5-iv4-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p5-iv4-score">0</b> / 6</div>'
+ '</div>';
html += secNav(null, 'p6');
html += readButton('p5');
box.innerHTML = html;
renderMath(box);
/* ====== JS-логика интерактивов ====== */
/* IV1 — 3D-визуализатор */
(function(){
if(!window.G3D) return;
const svg = document.getElementById('p5-iv1-svg');
const elR = document.getElementById('p5-iv1-R');
const vR = document.getElementById('p5-iv1-R-v');
const oR = document.getElementById('p5-iv1-R-o');
const oD = document.getElementById('p5-iv1-d-o');
const oS = document.getElementById('p5-iv1-S-o');
const oV = document.getElementById('p5-iv1-V-o');
const oCnt = document.getElementById('p5-iv1-cnt');
if(!svg) return;
const scene = G3D.createScene({W:480, H:400, scale:60, camDist:8, rotX:-0.35, rotY:0.7});
const seen = new Set();
let xpGiven = false;
const PI = 3.14;
function draw(){
const R = +elR.value;
vR.textContent = R.toFixed(1);
const sph = G3D.sphereWireframe(R, 6, 12);
const M = G3D.buildRotMatrix(scene);
svg.innerHTML = G3D.renderSphereWireframe(sph, M, scene);
oR.textContent = R.toFixed(1);
oD.textContent = (2*R).toFixed(1);
oS.textContent = (4*PI*R*R).toFixed(2);
oV.textContent = ((4/3)*PI*R*R*R).toFixed(2);
const key = R.toFixed(1);
seen.add(key);
oCnt.textContent = Math.min(seen.size, 4);
if(seen.size >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p5-iv1');
bumpProgress('p5', 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('p5-iv1');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
G3D.attachOrbit(svg, scene, draw);
elR.addEventListener('input', draw);
document.querySelectorAll('#p5-iv1 .g3d-tools .btn').forEach(function(b){
b.addEventListener('click', function(){ G3D.presetView(scene, b.dataset.view, draw); });
});
})();
/* IV2 — Уравнение сферы + проверка точки */
(function(){
let xpGiven = false, eqShown = false, ptChecked = false;
function parseNum(id){ const v = (document.getElementById(id).value||'').replace(',', '.').trim(); const x = parseFloat(v); return isFinite(x) ? x : NaN; }
function fmtSign(v, useVar){
// возвращает строку для "(x - a)" с учётом знака
if(v === 0) return useVar;
if(v > 0) return '('+useVar+' - '+fmt(v)+')';
return '('+useVar+' + '+fmt(-v)+')';
}
function maybeXp(){
if(eqShown && ptChecked && !xpGiven){
xpGiven = true;
addXp(10, 'p5-iv2');
bumpProgress('p5', 15);
}
}
document.getElementById('p5-iv2-show').addEventListener('click', function(){
const a = parseNum('p5-iv2-a'), b = parseNum('p5-iv2-b'), c = parseNum('p5-iv2-c'), R = parseNum('p5-iv2-R');
const out = document.getElementById('p5-iv2-eq');
if(!isFinite(a)||!isFinite(b)||!isFinite(c)||!isFinite(R)||R<=0){
out.innerHTML = '<span style="color:var(--bad)">&#10007; Введи корректные числа ($R>0$).</span>';
renderMath(out);
return;
}
const fx = fmtSign(a, 'x');
const fy = fmtSign(b, 'y');
const fz = fmtSign(c, 'z');
const R2 = R*R;
const eq = '$$' + fx + '^2 + ' + fy + '^2 + ' + fz + '^2 = ' + fmt(R2) + '$$';
out.innerHTML = '<p>Центр $C('+fmt(a)+';'+fmt(b)+';'+fmt(c)+')$, радиус $R='+fmt(R)+'$.</p>'
+ '<p>Уравнение сферы:</p>' + eq;
renderMath(out);
eqShown = true;
maybeXp();
});
document.getElementById('p5-iv2-check').addEventListener('click', function(){
const a = parseNum('p5-iv2-a'), b = parseNum('p5-iv2-b'), c = parseNum('p5-iv2-c'), R = parseNum('p5-iv2-R');
const x0 = parseNum('p5-iv2-x0'), y0 = parseNum('p5-iv2-y0'), z0 = parseNum('p5-iv2-z0');
const out = document.getElementById('p5-iv2-pt');
if(!isFinite(a)||!isFinite(b)||!isFinite(c)||!isFinite(R)||R<=0||!isFinite(x0)||!isFinite(y0)||!isFinite(z0)){
out.innerHTML = '<span style="color:var(--bad)">&#10007; Введи корректные числа.</span>';
renderMath(out);
return;
}
const dx = x0 - a, dy = y0 - b, dz = z0 - c;
const lhs = dx*dx + dy*dy + dz*dz;
const R2 = R*R;
const dist = Math.sqrt(lhs);
let verdict = '', color = '';
if(Math.abs(lhs - R2) < 1e-6){ verdict = 'лежит <b>на сфере</b>'; color = 'var(--ok)'; }
else if(lhs < R2){ verdict = 'лежит <b>внутри шара</b>'; color = '#2563eb'; }
else { verdict = 'лежит <b>вне шара</b>'; color = 'var(--warn)'; }
out.innerHTML = '<p>Подставляем $M('+fmt(x0)+';'+fmt(y0)+';'+fmt(z0)+')$:</p>'
+ '<p>$('+fmt(x0)+'-'+fmt(a)+')^2+('+fmt(y0)+'-'+fmt(b)+')^2+('+fmt(z0)+'-'+fmt(c)+')^2 = '
+ fmt(dx*dx)+'+'+fmt(dy*dy)+'+'+fmt(dz*dz)+' = '+fmt(lhs)+'$</p>'
+ '<p>Сравниваем с $R^2='+fmt(R2)+'$. Расстояние $|CM|='+fmt(+dist.toFixed(4))+'$.</p>'
+ '<p style="color:'+color+';font-weight:700">&#10003; Точка $M$ '+verdict+'.</p>';
renderMath(out);
ptChecked = true;
maybeXp();
});
})();
/* IV3 — Сечение сферы (квикфайр) */
(function(){
const tasks = [
{ R:5, d:3, a:'circle' },
{ R:5, d:5, a:'point' },
{ R:5, d:6, a:'none' },
{ R:10, d:0, a:'circle' },
{ R:3, d:3.5, a:'none' },
{ R:7, d:7, a:'point' }
];
const NAMES = {circle:'Окружность', point:'Точка', none:'Нет общих точек'};
const HINTS = {
circle: function(t){ const r = Math.sqrt(t.R*t.R - t.d*t.d); return '$d<R$, сечение — окружность радиуса $r=\\sqrt{'+t.R+'^2-'+t.d+'^2}='+fmt(+r.toFixed(4))+'$'+(t.d===0?' (большой круг)':'')+'.'; },
point: function(t){ return '$d=R='+t.R+'$, плоскость касается сферы — общая точка одна.'; },
none: function(t){ return '$d='+t.d+'>R='+t.R+'$, плоскость не пересекает сферу.'; }
};
const list = document.getElementById('p5-iv3-list');
const scoreEl = document.getElementById('p5-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:8px"><b>Задание '+(i+1)+'.</b> Сфера $R='+t.R+'$, расстояние от центра до плоскости $d='+t.d+'$. Что получится?</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="point">Точка</button>'
+ '<button class="btn" data-i="'+i+'" data-v="none">Нет общих точек</button>'
+ '</div>'
+ '<div class="feedback" id="p5-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, v = b.dataset.v, t = tasks[i];
const fb = document.getElementById('p5-iv3-fb-'+i);
if(solved.has(i)) return;
if(v === t.a){
feedback(fb, true, '&#10003; Верно — '+NAMES[t.a]+'. '+HINTS[t.a](t));
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p5-iv3');
bumpProgress('p5', 25);
}
} else {
feedback(fb, false, '&#10007; Не то. Сравни $d$ и $R$ внимательнее.');
}
});
});
})();
/* IV4 — Тренажёр */
(function(){
const tasks = [
{ q:'Сфера $R=5$. Радиус сечения плоскостью на расстоянии $d=3$ от центра: $r=\\,?$', a:4, tol:0.05 },
{ q:'Сфера с центром $C(2;3;-1)$, точка $M(5;7;3)$. Расстояние $|CM|=\\,?$ (точность 0,01)', a:6.40, tol:0.05 },
{ q:'Сфера $R=13$. Сечение — окружность радиуса $12$. Найти $d$ (расстояние от центра до плоскости).', a:5, tol:0.05 },
{ q:'К сфере $R=5$ построена касательная плоскость. Расстояние от центра до плоскости: $d=\\,?$', a:5, tol:0.05 },
{ q:'Точка $A(3;4;0)$ принадлежит сфере с центром в начале координат. Найти $R$.', a:5, tol:0.05 },
{ q:'Сфера $x^2+y^2+z^2=100$. Радиус большого круга: $R=\\,?$', a:10, tol:0.05 }
];
const list = document.getElementById('p5-iv4-list');
const scoreEl = document.getElementById('p5-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="p5-iv4-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p5-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('p5-iv4-inp-'+i);
const fb = document.getElementById('p5-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, 'p5-iv4');
bumpProgress('p5', 25);
setTimeout(function(){ achievement('p5_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
wireReadBtn('p5');
}
/* ===== § 6 «Шар» — Wave 2 ===== */
function buildP6(){
const box = document.getElementById('p6-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Площадь поверхности и объём шара', '§ 6.1',
'<p><b>Шар</b> — тело, ограниченное сферой. Это множество точек пространства, для которых $|OM|\\le R$.</p>'
+ '<p>Все точки шара удалены от центра $O$ не более чем на $R$. Поверхность шара — сама сфера.</p>'
+ '<p><b>Основные формулы</b> для шара радиуса $R$:</p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{\\text{сферы}}=4\\pi R^2 \\qquad V_{\\text{шара}}=\\dfrac{4}{3}\\pi R^3$$</p>'
+ '<p>Формула объёма обосновывается <b>методом Кавальери</b>: сравнивая шар с цилиндром, из которого вырезан конус, получаем те же объёмы у равных поперечных сечений на одной высоте.</p>'
+ '<details class="spoiler"><summary>Пример: $R=3$ — численное совпадение $S$ и $V$</summary><div class="spoiler-body">'
+ '<p>$S=4\\pi\\cdot 9=36\\pi\\approx 113{,}10$.</p>'
+ '<p>$V=\\dfrac{4}{3}\\pi\\cdot 27=36\\pi\\approx 113{,}10$.</p>'
+ '<p>Численно $S$ и $V$ совпадают — но только при $R=3$. Это лишь совпадение единиц измерения (одно — площадь, другое — объём).</p>'
+ '</div></details>'
+ '<details class="spoiler"><summary>Пример: связь радиуса и объёма</summary><div class="spoiler-body">'
+ '<p>Если радиус увеличить в $2$ раза, то площадь сферы вырастет в $4$ раза, а объём — в $8$ раз.</p>'
+ '<p>$\\dfrac{V_2}{V_1}=\\dfrac{(2R)^3}{R^3}=8$, $\\dfrac{S_2}{S_1}=\\dfrac{(2R)^2}{R^2}=4$.</p>'
+ '</div></details>');
html += makeCard('rule', 'Шаровой сегмент, сектор, слой', '§ 6.2',
'<p><b>Шаровой сегмент</b> — часть шара, отсекаемая плоскостью. <b>Высота сегмента</b> $h$ — расстояние от секущей плоскости до самой удалённой точки сегмента.</p>'
+ '<p style="text-align:center;margin:8px 0">$$V_{\\text{сегм}}=\\dfrac{\\pi h^2(3R-h)}{3}, \\qquad S_{\\text{сфер.часть}}=2\\pi R h$$</p>'
+ '<p>Радиус основания сегмента: $r=\\sqrt{2Rh-h^2}$ (по теореме Пифагора в осевом сечении).</p>'
+ '<p><b>Шаровой сектор</b> — тело, состоящее из шарового сегмента и конуса с вершиной в центре шара и основанием — кругом сегмента.</p>'
+ '<p style="text-align:center;margin:8px 0">$$V_{\\text{сект}}=\\dfrac{2}{3}\\pi R^2 h$$</p>'
+ '<p><b>Шаровой слой</b> — часть шара между двумя параллельными плоскостями. $r_1$, $r_2$ — радиусы оснований, $h$ — расстояние между плоскостями.</p>'
+ '<p style="text-align:center;margin:8px 0">$$V_{\\text{слой}}=\\dfrac{\\pi h}{6}\\bigl(3r_1^2+3r_2^2+h^2\\bigr), \\qquad S_{\\text{сфер.часть}}=2\\pi R h$$</p>'
+ '<p style="background:var(--sec-acc-soft,var(--pri-soft));border-left:4px solid var(--sec-acc,var(--pri));padding:8px 12px;border-radius:6px;margin:8px 0"><b>Замечательно:</b> формула $S=2\\pi R h$ работает <b>и для сегмента, и для шарового пояса (слоя)</b> — площадь сферического пояса между параллельными плоскостями зависит только от его высоты $h$ и от $R$ сферы.</p>'
+ '<details class="spoiler"><summary>Пример: $R=5$, $h=2$ (сегмент)</summary><div class="spoiler-body">'
+ '<p>$V_{\\text{сегм}}=\\dfrac{\\pi\\cdot 4\\cdot(15-2)}{3}=\\dfrac{52\\pi}{3}\\approx 54{,}45$.</p>'
+ '<p>$S_{\\text{сфер.часть}}=2\\pi\\cdot 5\\cdot 2=20\\pi\\approx 62{,}83$.</p>'
+ '<p>Радиус основания: $r=\\sqrt{2\\cdot 5\\cdot 2-4}=\\sqrt{16}=4$.</p>'
+ '</div></details>');
html += makeCard('example', 'Вписанные и описанные шары', '§ 6.3',
'<p><b>Шар вписан</b> в многогранник, если он касается всех его граней изнутри.</p>'
+ '<p><b>Шар описан</b> около многогранника, если все его вершины лежат на сфере.</p>'
+ '<p style="font-weight:700;margin-top:10px">Куб и шар.</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Шар вписан в куб</b> со стороной $a$: касается каждой грани в её центре. Радиус: $r_{\\text{вп}}=\\dfrac{a}{2}$.</li>'
+ '<li><b>Шар описан около куба</b> со стороной $a$: все 8 вершин на сфере. Диаметр шара равен диагонали куба $a\\sqrt{3}$. Радиус: $R_{\\text{оп}}=\\dfrac{a\\sqrt{3}}{2}$.</li>'
+ '<li>Отношение объёмов: $\\dfrac{V_{\\text{шара впис.}}}{V_{\\text{куба}}}=\\dfrac{\\tfrac{4}{3}\\pi(a/2)^3}{a^3}=\\dfrac{\\pi}{6}\\approx 0{,}524$.</li>'
+ '</ul>'
+ '<p style="font-weight:700;margin-top:6px">Цилиндр и шар.</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Шар вписан в цилиндр</b>, если касается боковой поверхности и обоих оснований. Тогда $h_{\\text{цил}}=2R$, а радиусы шара и цилиндра совпадают. Отношение: $\\dfrac{V_{\\text{шара}}}{V_{\\text{цил}}}=\\dfrac{2}{3}$ (теорема Архимеда).</li>'
+ '<li><b>Шар описан около цилиндра</b> с радиусом $R$ и высотой $h$: $R_{\\text{шар}}=\\sqrt{R^2+\\left(\\dfrac{h}{2}\\right)^2}$ (теорема Пифагора).</li>'
+ '</ul>'
+ '<details class="spoiler"><summary>Пример: куб со стороной $a=4$</summary><div class="spoiler-body">'
+ '<p>Вписанный шар: $r=2$, $V=\\dfrac{4}{3}\\pi\\cdot 8=\\dfrac{32\\pi}{3}\\approx 33{,}51$.</p>'
+ '<p>Описанный шар: $R=\\dfrac{4\\sqrt 3}{2}=2\\sqrt 3\\approx 3{,}46$.</p>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — 3D-визуализатор шара с сегментами === */
html += '<div class="wg" id="p6-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · шар + сегмент</span><div class="wg-title">Визуализатор шара и шарового сегмента</div></div>'
+ '<div class="wg-help">Меняй радиус $R$ и высоту сегмента $h$. Сегмент выделяется сверху. После <b>4 разных пар $(R,h)$</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>$R$ (радиус):<b id="p6-iv1-R-v">2.0</b><input type="range" id="p6-iv1-R" min="1" max="4" step="0.1" value="2"></label>'
+ '<label>$h$ (высота сегмента):<b id="p6-iv1-h-v">1.0</b><input type="range" id="p6-iv1-h" min="0" max="4" step="0.1" value="1"></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="p6-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>$S_{\\text{сферы}}=4\\pi R^2\\approx$<b id="p6-iv1-S-o">—</b></span>'
+ '<span>$V_{\\text{шара}}=\\tfrac{4}{3}\\pi R^3\\approx$<b id="p6-iv1-V-o">—</b></span>'
+ '<span>$V_{\\text{сегм}}\\approx$<b id="p6-iv1-Vseg-o">—</b></span>'
+ '<span>$S_{\\text{сфер.сегм}}=2\\pi R h\\approx$<b id="p6-iv1-Sseg-o">—</b></span>'
+ '</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Разных пар $(R,h)$ изучено: <b id="p6-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Калькулятор шара/сегмента/слоя === */
html += '<div class="wg" id="p6-iv2">'
+ '<div class="wg-header"><span class="wg-badge">калькулятор</span><div class="wg-title">Калькулятор шара, сегмента и слоя</div></div>'
+ '<div class="wg-help">Выбери режим, введи параметры и получи $S$, $V$ и другие характеристики.</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px">'
+ '<button class="btn primary" data-mode="ball" id="p6-iv2-mb">Шар целиком</button>'
+ '<button class="btn" data-mode="seg" id="p6-iv2-ms">Сегмент</button>'
+ '<button class="btn" data-mode="lay" id="p6-iv2-ml">Слой</button>'
+ '</div>'
+ '<div id="p6-iv2-form" style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px"></div>'
+ '<div id="p6-iv2-out" style="font-size:.95rem;line-height:1.7;margin-top:8px"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Вписан или описан? === */
html += '<div class="wg" id="p6-iv3">'
+ '<div class="wg-header"><span class="wg-badge">квикфайр · 6 заданий</span><div class="wg-title">Вписан или описан?</div></div>'
+ '<div class="wg-help">Прочитай условие и выбери, какое отношение шара и многогранника описано.</div>'
+ '<div id="p6-iv3-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Верно: <b id="p6-iv3-score">0</b> / 6</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 4 — Тренажёр === */
html += '<div class="wg" id="p6-iv4">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 6 задач</span><div class="wg-title">Шар, сегмент, вписанные и описанные тела</div></div>'
+ '<div class="wg-help">Используй $\\pi\\approx 3{,}14$. Допуск $\\pm 0{,}5$ для больших значений, $\\pm 0{,}05$ — для остальных.</div>'
+ '<div id="p6-iv4-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p6-iv4-score">0</b> / 6</div>'
+ '</div>';
html += secNav('p5', 'p7');
html += readButton('p6');
box.innerHTML = html;
renderMath(box);
/* ====== JS-логика интерактивов ====== */
/* IV1 — 3D-визуализатор шара с сегментом */
(function(){
if(!window.G3D) return;
const svg = document.getElementById('p6-iv1-svg');
const elR = document.getElementById('p6-iv1-R');
const elH = document.getElementById('p6-iv1-h');
const vR = document.getElementById('p6-iv1-R-v');
const vH = document.getElementById('p6-iv1-h-v');
const oS = document.getElementById('p6-iv1-S-o');
const oV = document.getElementById('p6-iv1-V-o');
const oVseg = document.getElementById('p6-iv1-Vseg-o');
const oSseg = document.getElementById('p6-iv1-Sseg-o');
const oCnt = document.getElementById('p6-iv1-cnt');
if(!svg) return;
const scene = G3D.createScene({W:480, H:400, scale:60, camDist:8, rotX:-0.35, rotY:0.7});
const seen = new Set();
let xpGiven = false;
const PI = 3.14;
function projector(v){
if (scene.proj === 'iso') return G3D.projectIso(v, scene.cx, scene.cy, scene.scale);
return G3D.projectPersp(v, scene.camDist, scene.cx, scene.cy, scene.scale);
}
function segmentOverlay(R, h, M){
// Сегмент сверху: от высоты y0 = R - h до y = R
if(h <= 0) return '';
const hClamp = Math.min(h, 2*R);
const y0 = R - hClamp; // y координата секущей плоскости
// 1) Окружность секущего круга — эллипс в проекции
const rBase = Math.sqrt(Math.max(0, 2*R*hClamp - hClamp*hClamp));
const N = 64;
const ringPts = [];
for(let i=0; i<=N; i++){
const t = 2*Math.PI*i/N;
ringPts.push({x: rBase*Math.cos(t), y: y0, z: rBase*Math.sin(t)});
}
const ringRot = ringPts.map(p => G3D.vApply(M, p)).map(projector);
let path = '';
for(let i=0; i<ringRot.length; i++){
const p = ringRot[i];
if(!p) continue;
path += (i===0?'M':'L') + p.x.toFixed(1) + ',' + p.y.toFixed(1) + ' ';
}
let out = '<path d="'+path+'Z" fill="rgba(244,114,182,.32)" stroke="#be185d" stroke-width="1.8" stroke-dasharray="4 3"/>';
// 2) Полупрозрачная "шапка" сегмента — несколько горизонтальных эллипсов выше плоскости
const SLICES = 6;
for(let k=1; k<=SLICES; k++){
const yk = y0 + hClamp * k / SLICES;
const rk = Math.sqrt(Math.max(0, R*R - yk*yk));
if(rk < 1e-3) continue;
const pts = [];
for(let i=0; i<=N; i++){
const t = 2*Math.PI*i/N;
pts.push({x: rk*Math.cos(t), y: yk, z: rk*Math.sin(t)});
}
const proj = pts.map(p => G3D.vApply(M, p)).map(projector);
let p2 = '';
for(let i=0; i<proj.length; i++){
const p = proj[i]; if(!p) continue;
p2 += (i===0?'M':'L') + p.x.toFixed(1) + ',' + p.y.toFixed(1) + ' ';
}
out += '<path d="'+p2+'" fill="none" stroke="rgba(244,114,182,.55)" stroke-width="1"/>';
}
return out;
}
function draw(){
const R = +elR.value;
let h = +elH.value;
if(h > 2*R) h = 2*R;
// Обновляем верхнюю границу слайдера h по текущему R
elH.max = (2*R).toFixed(1);
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
const sph = G3D.sphereWireframe(R, 6, 12);
const M = G3D.buildRotMatrix(scene);
const wire = G3D.renderSphereWireframe(sph, M, scene);
const seg = segmentOverlay(R, h, M);
svg.innerHTML = wire + seg;
oS.textContent = (4*PI*R*R).toFixed(2);
oV.textContent = ((4/3)*PI*R*R*R).toFixed(2);
oVseg.textContent = (PI*h*h*(3*R - h)/3).toFixed(2);
oSseg.textContent = (2*PI*R*h).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, 'p6-iv1');
bumpProgress('p6', 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('p6-iv1');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
G3D.attachOrbit(svg, scene, draw);
elR.addEventListener('input', draw);
elH.addEventListener('input', draw);
document.querySelectorAll('#p6-iv1 .g3d-tools .btn').forEach(function(b){
b.addEventListener('click', function(){ G3D.presetView(scene, b.dataset.view, draw); });
});
})();
/* IV2 — Калькулятор */
(function(){
const formBox = document.getElementById('p6-iv2-form');
const outBox = document.getElementById('p6-iv2-out');
let mode = 'ball';
let xpGiven = false;
const seenModes = new Set();
const PI = 3.14;
function setActive(){
['mb','ms','ml'].forEach(function(s, i){
const b = document.getElementById('p6-iv2-'+s);
const m = ['ball','seg','lay'][i];
if(!b) return;
b.classList.toggle('primary', m === mode);
});
}
function parseNum(id){ const el = document.getElementById(id); if(!el) return NaN; const v = (el.value||'').replace(',', '.').trim(); const x = parseFloat(v); return isFinite(x) ? x : NaN; }
function render(){
let h = '';
if(mode === 'ball'){
h = '<p style="font-weight:700;margin-bottom:6px">Шар целиком</p>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
+ '<span>$R=$</span><input type="text" class="tinp" id="p6-iv2-R" value="5" style="width:80px">'
+ '<button class="btn primary" id="p6-iv2-calc">Вычислить</button>'
+ '</div>';
} else if(mode === 'seg'){
h = '<p style="font-weight:700;margin-bottom:6px">Шаровой сегмент</p>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
+ '<span>$R=$</span><input type="text" class="tinp" id="p6-iv2-R" value="5" style="width:70px">'
+ '<span>$h=$</span><input type="text" class="tinp" id="p6-iv2-h" value="2" style="width:70px">'
+ '<button class="btn primary" id="p6-iv2-calc">Вычислить</button>'
+ '</div>';
} else {
h = '<p style="font-weight:700;margin-bottom:6px">Шаровой слой</p>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
+ '<span>$R=$</span><input type="text" class="tinp" id="p6-iv2-R" value="10" style="width:60px">'
+ '<span>$r_1=$</span><input type="text" class="tinp" id="p6-iv2-r1" value="6" style="width:60px">'
+ '<span>$r_2=$</span><input type="text" class="tinp" id="p6-iv2-r2" value="8" style="width:60px">'
+ '<span>$h=$</span><input type="text" class="tinp" id="p6-iv2-h" value="4" style="width:60px">'
+ '<button class="btn primary" id="p6-iv2-calc">Вычислить</button>'
+ '</div>';
}
formBox.innerHTML = h;
renderMath(formBox);
const btn = document.getElementById('p6-iv2-calc');
if(btn) btn.addEventListener('click', calc);
}
function calc(){
let out = '';
if(mode === 'ball'){
const R = parseNum('p6-iv2-R');
if(!isFinite(R) || R<=0){ outBox.innerHTML = '<span style="color:var(--bad)">&#10007; Введи $R>0$.</span>'; renderMath(outBox); return; }
const S = 4*PI*R*R;
const V = (4/3)*PI*R*R*R;
const C = 2*PI*R;
out = '<p>Шар радиуса $R='+fmt(R)+'$:</p>'
+ '<p>$S_{\\text{сферы}}=4\\pi R^2=4\\cdot 3{,}14\\cdot '+fmt(R*R)+'\\approx '+fmt(+S.toFixed(2))+'$</p>'
+ '<p>$V_{\\text{шара}}=\\dfrac{4}{3}\\pi R^3\\approx '+fmt(+V.toFixed(2))+'$</p>'
+ '<p>Длина большой окружности: $C=2\\pi R\\approx '+fmt(+C.toFixed(2))+'$</p>'
+ '<p>$\\dfrac{V}{S}=\\dfrac{R}{3}='+fmt(+(R/3).toFixed(4))+'$</p>';
} else if(mode === 'seg'){
const R = parseNum('p6-iv2-R');
const h = parseNum('p6-iv2-h');
if(!isFinite(R) || R<=0 || !isFinite(h) || h<=0 || h>2*R){ outBox.innerHTML = '<span style="color:var(--bad)">&#10007; Введи $R>0$ и $0<h\\le 2R$.</span>'; renderMath(outBox); return; }
const V = PI*h*h*(3*R - h)/3;
const S = 2*PI*R*h;
const r = Math.sqrt(Math.max(0, 2*R*h - h*h));
out = '<p>Шаровой сегмент $R='+fmt(R)+'$, $h='+fmt(h)+'$:</p>'
+ '<p>$V_{\\text{сегм}}=\\dfrac{\\pi h^2(3R-h)}{3}=\\dfrac{3{,}14\\cdot '+fmt(h*h)+'\\cdot '+fmt(3*R-h)+'}{3}\\approx '+fmt(+V.toFixed(2))+'$</p>'
+ '<p>$S_{\\text{сфер.часть}}=2\\pi R h=2\\cdot 3{,}14\\cdot '+fmt(R)+'\\cdot '+fmt(h)+'\\approx '+fmt(+S.toFixed(2))+'$</p>'
+ '<p>Радиус основания сегмента: $r=\\sqrt{2Rh-h^2}=\\sqrt{'+fmt(2*R*h-h*h)+'}\\approx '+fmt(+r.toFixed(4))+'$</p>';
} else {
const R = parseNum('p6-iv2-R');
const r1 = parseNum('p6-iv2-r1');
const r2 = parseNum('p6-iv2-r2');
const h = parseNum('p6-iv2-h');
if(!isFinite(R) || R<=0 || !isFinite(r1) || r1<0 || !isFinite(r2) || r2<0 || !isFinite(h) || h<=0){ outBox.innerHTML = '<span style="color:var(--bad)">&#10007; Введи корректные значения ($R>0$, $r_1,r_2\\ge 0$, $h>0$).</span>'; renderMath(outBox); return; }
const V = PI*h*(3*r1*r1 + 3*r2*r2 + h*h)/6;
const S = 2*PI*R*h;
out = '<p>Шаровой слой $R='+fmt(R)+'$, $r_1='+fmt(r1)+'$, $r_2='+fmt(r2)+'$, $h='+fmt(h)+'$:</p>'
+ '<p>$V_{\\text{слой}}=\\dfrac{\\pi h(3r_1^2+3r_2^2+h^2)}{6}\\approx '+fmt(+V.toFixed(2))+'$</p>'
+ '<p>$S_{\\text{сфер.часть}}=2\\pi R h\\approx '+fmt(+S.toFixed(2))+'$</p>'
+ '<p>Площадь полной боковой поверхности слоя зависит только от $R$ и $h$ — это и есть свойство сферического пояса.</p>';
}
outBox.innerHTML = out;
renderMath(outBox);
seenModes.add(mode);
if(seenModes.size >= 3 && !xpGiven){
xpGiven = true;
addXp(10, 'p6-iv2');
bumpProgress('p6', 15);
}
}
['mb','ms','ml'].forEach(function(s, i){
const b = document.getElementById('p6-iv2-'+s);
const m = ['ball','seg','lay'][i];
if(!b) return;
b.addEventListener('click', function(){ mode = m; setActive(); render(); outBox.innerHTML=''; });
});
setActive();
render();
})();
/* IV3 — Вписан или описан? */
(function(){
const tasks = [
{ q:'Шар касается всех 6 граней куба, его центр совпадает с центром куба.', a:'in', h:'Если шар касается всех граней изнутри — он <b>вписан</b> в куб.' },
{ q:'Все 8 вершин куба лежат на сфере с центром в центре куба.', a:'out', h:'Все вершины на сфере — шар <b>описан около</b> куба.' },
{ q:'Радиус шара равен $\\dfrac{a}{2}$, где $a$ — сторона куба.', a:'in', h:'$r=a/2$ — это вписанный шар (касается граней в центре).' },
{ q:'Радиус шара равен $\\dfrac{a\\sqrt{3}}{2}$, где $a$ — сторона куба.', a:'out', h:'$R=\\dfrac{a\\sqrt 3}{2}$ = половина диагонали — шар <b>описан</b>.' },
{ q:'Шар касается боковой поверхности цилиндра и обоих его оснований.', a:'in', h:'Касается всей поверхности изнутри — шар <b>вписан</b> в цилиндр ($h=2R$).' },
{ q:'Все 4 вершины правильного тетраэдра лежат на сфере.', a:'out', h:'Все вершины на сфере — сфера <b>описана около</b> тетраэдра.' }
];
const list = document.getElementById('p6-iv3-list');
const scoreEl = document.getElementById('p6-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:8px"><b>Задание '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap">'
+ '<button class="btn" data-i="'+i+'" data-v="in">Шар вписан</button>'
+ '<button class="btn" data-i="'+i+'" data-v="out">Шар описан</button>'
+ '</div>'
+ '<div class="feedback" id="p6-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, v = b.dataset.v, t = tasks[i];
const fb = document.getElementById('p6-iv3-fb-'+i);
if(solved.has(i)) return;
if(v === t.a){
feedback(fb, true, '&#10003; Верно. '+t.h);
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p6-iv3');
bumpProgress('p6', 25);
}
} else {
feedback(fb, false, '&#10007; Не то. Подумай: шар <i>вписан</i> = касается граней; <i>описан</i> = вершины на сфере.');
}
});
});
})();
/* IV4 — Тренажёр */
(function(){
const tasks = [
{ q:'Шар $R=5$. Найди $V$ (используй $\\pi\\approx 3{,}14$).', a:523.33, tol:0.6 },
{ q:'Шар $R=3$. Найди $S$ (используй $\\pi\\approx 3{,}14$).', a:113.04, tol:0.1 },
{ q:'Шар вписан в куб со стороной $a=6$. Найди $V_{\\text{шара}}$ ($\\pi\\approx 3{,}14$).', a:113.04, tol:0.1 },
{ q:'Шар описан около куба со стороной $a=2$. Найди радиус шара $R$ (с точностью до 0,01).', a:1.73, tol:0.05 },
{ q:'Шаровой сегмент: $R=5$, $h=2$. Найди $V_{\\text{сегм}}$ ($\\pi\\approx 3{,}14$).', a:54.43, tol:0.1 },
{ q:'Шар $R=10$, сферический пояс высотой $h=4$. Найди $S$ пояса ($\\pi\\approx 3{,}14$).', a:251.2, tol:0.3 }
];
const list = document.getElementById('p6-iv4-list');
const scoreEl = document.getElementById('p6-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="p6-iv4-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p6-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('p6-iv4-inp-'+i);
const fb = document.getElementById('p6-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, 'p6-iv4');
bumpProgress('p6', 25);
setTimeout(function(){ achievement('p6_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай с $\\pi\\approx 3{,}14$.');
}
});
});
})();
wireReadBtn('p6');
}
/* ===== § 7 «Правильные многогранники» — Wave 3 ===== */
/* --- Платоновы тела: вершины и рёбра для каркасного рендера --- */
function platonicMesh(kind, a){
// Возвращает {verts:[{x,y,z}], edges:[[i,j],...], faces:[[i0,i1,...],...]}
// Все тела отмасштабированы так, чтобы длина ребра была равна a.
const PHI = (1 + Math.sqrt(5)) / 2;
if(kind === 'tetra'){
// (1,1,1),(1,-1,-1),(-1,1,-1),(-1,-1,1) — ребро = 2*sqrt(2)
const k = a / (2 * Math.sqrt(2));
const v = [
{x: 1, y: 1, z: 1},
{x: 1, y:-1, z:-1},
{x:-1, y: 1, z:-1},
{x:-1, y:-1, z: 1}
].map(p => ({x:p.x*k, y:p.y*k, z:p.z*k}));
const edges = [[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]];
const faces = [[0,1,2],[0,3,1],[0,2,3],[1,3,2]];
return {verts:v, edges, faces};
}
if(kind === 'cube'){
// (±1,±1,±1), ребро = 2
const k = a / 2;
const v = [];
for(let sx=-1; sx<=1; sx+=2)
for(let sy=-1; sy<=1; sy+=2)
for(let sz=-1; sz<=1; sz+=2)
v.push({x:sx*k, y:sy*k, z:sz*k});
// Индексация: idx = (sx+1)/2*4 + (sy+1)/2*2 + (sz+1)/2
// 0=(-,-,-),1=(-,-,+),2=(-,+,-),3=(-,+,+),4=(+,-,-),5=(+,-,+),6=(+,+,-),7=(+,+,+)
const edges = [
[0,1],[0,2],[0,4],[1,3],[1,5],[2,3],[2,6],[3,7],
[4,5],[4,6],[5,7],[6,7]
];
const faces = [
[0,1,3,2], [4,6,7,5], // x=-1, x=+1
[0,4,5,1], [2,3,7,6], // y=-1, y=+1
[0,2,6,4], [1,5,7,3] // z=-1, z=+1
];
return {verts:v, edges, faces};
}
if(kind === 'octa'){
// (±1,0,0),(0,±1,0),(0,0,±1), ребро = sqrt(2)
const k = a / Math.sqrt(2);
const v = [
{x: 1,y: 0,z: 0}, // 0 +x
{x:-1,y: 0,z: 0}, // 1 -x
{x: 0,y: 1,z: 0}, // 2 +y
{x: 0,y:-1,z: 0}, // 3 -y
{x: 0,y: 0,z: 1}, // 4 +z
{x: 0,y: 0,z:-1} // 5 -z
].map(p => ({x:p.x*k, y:p.y*k, z:p.z*k}));
const edges = [
[0,2],[0,3],[0,4],[0,5],
[1,2],[1,3],[1,4],[1,5],
[2,4],[2,5],[3,4],[3,5]
];
const faces = [
[0,2,4],[0,4,3],[0,3,5],[0,5,2],
[1,4,2],[1,3,4],[1,5,3],[1,2,5]
];
return {verts:v, edges, faces};
}
if(kind === 'icosa'){
// (0,±1,±phi),(±1,±phi,0),(±phi,0,±1). Ребро = 2.
const k = a / 2;
const raw = [
// (0,±1,±phi)
{x: 0,y: 1,z: PHI}, // 0
{x: 0,y: 1,z:-PHI}, // 1
{x: 0,y:-1,z: PHI}, // 2
{x: 0,y:-1,z:-PHI}, // 3
// (±1,±phi,0)
{x: 1,y: PHI,z: 0}, // 4
{x: 1,y:-PHI,z: 0}, // 5
{x:-1,y: PHI,z: 0}, // 6
{x:-1,y:-PHI,z: 0}, // 7
// (±phi,0,±1)
{x: PHI,y: 0,z: 1}, // 8
{x: PHI,y: 0,z:-1}, // 9
{x:-PHI,y: 0,z: 1}, //10
{x:-PHI,y: 0,z:-1} //11
];
const v = raw.map(p => ({x:p.x*k, y:p.y*k, z:p.z*k}));
// Рёбра — пары вершин с расстоянием = 2 (до масштабирования). Сгенерируем.
const edges = [];
const targ = 2; // длина ребра в исходных координатах
for(let i=0; i<raw.length; i++){
for(let j=i+1; j<raw.length; j++){
const dx=raw[i].x-raw[j].x, dy=raw[i].y-raw[j].y, dz=raw[i].z-raw[j].z;
const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
if(Math.abs(d - targ) < 1e-3) edges.push([i,j]);
}
}
// Грани — треугольники (i,j,k), где все три пары — рёбра.
const ES = new Set(edges.map(e => e[0]+'_'+e[1]));
function has(a,b){ return ES.has((a<b?a+'_'+b:b+'_'+a)); }
const faces = [];
for(let i=0; i<raw.length; i++){
for(let j=i+1; j<raw.length; j++){
if(!has(i,j)) continue;
for(let k2=j+1; k2<raw.length; k2++){
if(has(i,k2) && has(j,k2)) faces.push([i,j,k2]);
}
}
}
return {verts:v, edges, faces};
}
if(kind === 'dodeca'){
// Вершины: (±1,±1,±1), (0,±1/phi,±phi), (±1/phi,±phi,0), (±phi,0,±1/phi).
// Длина ребра = 2/phi.
const INV = 1 / PHI;
const raw = [];
// куб (±1,±1,±1) → 8
for(let sx=-1; sx<=1; sx+=2)
for(let sy=-1; sy<=1; sy+=2)
for(let sz=-1; sz<=1; sz+=2)
raw.push({x:sx, y:sy, z:sz});
// (0,±1/phi,±phi) → 4
for(let sy=-1; sy<=1; sy+=2)
for(let sz=-1; sz<=1; sz+=2)
raw.push({x:0, y:sy*INV, z:sz*PHI});
// (±1/phi,±phi,0) → 4
for(let sx=-1; sx<=1; sx+=2)
for(let sy=-1; sy<=1; sy+=2)
raw.push({x:sx*INV, y:sy*PHI, z:0});
// (±phi,0,±1/phi) → 4
for(let sx=-1; sx<=1; sx+=2)
for(let sz=-1; sz<=1; sz+=2)
raw.push({x:sx*PHI, y:0, z:sz*INV});
const targ = 2 / PHI;
const k = a / targ;
const v = raw.map(p => ({x:p.x*k, y:p.y*k, z:p.z*k}));
const edges = [];
for(let i=0; i<raw.length; i++){
for(let j=i+1; j<raw.length; j++){
const dx=raw[i].x-raw[j].x, dy=raw[i].y-raw[j].y, dz=raw[i].z-raw[j].z;
const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
if(Math.abs(d - targ) < 1e-3) edges.push([i,j]);
}
}
return {verts:v, edges, faces:[]};
}
return {verts:[], edges:[], faces:[]};
}
/* Рендер каркаса по {verts,edges} через G3D */
function renderWireframe(mesh, M, scene, opts){
opts = opts || {};
const strokeFront = opts.strokeFront || '#7c3aed';
const strokeBack = opts.strokeBack || '#c4b5fd';
const projector = (scene.proj === 'iso')
? v => G3D.projectIso(v, scene.cx, scene.cy, scene.scale)
: v => G3D.projectPersp(v, scene.camDist, scene.cx, scene.cy, scene.scale);
const rotated = mesh.verts.map(v => G3D.vApply(M, v));
const proj = rotated.map(projector);
let out = '';
// Грани (полупрозрачная заливка) — если есть
if(mesh.faces && mesh.faces.length){
const fd = mesh.faces.map(face => {
let avgZ = 0;
for(let i=0; i<face.length; i++) avgZ += rotated[face[i]].z;
avgZ /= face.length;
// нормаль — по первым трём вершинам
let visible = true;
if(face.length >= 3){
const v0 = rotated[face[0]], v1 = rotated[face[1]], v2 = rotated[face[2]];
const e1 = G3D.vSub(v1, v0), e2 = G3D.vSub(v2, v0);
const n = G3D.vCross(e1, e2);
visible = n.z > -1e-6;
}
return {face, avgZ, visible};
});
fd.sort((a,b)=>a.avgZ-b.avgZ);
for(const f of fd){
if(!f.visible) continue;
let anyMissing = false;
const pts = [];
for(const idx of f.face){
const p = proj[idx]; if(!p){ anyMissing=true; break; }
pts.push(p.x.toFixed(1)+','+p.y.toFixed(1));
}
if(anyMissing) continue;
out += '<polygon points="'+pts.join(' ')+'" fill="rgba(237,233,254,.45)" stroke="none"/>';
}
}
// Рёбра
for(const e of mesh.edges){
const a = proj[e[0]], b = proj[e[1]];
if(!a || !b) continue;
const za = rotated[e[0]].z, zb = rotated[e[1]].z;
const midZ = (za + zb) / 2;
const front = midZ > -1e-6;
const col = front ? strokeFront : strokeBack;
const w = front ? 2 : 1.1;
const dash = front ? '' : ' stroke-dasharray="4 3"';
out += '<line x1="'+a.x.toFixed(1)+'" y1="'+a.y.toFixed(1)+'" x2="'+b.x.toFixed(1)+'" y2="'+b.y.toFixed(1)+'" stroke="'+col+'" stroke-width="'+w+'" stroke-linecap="round" opacity="'+(front?'1':'.55')+'"'+dash+'/>';
}
// Вершины
for(const p of proj){
if(!p) continue;
out += '<circle cx="'+p.x.toFixed(1)+'" cy="'+p.y.toFixed(1)+'" r="2.5" fill="#3b0764"/>';
}
return out;
}
/* Свойства платоновых тел */
const PLATONIC_INFO = {
tetra: {name:'Тетраэдр', F:4, V:4, E:6, vAtVertex:3, faceShape:'△',
volume: a => a*a*a*Math.sqrt(2)/12,
area: a => a*a*Math.sqrt(3),
volFmt: 'V = \\dfrac{a^3\\sqrt{2}}{12}',
areaFmt:'S = a^2\\sqrt{3}'},
cube: {name:'Куб', F:6, V:8, E:12, vAtVertex:3, faceShape:'□',
volume: a => a*a*a,
area: a => 6*a*a,
volFmt: 'V = a^3',
areaFmt:'S = 6a^2'},
octa: {name:'Октаэдр', F:8, V:6, E:12, vAtVertex:4, faceShape:'△',
volume: a => a*a*a*Math.sqrt(2)/3,
area: a => 2*a*a*Math.sqrt(3),
volFmt: 'V = \\dfrac{a^3\\sqrt{2}}{3}',
areaFmt:'S = 2a^2\\sqrt{3}'},
dodeca: {name:'Додекаэдр', F:12, V:20, E:30, vAtVertex:3, faceShape:'⬠',
volume: a => (15 + 7*Math.sqrt(5)) * a*a*a / 4,
area: a => 3*a*a*Math.sqrt(25 + 10*Math.sqrt(5)),
volFmt: 'V = \\dfrac{(15+7\\sqrt{5})\\,a^3}{4}',
areaFmt:'S = 3a^2\\sqrt{25+10\\sqrt{5}}'},
icosa: {name:'Икосаэдр', F:20, V:12, E:30, vAtVertex:5, faceShape:'△',
volume: a => 5 * (3 + Math.sqrt(5)) * a*a*a / 12,
area: a => 5*a*a*Math.sqrt(3),
volFmt: 'V = \\dfrac{5(3+\\sqrt{5})\\,a^3}{12}',
areaFmt:'S = 5a^2\\sqrt{3}'}
};
function buildP7(){
const box = document.getElementById('p7-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Определение и таблица 5 тел', '§ 7.1',
'<p><b>Правильный многогранник</b> — выпуклый многогранник, у которого:</p>'
+ '<ol style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>Все грани — <b>конгруэнтные правильные многоугольники</b>.</li>'
+ '<li>Все двугранные углы между соседними гранями равны.</li>'
+ '<li>В каждой вершине сходится одинаковое число рёбер.</li>'
+ '</ol>'
+ '<p style="background:var(--sec-acc-soft,var(--pri-soft));border-left:4px solid var(--sec-acc,var(--pri));padding:8px 12px;border-radius:6px;margin:8px 0"><b>Существует ровно $5$ правильных многогранников</b> — их называют <b>платоновыми телами</b>.</p>'
+ '<div style="overflow-x:auto;margin:10px 0"><table style="width:100%;border-collapse:collapse;font-size:.92rem">'
+ '<thead><tr style="background:var(--sec-acc-soft,var(--pri-soft));color:var(--sec-acc-d,var(--pri2))">'
+ '<th style="padding:8px 10px;text-align:left;border-bottom:2px solid var(--sec-acc,var(--pri))">Тело</th>'
+ '<th style="padding:8px 10px;text-align:center;border-bottom:2px solid var(--sec-acc,var(--pri))">Грань</th>'
+ '<th style="padding:8px 10px;text-align:center;border-bottom:2px solid var(--sec-acc,var(--pri))">$F$</th>'
+ '<th style="padding:8px 10px;text-align:center;border-bottom:2px solid var(--sec-acc,var(--pri))">$V$</th>'
+ '<th style="padding:8px 10px;text-align:center;border-bottom:2px solid var(--sec-acc,var(--pri))">$E$</th>'
+ '<th style="padding:8px 10px;text-align:center;border-bottom:2px solid var(--sec-acc,var(--pri))">В вершине</th>'
+ '</tr></thead><tbody>'
+ '<tr><td style="padding:7px 10px;border-bottom:1px solid var(--border)"><b>Тетраэдр</b></td><td style="text-align:center">△</td><td style="text-align:center">4</td><td style="text-align:center">4</td><td style="text-align:center">6</td><td style="text-align:center">3</td></tr>'
+ '<tr><td style="padding:7px 10px;border-bottom:1px solid var(--border)"><b>Куб (гексаэдр)</b></td><td style="text-align:center">□</td><td style="text-align:center">6</td><td style="text-align:center">8</td><td style="text-align:center">12</td><td style="text-align:center">3</td></tr>'
+ '<tr><td style="padding:7px 10px;border-bottom:1px solid var(--border)"><b>Октаэдр</b></td><td style="text-align:center">△</td><td style="text-align:center">8</td><td style="text-align:center">6</td><td style="text-align:center">12</td><td style="text-align:center">4</td></tr>'
+ '<tr><td style="padding:7px 10px;border-bottom:1px solid var(--border)"><b>Додекаэдр</b></td><td style="text-align:center">⬠</td><td style="text-align:center">12</td><td style="text-align:center">20</td><td style="text-align:center">30</td><td style="text-align:center">3</td></tr>'
+ '<tr><td style="padding:7px 10px"><b>Икосаэдр</b></td><td style="text-align:center">△</td><td style="text-align:center">20</td><td style="text-align:center">12</td><td style="text-align:center">30</td><td style="text-align:center">5</td></tr>'
+ '</tbody></table></div>'
+ '<p><b>Почему ровно $5$?</b> Если в вершине сходится $k$ правильных $n$-угольников, то сумма плоских углов должна быть меньше $360^{\\circ}$. Угол правильного $n$-угольника равен $\\dfrac{180^{\\circ}(n-2)}{n}$. Условие:</p>'
+ '<p style="text-align:center;margin:8px 0">$$k\\cdot\\dfrac{180^{\\circ}(n-2)}{n} < 360^{\\circ} \\iff \\dfrac{1}{n}+\\dfrac{1}{k} > \\dfrac{1}{2}.$$</p>'
+ '<p>Целочисленных решений ($n\\ge 3$, $k\\ge 3$) ровно $5$: $(n,k)\\in\\{(3,3),(3,4),(3,5),(4,3),(5,3)\\}$ — это и есть пять платоновых тел.</p>');
html += makeCard('rule', 'Формула Эйлера и двойственность', '§ 7.2',
'<p><b>Формула Эйлера</b> для любого выпуклого многогранника:</p>'
+ '<p style="text-align:center;margin:8px 0">$$V - E + F = 2,$$</p>'
+ '<p>где $V$ — число вершин, $E$ — рёбер, $F$ — граней.</p>'
+ '<p><b>Проверка для платоновых тел:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>Тетраэдр: $4 - 6 + 4 = 2$ &#10003;</li>'
+ '<li>Куб: $8 - 12 + 6 = 2$ &#10003;</li>'
+ '<li>Октаэдр: $6 - 12 + 8 = 2$ &#10003;</li>'
+ '<li>Додекаэдр: $20 - 30 + 12 = 2$ &#10003;</li>'
+ '<li>Икосаэдр: $12 - 30 + 20 = 2$ &#10003;</li>'
+ '</ul>'
+ '<p style="font-weight:700;margin-top:10px">Двойственные пары.</p>'
+ '<p>Если в исходном теле соединить центры соседних граней — получится <b>двойственный</b> многогранник. Числа $F$ и $V$ у двойственной пары меняются местами, число рёбер $E$ сохраняется.</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Тетраэдр $\\leftrightarrow$ тетраэдр</b> — <i>автодуальное</i> тело: центрам граней тетраэдра соответствуют вершины другого тетраэдра.</li>'
+ '<li><b>Куб $\\leftrightarrow$ октаэдр</b>: $8$ вершин куба $\\leftrightarrow$ $8$ граней октаэдра; рёбер у обоих по $12$.</li>'
+ '<li><b>Додекаэдр $\\leftrightarrow$ икосаэдр</b>: $20$ вершин $\\leftrightarrow$ $20$ граней; рёбер по $30$.</li>'
+ '</ul>'
+ '<details class="spoiler"><summary>Пример: куб $\\to$ октаэдр</summary><div class="spoiler-body">'
+ '<p>Соединим центры $6$ граней куба — получим $6$ вершин нового тела. Каждая пара соседних граней даёт ребро ($12$ пар $\\Rightarrow 12$ рёбер). Грани нового тела — треугольники вокруг каждой вершины куба ($8$ вершин $\\Rightarrow 8$ граней). Это <b>октаэдр</b>.</p>'
+ '</div></details>');
html += makeCard('example', 'Формулы объёмов и применение', '§ 7.3',
'<p><b>Объёмы и площади</b> для длины ребра $a$:</p>'
+ '<div style="overflow-x:auto;margin:8px 0"><table style="width:100%;border-collapse:collapse;font-size:.92rem">'
+ '<thead><tr style="background:var(--sec-acc-soft,var(--pri-soft));color:var(--sec-acc-d,var(--pri2))">'
+ '<th style="padding:8px 10px;text-align:left;border-bottom:2px solid var(--sec-acc,var(--pri))">Тело</th>'
+ '<th style="padding:8px 10px;text-align:center;border-bottom:2px solid var(--sec-acc,var(--pri))">Объём $V$</th>'
+ '<th style="padding:8px 10px;text-align:center;border-bottom:2px solid var(--sec-acc,var(--pri))">Площадь $S$</th>'
+ '</tr></thead><tbody>'
+ '<tr><td style="padding:7px 10px;border-bottom:1px solid var(--border)"><b>Тетраэдр</b></td><td style="text-align:center">$\\dfrac{a^3\\sqrt{2}}{12}$</td><td style="text-align:center">$a^2\\sqrt{3}$</td></tr>'
+ '<tr><td style="padding:7px 10px;border-bottom:1px solid var(--border)"><b>Куб</b></td><td style="text-align:center">$a^3$</td><td style="text-align:center">$6a^2$</td></tr>'
+ '<tr><td style="padding:7px 10px;border-bottom:1px solid var(--border)"><b>Октаэдр</b></td><td style="text-align:center">$\\dfrac{a^3\\sqrt{2}}{3}$</td><td style="text-align:center">$2a^2\\sqrt{3}$</td></tr>'
+ '<tr><td style="padding:7px 10px;border-bottom:1px solid var(--border)"><b>Додекаэдр</b></td><td style="text-align:center">$\\dfrac{(15+7\\sqrt{5})\\,a^3}{4}$</td><td style="text-align:center">$3a^2\\sqrt{25+10\\sqrt{5}}$</td></tr>'
+ '<tr><td style="padding:7px 10px"><b>Икосаэдр</b></td><td style="text-align:center">$\\dfrac{5(3+\\sqrt{5})\\,a^3}{12}$</td><td style="text-align:center">$5a^2\\sqrt{3}$</td></tr>'
+ '</tbody></table></div>'
+ '<p style="font-weight:700;margin-top:8px">Применение в природе и культуре.</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Кристаллы:</b> пирит образует кубические кристаллы, флюорит — октаэдрические, гранат — додекаэдрические.</li>'
+ '<li><b>Вирусы:</b> капсиды многих вирусов (включая ВИЧ, аденовирусы) имеют форму икосаэдра.</li>'
+ '<li><b>Математика и философия:</b> Пифагорейцы и Платон связывали $5$ тел с пятью «стихиями»: куб — земля, тетраэдр — огонь, октаэдр — воздух, икосаэдр — вода, додекаэдр — эфир (космос).</li>'
+ '<li><b>Игральные кости:</b> D4, D6, D8, D12, D20 — ровно по одной кости каждой формы платонова тела.</li>'
+ '</ul>'
+ '<details class="spoiler"><summary>Пример: куб со стороной $a=4$</summary><div class="spoiler-body">'
+ '<p>$V = a^3 = 64$.</p>'
+ '<p>$S = 6a^2 = 96$.</p>'
+ '</div></details>'
+ '<details class="spoiler"><summary>Пример: октаэдр со стороной $a=3$</summary><div class="spoiler-body">'
+ '<p>$V = \\dfrac{a^3\\sqrt{2}}{3} = \\dfrac{27\\sqrt{2}}{3} = 9\\sqrt{2} \\approx 12{,}73$.</p>'
+ '<p>$S = 2a^2\\sqrt{3} = 18\\sqrt{3} \\approx 31{,}18$.</p>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — 3D-визуализатор 5 платоновых тел === */
html += '<div class="wg" id="p7-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D \xb7 платоновы тела</span><div class="wg-title">Визуализатор $5$ правильных многогранников</div></div>'
+ '<div class="wg-help">Переключай тела, меняй ребро $a$, вращай мышью. После просмотра <b>всех $5$ тел</b> — +10 XP.</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px">'
+ '<button class="btn primary" data-pt="tetra" id="p7-iv1-tetra">Тетраэдр</button>'
+ '<button class="btn" data-pt="cube" id="p7-iv1-cube">Куб</button>'
+ '<button class="btn" data-pt="octa" id="p7-iv1-octa">Октаэдр</button>'
+ '<button class="btn" data-pt="dodeca" id="p7-iv1-dodeca">Додекаэдр</button>'
+ '<button class="btn" data-pt="icosa" id="p7-iv1-icosa">Икосаэдр</button>'
+ '</div>'
+ '<div class="sliders">'
+ '<label>$a$ (ребро):<b id="p7-iv1-a-v">2.0</b><input type="range" id="p7-iv1-a" min="1" max="4" step="0.1" value="2"></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="p7-iv1-svg" viewBox="0 0 480 400" width="100%" style="max-width:480px;height:auto"></svg></div>'
+ '<div id="p7-iv1-info" style="margin-top:10px;background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;font-size:.93rem;line-height:1.65"></div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Изучено тел: <b id="p7-iv1-cnt">0</b> / 5</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Калькулятор V и S === */
html += '<div class="wg" id="p7-iv2">'
+ '<div class="wg-header"><span class="wg-badge">калькулятор</span><div class="wg-title">$V$ и $S$ платоновых тел</div></div>'
+ '<div class="wg-help">Выбери тело, введи длину ребра $a$ — получи объём и площадь поверхности с подстановкой.</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:10px">'
+ '<select id="p7-iv2-sel" class="tinp" style="padding:6px 10px">'
+ '<option value="tetra">Тетраэдр</option>'
+ '<option value="cube" selected>Куб</option>'
+ '<option value="octa">Октаэдр</option>'
+ '<option value="dodeca">Додекаэдр</option>'
+ '<option value="icosa">Икосаэдр</option>'
+ '</select>'
+ '<span>$a=$</span><input type="text" class="tinp" id="p7-iv2-a" value="3" style="width:80px">'
+ '<button class="btn primary" id="p7-iv2-calc">Вычислить</button>'
+ '</div>'
+ '<div id="p7-iv2-out" style="font-size:.95rem;line-height:1.7"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Двойственные пары (DnD) === */
html += '<div class="wg" id="p7-iv3">'
+ '<div class="wg-header"><span class="wg-badge">DnD \xb7 двойственность</span><div class="wg-title">Двойственные пары платоновых тел</div></div>'
+ '<div class="wg-help">Перетащи $6$ карточек по $3$ ящикам так, чтобы в каждом ящике стояла правильная двойственная пара. После проверки — +15 XP.</div>'
+ '<div id="p7-iv3-pool" class="dnd-pool"></div>'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px">'
+ '<div class="drop-box"><h5>Пара 1</h5><div class="drop-items" data-cat="pair1"></div></div>'
+ '<div class="drop-box"><h5>Пара 2</h5><div class="drop-items" data-cat="pair2"></div></div>'
+ '<div class="drop-box"><h5>Пара 3</h5><div class="drop-items" data-cat="pair3"></div></div>'
+ '</div>'
+ '<div class="actions" style="margin-top:10px;display:flex;gap:6px"><button class="btn primary" id="p7-iv3-check">Проверить</button><button class="btn" id="p7-iv3-reset">Сброс</button></div>'
+ '<div class="feedback" id="p7-iv3-fb"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 4 — Тренажёр === */
html += '<div class="wg" id="p7-iv4">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр \xb7 6 задач</span><div class="wg-title">Грани, вершины, рёбра и объёмы</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$ для дробных значений.</div>'
+ '<div id="p7-iv4-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p7-iv4-score">0</b> / 6</div>'
+ '</div>';
html += secNav('p6', 'final3');
html += readButton('p7');
box.innerHTML = html;
renderMath(box);
/* ====== JS-логика интерактивов ====== */
/* IV1 — 3D-визуализатор */
(function(){
if(!window.G3D) return;
const svg = document.getElementById('p7-iv1-svg');
const elA = document.getElementById('p7-iv1-a');
const vA = document.getElementById('p7-iv1-a-v');
const info = document.getElementById('p7-iv1-info');
const oCnt = document.getElementById('p7-iv1-cnt');
if(!svg) return;
const scene = G3D.createScene({W:480, H:400, scale:60, camDist:8, rotX:-0.35, rotY:0.7});
let curKind = 'tetra';
const seen = new Set();
let xpGiven = false;
const KINDS = ['tetra','cube','octa','dodeca','icosa'];
function setActiveBtn(){
KINDS.forEach(function(k){
const b = document.getElementById('p7-iv1-' + k);
if(b) b.classList.toggle('primary', k === curKind);
});
}
function draw(){
const a = +elA.value;
vA.textContent = a.toFixed(1);
const mesh = platonicMesh(curKind, a);
const M = G3D.buildRotMatrix(scene);
svg.innerHTML = renderWireframe(mesh, M, scene);
const inf = PLATONIC_INFO[curKind];
const Vnum = inf.volume(a);
const Snum = inf.area(a);
info.innerHTML = '<p style="font-weight:700;font-size:1rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:6px">' + inf.name + '</p>'
+ '<div style="display:flex;gap:14px;flex-wrap:wrap;font-size:.9rem;margin-bottom:6px">'
+ '<span>Граней $F=$<b>' + inf.F + '</b></span>'
+ '<span>Вершин $V=$<b>' + inf.V + '</b></span>'
+ '<span>Рёбер $E=$<b>' + inf.E + '</b></span>'
+ '<span>В вершине рёбер: <b>' + inf.vAtVertex + '</b></span>'
+ '</div>'
+ '<p>$' + inf.volFmt + '\\approx ' + fmt(+Vnum.toFixed(3)) + '$ (для $a=' + fmt(a) + '$)</p>'
+ '<p>$' + inf.areaFmt + '\\approx ' + fmt(+Snum.toFixed(3)) + '$</p>'
+ '<p style="font-size:.84rem;color:var(--muted);margin-top:4px">Проверка Эйлера: $V-E+F=' + inf.V + '-' + inf.E + '+' + inf.F + '=' + (inf.V - inf.E + inf.F) + '$.</p>';
renderMath(info);
seen.add(curKind);
oCnt.textContent = seen.size;
if(seen.size >= 5 && !xpGiven){
xpGiven = true;
addXp(10, 'p7-iv1');
bumpProgress('p7', 15);
const note = document.createElement('div');
note.className = 'feedback ok';
note.innerHTML = '&#10003; +10 XP за изучение всех $5$ платоновых тел!';
note.style.cssText = 'display:block;margin-top:8px';
const host = document.getElementById('p7-iv1');
if(host) host.appendChild(note);
try{ renderMath(note); }catch(e){}
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
setActiveBtn(); draw();
G3D.attachOrbit(svg, scene, draw);
elA.addEventListener('input', draw);
KINDS.forEach(function(k){
const b = document.getElementById('p7-iv1-' + k);
if(b) b.addEventListener('click', function(){ curKind = k; setActiveBtn(); draw(); });
});
document.querySelectorAll('#p7-iv1 .g3d-tools .btn').forEach(function(b){
b.addEventListener('click', function(){ G3D.presetView(scene, b.dataset.view, draw); });
});
})();
/* IV2 — Калькулятор */
(function(){
const sel = document.getElementById('p7-iv2-sel');
const elA = document.getElementById('p7-iv2-a');
const outBox = document.getElementById('p7-iv2-out');
const btn = document.getElementById('p7-iv2-calc');
let xpGiven = false;
const seenKinds = new Set();
function parseNum(id){ const el = document.getElementById(id); if(!el) return NaN; const v = (el.value||'').replace(',', '.').trim(); const x = parseFloat(v); return isFinite(x) ? x : NaN; }
btn.addEventListener('click', function(){
const kind = sel.value;
const a = parseNum('p7-iv2-a');
if(!isFinite(a) || a <= 0){
outBox.innerHTML = '<span style="color:var(--bad)">&#10007; Введи $a>0$.</span>';
renderMath(outBox);
return;
}
const inf = PLATONIC_INFO[kind];
const V = inf.volume(a);
const S = inf.area(a);
outBox.innerHTML = '<p><b>' + inf.name + '</b> с длиной ребра $a=' + fmt(a) + '$:</p>'
+ '<p>$' + inf.volFmt + ' \\approx ' + fmt(+V.toFixed(4)) + '$</p>'
+ '<p>$' + inf.areaFmt + ' \\approx ' + fmt(+S.toFixed(4)) + '$</p>'
+ '<p style="font-size:.84rem;color:var(--muted)">Граней: $F=' + inf.F + '$, вершин: $V=' + inf.V + '$, рёбер: $E=' + inf.E + '$.</p>';
renderMath(outBox);
seenKinds.add(kind);
if(seenKinds.size >= 3 && !xpGiven){
xpGiven = true;
addXp(10, 'p7-iv2');
bumpProgress('p7', 15);
}
});
})();
/* IV3 — Двойственные пары (DnD) */
(function(){
// 6 карточек, 3 пары: t1+t2 (тетраэдр), c+o (куб/октаэдр), d+i (додекаэдр/икосаэдр).
// Правильное распределение по ящикам: pair1={t1,t2}, pair2={c,o}, pair3={d,i}.
const items = [
{ id:'t1', html:'Тетраэдр', cat:'pair1' },
{ id:'t2', html:'Тетраэдр (двойственный)', cat:'pair1' },
{ id:'c', html:'Куб', cat:'pair2' },
{ id:'o', html:'Октаэдр', cat:'pair2' },
{ id:'d', html:'Додекаэдр', cat:'pair3' },
{ id:'i', html:'Икосаэдр', cat:'pair3' }
];
// Поскольку пары симметричны (любой ящик может быть любой парой), проверяем по структуре, а не по cat.
const REAL_PAIRS = {
tetra: new Set(['t1','t2']),
cube_octa: new Set(['c','o']),
dodeca_icosa: new Set(['d','i'])
};
const sorter = setupSorter({
poolId:'p7-iv3-pool',
scopeSelector:'#p7-iv3',
items: items,
cats: ['pair1','pair2','pair3']
});
let xpGiven = false;
document.getElementById('p7-iv3-check').addEventListener('click', function(){
const fb = document.getElementById('p7-iv3-fb');
// Группируем по cat
const byCat = {pair1:[], pair2:[], pair3:[]};
let allPlaced = true;
items.forEach(function(it){
const c = sorter.placed[it.id];
if(!c){ allPlaced = false; return; }
byCat[c].push(it.id);
});
if(!allPlaced){
feedback(fb, false, '&#10007; Распредели все $6$ карточек по ящикам.');
return;
}
// Проверяем, что каждая группа из двух элементов соответствует одной из настоящих пар
const groups = ['pair1','pair2','pair3'].map(function(c){ return new Set(byCat[c]); });
function eqSet(a, b){
if(a.size !== b.size) return false;
for(const x of a) if(!b.has(x)) return false;
return true;
}
const realSets = [REAL_PAIRS.tetra, REAL_PAIRS.cube_octa, REAL_PAIRS.dodeca_icosa];
// Каждая группа должна совпасть с одной из настоящих пар, без повторов
const used = new Set();
let ok = true;
for(const g of groups){
if(g.size !== 2){ ok = false; break; }
let found = -1;
for(let r = 0; r < realSets.length; r++){
if(used.has(r)) continue;
if(eqSet(g, realSets[r])){ found = r; break; }
}
if(found < 0){ ok = false; break; }
used.add(found);
}
if(ok){
feedback(fb, true, '&#10003; Верно! Двойственные пары: <b>тетраэдр $\\leftrightarrow$ тетраэдр</b>, <b>куб $\\leftrightarrow$ октаэдр</b>, <b>додекаэдр $\\leftrightarrow$ икосаэдр</b>.');
if(!xpGiven){
xpGiven = true;
addXp(15, 'p7-iv3');
bumpProgress('p7', 25);
}
} else {
feedback(fb, false, '&#10007; В каждом ящике должна стоять одна двойственная пара: тетраэдр+тетраэдр, куб+октаэдр, додекаэдр+икосаэдр.');
}
});
document.getElementById('p7-iv3-reset').addEventListener('click', function(){
sorter.reset();
const fb = document.getElementById('p7-iv3-fb');
if(fb){ fb.style.display = 'none'; fb.innerHTML = ''; }
});
})();
/* IV4 — Тренажёр */
(function(){
const tasks = [
{ q:'Сколько граней у икосаэдра?', a:20, tol:0.05 },
{ q:'Сколько вершин у додекаэдра?', a:20, tol:0.05 },
{ q:'Сколько рёбер у куба?', a:12, tol:0.05 },
{ q:'Куб со стороной $a=3$. Найди $V$.', a:27, tol:0.05 },
{ q:'Тетраэдр со стороной $a=2$. Найди $V$ (с точностью до $0{,}01$).', a:0.94, tol:0.05 },
{ q:'Октаэдр со стороной $a=3$. Найди $V$ (с точностью до $0{,}01$).', a:12.73, tol:0.05 }
];
const list = document.getElementById('p7-iv4-list');
const scoreEl = document.getElementById('p7-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="p7-iv4-inp-' + i + '" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="' + i + '">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p7-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('p7-iv4-inp-' + i);
const fb = document.getElementById('p7-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, 'p7-iv4');
bumpProgress('p7', 25);
setTimeout(function(){ achievement('p7_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
wireReadBtn('p7');
}
/* ===== ФИНАЛ РАЗДЕЛА 3 — Сфера, шар, правильные многогранники ===== */
function buildFinal3(){
const box = document.getElementById('final3-body');
if(!box) return;
let html = '';
/* Часть А — Шпаргалка раздела 3 (3 mini-карточки по числу § в разделе) */
html += '<div class="card">'
+ '<div class="card-header">'
+ '<div class="card-icon theory">' + ICONS.theory + '</div>'
+ '<div class="card-title">Шпаргалка раздела 3</div>'
+ '<div class="card-num">Итог</div>'
+ '</div>'
+ '<div class="card-body">'
+ '<p>Ключевые формулы трёх параграфов раздела в одном месте — пробеги глазами перед битвой с боссами.</p>'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:10px">'
+ '<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">'
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" style="width:18px;height:18px"><circle cx="12" cy="12" r="9"/><ellipse cx="12" cy="12" rx="9" ry="3.5"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 5 · Сфера</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">Уравнение: $(x-a)^2+(y-b)^2+(z-c)^2=R^2$. Касательная плоскость $\\perp$ радиусу в точке касания. Сечение плоскостью — окружность радиуса $r=\\sqrt{R^2-d^2}$, где $d$ — расстояние от центра до плоскости.</div>'
+ '</div>'
+ '<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">'
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" style="width:18px;height:18px"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 6 · Шар</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">$S=4\\pi R^2$, $V=\\tfrac{4}{3}\\pi R^3$. Сегмент: $V_{сег}=\\tfrac{\\pi h^2(3R-h)}{3}$, $S_{сферич}=2\\pi R h$. Куб со стороной $a$: вписанный шар $r=a/2$, описанный $R=\\tfrac{a\\sqrt{3}}{2}$.</div>'
+ '</div>'
+ '<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">'
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#7c3aed" stroke-width="2" style="width:18px;height:18px"><polygon points="12,2 22,8 22,16 12,22 2,16 2,8"/><polyline points="2,8 12,12 22,8"/><line x1="12" y1="12" x2="12" y2="22"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 7 · Многогранники</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">$5$ платоновых тел: тетраэдр, куб, октаэдр, додекаэдр, икосаэдр. Формула Эйлера: $V-E+F=2$. Двойственные пары: тетраэдр$\\leftrightarrow$тетраэдр, куб$\\leftrightarrow$октаэдр, додекаэдр$\\leftrightarrow$икосаэдр.</div>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>';
/* Часть Б — анонс 5 боссов */
html += '<div class="card">'
+ '<div class="card-header">'
+ '<div class="card-icon rule">' + ICONS.rule + '</div>'
+ '<div class="card-title">Боссы раздела 3</div>'
+ '<div class="card-num">5</div>'
+ '</div>'
+ '<div class="card-body">'
+ '<p>5 интегрированных задач — каждая опирается на темы § 5, § 6 или § 7. За каждого побеждённого босса: <b>+10 XP, +18% к прогрессу</b>. Победишь всех — ачивка <b>«Мастер сферической геометрии»</b> и <b>+50 XP бонус</b>.</p>'
+ '<p style="font-size:.88rem;color:var(--muted);margin-top:6px">Для расчётов с $\\pi$ используй $\\pi\\approx 3{,}14$. Допуск ответа — $\\pm 0{,}05$ (для больших чисел — $\\pm 0{,}5$).</p>'
+ '</div>'
+ '</div>';
html += '<div id="r3-bosses-container"></div>';
/* Прогресс-итог + ачивка */
html += '<div style="margin-top:18px;padding:18px 20px;background:linear-gradient(135deg,var(--pri-soft),var(--sec-acc-soft));border-radius:14px;border:1.5px solid var(--pri);text-align:center" id="r3-final-summary">'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:var(--pri2);font-size:1.1rem;margin-bottom:6px">Прогресс по боссам</div>'
+ '<div id="r3-boss-overall" style="font-size:.95rem;color:var(--text);margin-bottom:10px">0 / 5 боссов побеждено</div>'
+ '<div style="height:12px;background:var(--card);border-radius:8px;overflow:hidden;border:1px solid var(--border)">'
+ '<div id="r3-boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,#7c3aed,#a78bfa)"></div>'
+ '</div>'
+ '<div id="r3-final-reward" style="margin-top:14px;display:none;padding:14px;background:var(--card);border-radius:11px;border:2px solid #f59e0b">'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:#92400e;font-size:1.05rem;margin-bottom:6px">Мастер сферической геометрии</div>'
+ '<div style="font-size:.92rem;margin-bottom:10px">Раздел 3 пройден! Все 5 боссов повержены. +50 XP бонус.</div>'
+ '<a class="btn primary" href="/textbook/geometry-11-ch4" style="text-decoration:none">Дальше: Раздел 4 <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>'
+ '</div>'
+ '</div>';
html += secNav('p7', null);
box.innerHTML = html;
renderMath(box);
/* === Боссы === */
const BOSSES = [
{
n:1, color:'#2563eb',
title:'Циклоп Сферы',
tag:'§ 5',
q:'Сфера задана уравнением $x^2+y^2+z^2=49$. Найдите радиус $R$.',
ans:7, tol:0.05,
hint:'$R=\\sqrt{49}=7$.'
},
{
n:2, color:'#dc2626',
title:'Минотавр Шара',
tag:'§ 6',
q:'Шар с $R=6$. Найдите площадь поверхности (используй $\\pi\\approx 3{,}14$).',
ans:452.16, tol:0.5,
hint:'$S=4\\pi R^2=4\\cdot 3{,}14\\cdot 36=452{,}16$.'
},
{
n:3, color:'#0891b2',
title:'Гарпия Куба и Шара',
tag:'§ 6 + куб',
q:'Шар вписан в куб со стороной $a=8$. Найдите объём шара (используй $\\pi\\approx 3{,}14$).',
ans:267.95, tol:0.5,
hint:'Радиус вписанного шара: $r=a/2=4$. $V=\\tfrac{4}{3}\\pi r^3=\\tfrac{4}{3}\\cdot 3{,}14\\cdot 64\\approx 267{,}95$.'
},
{
n:4, color:'#7c3aed',
title:'Дракон Многогранников',
tag:'§ 7',
q:'Сколько вершин у икосаэдра?',
ans:12, tol:0.05,
hint:'У икосаэдра $V=12$, $E=30$, $F=20$. Проверка Эйлера: $V-E+F=12-30+20=2$.'
},
{
n:5, color:'#f59e0b',
title:'Мастер Сферической Геометрии',
tag:'синтез § 6 + § 7',
q:'Шар описан около куба со стороной $a=4$. Найдите радиус шара (округлите до сотых).',
ans:3.46, tol:0.05,
hint:'Диагональ куба $d=a\\sqrt{3}$. Радиус описанного шара $R=\\tfrac{a\\sqrt{3}}{2}=\\tfrac{4\\sqrt{3}}{2}=2\\sqrt{3}\\approx 3{,}46$.'
}
];
const cont = document.getElementById('r3-bosses-container');
const STATE_KEY = 'geometry11_ch3_bosses';
const BOSS_STATE = (function(){
try{ const s = localStorage.getItem(STATE_KEY); if(s){ const p = JSON.parse(s); if(Array.isArray(p) && p.length === BOSSES.length) return p; } }catch(e){}
return BOSSES.map(()=>({defeated:false}));
})();
function saveBosses(){ try{ localStorage.setItem(STATE_KEY, JSON.stringify(BOSS_STATE)); }catch(e){} }
cont.innerHTML = BOSSES.map(b=>{
return '<div class="boss-card" id="boss3-'+b.n+'-card" style="padding:16px;background:var(--card);border-radius:12px;border:2px solid '+b.color+';margin-bottom:14px">'
+ '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="'+b.color+'" stroke-width="2.2" style="width:28px;height:28px;flex-shrink:0"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:'+b.color+';font-size:1.05rem">Босс '+b.n+': '+b.title+'</div>'
+ '<div style="margin-left:auto;font-size:.78rem;color:var(--muted);padding:3px 8px;background:var(--sec-acc-soft);border-radius:6px">'+b.tag+'</div>'
+ '</div>'
+ '<div class="boss-q" id="boss3-'+b.n+'-q" style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:1rem;line-height:1.5;margin-bottom:10px">'+b.q+'</div>'
+ '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
+ '<span style="font-family:\'JetBrains Mono\',monospace;font-size:.92rem">ответ =</span>'
+ '<input type="number" id="boss3-'+b.n+'-ans" class="tinp" style="width:140px;text-align:center" step="0.01" placeholder="число">'
+ '<button class="btn primary" id="boss3-'+b.n+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атаковать</button>'
+ '<button class="btn" id="boss3-'+b.n+'-hint">Подсказка</button>'
+ '</div>'
+ '<div class="feedback" id="boss3-'+b.n+'-fb"></div>'
+ '</div>';
}).join('');
renderMath(cont);
function refreshOverall(){
const won = BOSS_STATE.filter(s => s.defeated).length;
const txt = document.getElementById('r3-boss-overall');
const fill = document.getElementById('r3-boss-overall-fill');
if(txt) txt.textContent = won + ' / ' + BOSSES.length + ' боссов побеждено';
if(fill) fill.style.width = (won * 100 / BOSSES.length) + '%';
if(won >= BOSSES.length){
const reward = document.getElementById('r3-final-reward');
if(reward && reward.style.display === 'none'){
reward.style.display = 'block';
if(!STATE.achievements.has('r3_done')){
achievement('r3_done','Мастер сферической геометрии');
addXp(50, 'r3-bonus');
bumpProgress('final3', 30);
if(window.confetti){ try{ confetti(); }catch(e){} }
}
}
}
}
BOSSES.forEach((b, idx)=>{
const card = document.getElementById('boss3-'+b.n+'-card');
const goBtn = document.getElementById('boss3-'+b.n+'-go');
const hintBtn= document.getElementById('boss3-'+b.n+'-hint');
const ansInp = document.getElementById('boss3-'+b.n+'-ans');
if(BOSS_STATE[idx].defeated){
card.style.background = 'linear-gradient(135deg,var(--sec-acc-soft),var(--pri-soft))';
card.classList.add('glow');
goBtn.disabled = true; goBtn.style.opacity = .55; goBtn.innerHTML = '&#10003; Повержен';
ansInp.disabled = true;
}
goBtn.addEventListener('click', ()=>{
if(BOSS_STATE[idx].defeated) return;
const fb = document.getElementById('boss3-'+b.n+'-fb');
const raw = (ansInp.value||'').replace(',', '.').trim();
const val = parseFloat(raw);
if(!isFinite(val)){ feedback(fb, false, '&#10007; Введи число.'); return; }
if(Math.abs(val - b.ans) <= b.tol){
BOSS_STATE[idx].defeated = true; saveBosses();
feedback(fb, true, '&#10003; Босс '+b.n+' повержен! +10 XP. '+b.hint);
addXp(10, 'boss-r3-'+b.n);
bumpProgress('final3', 18);
goBtn.disabled = true; goBtn.style.opacity = .55; goBtn.innerHTML = '&#10003; Повержен';
ansInp.disabled = true;
card.style.background = 'linear-gradient(135deg,var(--sec-acc-soft),var(--pri-soft))';
card.classList.add('glow','pulse');
setTimeout(()=>card.classList.remove('pulse'), 900);
refreshOverall();
} else {
feedback(fb, false, '&#10007; Промах. Попробуй ещё. Подсказка доступна.');
}
});
hintBtn.addEventListener('click', ()=>{
const fb = document.getElementById('boss3-'+b.n+'-fb');
fb.className = 'feedback ok';
fb.innerHTML = '<b>Подсказка:</b> '+b.hint;
fb.style.display = 'block';
fb.style.background = 'var(--warn-bg,#fef3c7)';
fb.style.color = '#92400e';
fb.style.borderLeftColor = 'var(--warn,#f59e0b)';
try{ renderMath(fb); }catch(e){}
});
ansInp.addEventListener('keydown', e=>{ if(e.key === 'Enter') goBtn.click(); });
});
refreshOverall();
}
/* ===== STUB BUILDER — единый для всех параграфов раздела (Phase 0) ===== */
function buildStub(id){
const p = PARAS.find(x => x.id === id);
if(!p) return;
const box = document.getElementById(id + '-body');
if(!box) return;
let html = '';
html += '<div class="stub-note">'
+ '<h3>' + p.num + ' «' + p.name + '» — в разработке</h3>'
+ '<p>Это параграф раздела ' + 3 + '. Полное наполнение (теория + 3 интерактива + DnD + тренажёр) появится в Phase 1+. Сейчас доступны только базовая навигация, прогресс-бар и подсказка в боковой панели.</p>'
+ '</div>';
html += makeCard('theory', 'План параграфа', p.num, '<p>Тема: <b>' + p.name + '</b>.</p>' + (p.sub ? '<p>Ключевая формула: ' + p.sub + '</p>' : '') + '<p>Содержание будет реализовано в следующих фазах разработки.</p>');
/* Демо-интерактив с G3D (если доступен) — показываем разработчику, что движок работает */
if(window.G3D && !p.final){
html += '<div class="wg" id="' + id + '-iv-demo">'
+ '<div class="wg-header"><span class="wg-badge">DEMO 3D</span><div class="wg-title">Превью мини-3D движка</div></div>'
+ '<div class="wg-help">Скелет-демо: тело можно вращать мышью. В Phase 1+ здесь появятся полноценные интерактивы с сечениями, развёртками и формулами.</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"><svg id="' + id + '-iv-svg" viewBox="0 0 480 360" width="100%" style="max-width:480px;height:auto"></svg></div>'
+ '</div>';
}
html += secNavFor(id);
html += readButton(id);
box.innerHTML = html;
renderMath(box);
/* Установка демо-3D */
if(window.G3D && !p.final){
const svg = document.getElementById(id + '-iv-svg');
if(svg){
const scene = G3D.createScene({W:480, H:360, scale:42, camDist:8, rotX:-0.35, rotY:0.7});
/* выбираем фигуру по id параграфа */
let mesh;
if(id === 'p1') mesh = G3D.prismMesh(4, 1.6, 2.4); /* куб/призма */
else if(id === 'p2') mesh = G3D.cylinderMesh(1.5, 2.6, 32);
else if(id === 'p3') mesh = G3D.pyramidMesh(4, 1.8, 2.6);
else if(id === 'p4') mesh = G3D.coneMesh(1.5, 2.6, 32);
else if(id === 'p5' || id === 'p6') mesh = null; /* сфера — отдельный wireframe */
else if(id === 'p7') mesh = G3D.prismMesh(3, 1.6, 1.8); /* тетраэдр-подобно */
else if(id === 'p10') mesh = G3D.prismMesh(4, 1.6, 2.0); /* куб для координат */
else mesh = G3D.prismMesh(6, 1.5, 2.2);
function draw(){
const M = G3D.buildRotMatrix(scene);
let inner = '';
if(mesh){
inner = G3D.renderMesh(mesh, M, scene);
} else {
/* сфера */
const sph = G3D.sphereWireframe(1.7, 5, 10);
inner = G3D.renderSphereWireframe(sph, M, scene);
}
svg.innerHTML = inner;
}
draw();
G3D.attachOrbit(svg, scene, draw);
const tools = document.querySelectorAll('#' + id + '-iv-demo .g3d-tools .btn');
tools.forEach(b => b.addEventListener('click', () => {
G3D.presetView(scene, b.dataset.view, draw);
}));
}
}
wireReadBtn(id);
}
/* ===== Search ===== */
const SEARCH_INDEX = (function(){
const arr=[];
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
arr.push({kind:'Теория',title:'Сфера и шар: определение',desc:'центр, радиус, диаметр, хорда',sec:'p5'});
arr.push({kind:'Теория',title:'Уравнение сферы',desc:'(x-a)^2+(y-b)^2+(z-c)^2=R^2',sec:'p5'});
arr.push({kind:'Теория',title:'Касательная плоскость к сфере',desc:'перпендикулярна радиусу в точке касания',sec:'p5'});
arr.push({kind:'Теория',title:'Сечение сферы плоскостью',desc:'r = sqrt(R^2 - d^2), большой круг',sec:'p5'});
arr.push({kind:'Теория',title:'Площадь сферы и объём шара',desc:'S = 4πR², V = (4/3)πR³',sec:'p6'});
arr.push({kind:'Теория',title:'Шаровой сегмент',desc:'V = πh²(3Rh)/3, S = 2πRh',sec:'p6'});
arr.push({kind:'Теория',title:'Шаровой сектор и слой',desc:'V_сект = (2/3)πR²h; V_слой = πh(3r1²+3r2²+h²)/6',sec:'p6'});
arr.push({kind:'Теория',title:'Шар, вписанный и описанный около куба',desc:'r = a/2, R = a√3/2',sec:'p6'});
arr.push({kind:'Теория',title:'Шар и цилиндр',desc:'вписан: h=2R; описан: R_шар=√(R²+(h/2)²)',sec:'p6'});
arr.push({kind:'Теория',title:'Правильные многогранники',desc:'тетраэдр, куб, октаэдр, додекаэдр, икосаэдр — 5 платоновых тел',sec:'p7'});
arr.push({kind:'Теория',title:'Формула Эйлера',desc:'V E + F = 2 для выпуклых многогранников',sec:'p7'});
arr.push({kind:'Теория',title:'Двойственные платоновы тела',desc:'куб ↔ октаэдр, додекаэдр ↔ икосаэдр, тетраэдр ↔ тетраэдр',sec:'p7'});
arr.push({kind:'Теория',title:'Объёмы платоновых тел',desc:'тетраэдр a³√2/12, куб a³, октаэдр a³√2/3, икосаэдр 5(3+√5)a³/12',sec:'p7'});
return arr;
})();
function initSearch(){
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
if(!modal||!inp||!out) return;
let cur=0,rows=[];
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'\u2026':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
function close(){ modal.classList.remove('show'); }
btn&&btn.addEventListener('click',open);
modal.addEventListener('click',e=>{if(e.target===modal)close();});
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
}
function initSidebarToggle(){
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
if(!side||!btn) return;
function open(){ side.classList.add('open'); back.classList.add('show'); }
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
back.addEventListener('click',close);
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
}
function init(){
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo(PARAS[0].id);
setTimeout(()=>achievement('start'), 600);
if(window.LS&&window.LS.xp){
window.LS.xp.load().then(function(s){ if(s&&s.xp>STATE.xp){ STATE.xp=s.xp; STATE.level=calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if(STATE.current) buildSidebar(STATE.current); } });
}
}
document.addEventListener('DOMContentLoaded', init);
/* === GEOM11 POLISH JS === */
(function(){
function bumpScore(el){
if(!el) return;
el.classList.remove('bump');
void el.offsetWidth;
el.classList.add('bump');
setTimeout(function(){ try{ el.classList.remove('bump'); }catch(e){} }, 270);
}
window.__geom11BumpScore = bumpScore;
function observeScores(root){
root = root || document;
var nodes = root.querySelectorAll('.score-display b');
nodes.forEach(function(b){
if(b.__scoreObs) return;
b.__scoreObs = true;
var last = b.textContent;
try{
var mo = new MutationObserver(function(){
var nv = b.textContent;
if(nv !== last){ last = nv; bumpScore(b); }
});
mo.observe(b, {childList:true, characterData:true, subtree:true});
}catch(e){}
});
}
function rescanScores(){ try{ observeScores(document); }catch(e){} }
if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', rescanScores);
else rescanScores();
try{
var rootObs = new MutationObserver(function(muts){
var need = false;
for(var i=0;i<muts.length && !need;i++){
var m = muts[i];
for(var j=0;j<m.addedNodes.length;j++){
var n = m.addedNodes[j];
if(n.nodeType===1){
if(n.classList && n.classList.contains('score-display')) { need = true; break; }
if(n.querySelector && n.querySelector('.score-display b')) { need = true; break; }
}
}
}
if(need) rescanScores();
});
rootObs.observe(document.body, {childList:true, subtree:true});
}catch(e){}
function refreshDoneMarks(){
try{
if(typeof STATE === 'undefined' || !STATE.progress) return;
document.querySelectorAll('.psel-card').forEach(function(c){
var id = c.dataset.id || c.dataset.progCard;
if(!id) return;
var pct = +STATE.progress[id] || 0;
if(!c.querySelector('.psel-done')){
var s = document.createElement('span');
s.className = 'psel-done';
s.setAttribute('title','Прочитано');
s.innerHTML = '<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>';
c.appendChild(s);
}
c.classList.toggle('done', pct >= 50);
});
}catch(e){}
}
try{
if(typeof window.refreshProgressUI === 'function'){
var _origRP = window.refreshProgressUI;
window.refreshProgressUI = function(){ var r = _origRP.apply(this, arguments); setTimeout(refreshDoneMarks, 0); return r; };
}
}catch(e){}
setTimeout(refreshDoneMarks, 600);
setTimeout(refreshDoneMarks, 1800);
window.addEventListener('focus', function(){ setTimeout(refreshDoneMarks, 200); });
document.addEventListener('click', function(e){
var card = e.target.closest && e.target.closest('.psel-card');
if(!card) return;
var id = card.dataset.id;
if(!id) return;
setTimeout(function(){
var sec = document.getElementById('sec-' + id);
if(sec) try{ sec.scrollIntoView({behavior:'smooth', block:'start'}); }catch(e){}
}, 60);
});
})();
</script>
</body>
</html>