Files
Learn_System/frontend/textbooks/geometry_11_ch2.html
T
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

1818 lines
117 KiB
HTML
Raw 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 · Раздел 2 · «Пирамида и конус»</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:#059669; --pri2:#047857; --pri-soft:#d1fae5;
--acc:#10b981; --acc2:#059669; --acc-soft:#a7f3d0;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#04140e; --card:#082017; --card-soft:#0a2a1d; --text:#d1fae5; --ink:#d1fae5; --muted:#6ee7b7; --border:#0f3a28}
*{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,#064e3b 0%,#059669 55%,#34d399 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(52,211,153,.2);min-height:130px}
.hdr::before{content:'РАЗДЕЛ 2';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-p3"]{ --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
.sec[id="sec-p4"]{ --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
.sec[id="sec-final2"]{ --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
.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 #d1fae5;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 · Раздел 2</h1>
<div class="hdr-sub">Пирамида (правильная, усечённая) · конус (правильный, усечённый) · объёмы через 1/3</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('p3')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 3</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-p3" class="sec" data-watermark="\triangledown"><div class="sec-header"><span class="sec-num">§ 3</span><h2 class="sec-h">Пирамида</h2></div><div id="p3-body"></div></section>
<section id="sec-p4" class="sec" data-watermark="\nabla"><div class="sec-header"><span class="sec-num">§ 4</span><h2 class="sec-h">Конус</h2></div><div id="p4-body"></div></section>
<section id="sec-final2" class="sec" data-watermark="&#9733;"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#059669,#10b981)">&#9733;</span><h2 class="sec-h">Финал раздела</h2></div><div id="final2-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» · Раздел 2 · «Пирамида и конус» · 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:'p3', progress:{}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 3;
const _TB_SLUG = 'geometry-11-ch2';
const PARAS = [
{ id:'p3', num:'§ 3', name:"Пирамида", sub:'$V=\\frac{1}{3}S_{осн}h$' },
{ id:'p4', num:'§ 4', name:"Конус", sub:'$S_{бок}=\\pi Rl$' },
{ id:'final2', num:'&#9733;', name:"Финал раздела", sub:'Итоги · боссы раздела 2', 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 = {
p3_done:"Пирамида освоено!",
p4_done:"Конус освоено!",
start:"Начало раздела 2!",
ch2_done:"Раздел 2 пройден!",
r2_done:"Мастер пирамиды и конуса"
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry11_ch2_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry11_ch2_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_ch2_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry11_ch2_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-ch2-'+(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 = { p3:buildP3, p4:buildP4, final2:buildFinal2 };
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 = {
p3:{title:"Шпаргалка § 3", rows:[
["Пирамида", "основание + апекс"],
["Правильная","основание — правильный многоугольник; высота в центре основания"],
["Апофема $l$","высота боковой грани"],
["Объём","$V=\\dfrac{1}{3}S_{осн}\\cdot h$"],
["$S_{бок}$ (правильной)","$\\dfrac{1}{2}P_{осн}\\cdot l$"],
["Боковое ребро","$b=\\sqrt{R^2+h^2}$"],
["Апофема","$l=\\sqrt{r^2+h^2}$"],
["Усечённая","$V=\\dfrac{h}{3}(S_1+S_2+\\sqrt{S_1 S_2})$"]
]},
p4:{title:"Шпаргалка § 4", rows:[
["Конус","основание ($R$) + апекс"],
["Связь","$l^2=R^2+h^2$"],
["$S_{осн}$","$\\pi R^2$"],
["$S_{бок}$","$\\pi R l$"],
["$S_{полн}$","$\\pi R(R+l)$"],
["$V$","$\\dfrac{1}{3}\\pi R^2 h$"],
["Развёртка","сектор $r=l$, дуга $2\\pi R$"],
["Угол развёртки","$\\varphi=\\dfrac{360°R}{l}$"],
["Усечённый $V$","$\\dfrac{\\pi h}{3}(R_1^2+R_2^2+R_1R_2)$"],
["Усечённый $S_{бок}$","$\\pi(R_1+R_2)l$"]
]},
final2:{title:"Финал раздела 2", rows:[["§ 3–§ 4","теория раздела 2"],["Награда","+50 XP"]]}
};
const TIPS=[
{sec:'p3',html:"Главное правило: <b>$V=\\dfrac{1}{3}S_{осн}h$</b> для <i>любой</i> пирамиды. А для правильной — $S_{бок}=\\dfrac{1}{2}P_{осн}l$, где $l$ — апофема."},
{sec:'p4',html:"Запомни связку: $l^2=R^2+h^2$. Все формулы конуса — это $\\pi R$ умноженное на соответствующий «множитель»: $R$ (основание), $l$ (боковая), $R+l$ (полная), $\\dfrac{Rh}{3}$ (объём)."},
{sec:'final2',html:"Финал раздела 2 — интегрированные задачи по разделу."}
];
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_ch2_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_ch2_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 = {p3:'\xA73',p4:'\xA74',final2:'Финал'};
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);
});
}
/* ===== § 3 «Пирамида» — Wave 1 ===== */
function buildP3(){
const box = document.getElementById('p3-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Определение и виды', '§ 3.1',
'<p><b>Пирамида</b> — многогранник, у которого одна грань (<b>основание</b>) — многоугольник, а остальные грани (<b>боковые</b>) — треугольники с общей вершиной.</p>'
+ '<p><b>Элементы пирамиды:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Основание</b> — многоугольник.</li>'
+ '<li><b>Вершина пирамиды</b> (апекс) — общая точка боковых граней.</li>'
+ '<li><b>Боковые грани</b> — треугольники.</li>'
+ '<li><b>Боковые рёбра</b> — отрезки от апекса к вершинам основания.</li>'
+ '<li><b>Высота</b> $h$ — перпендикуляр из апекса к плоскости основания.</li>'
+ '</ul>'
+ '<p><b>Правильная пирамида</b> — пирамида, у которой:</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>Основание — <b>правильный</b> многоугольник.</li>'
+ '<li>Высота попадает в <b>центр основания</b>.</li>'
+ '</ul>'
+ '<p>Тогда автоматически выполняются <b>следствия:</b> все боковые рёбра равны, а боковые грани — равные равнобедренные треугольники.</p>'
+ '<p><b>Апофема</b> $l$ — высота боковой грани, опущенная из апекса к середине стороны основания. У правильной пирамиды все апофемы равны.</p>');
html += makeCard('rule', 'Площадь и объём', '§ 3.2',
'<p><b>Объём</b> (для любой пирамиды):</p>'
+ '<p style="text-align:center;margin:8px 0">$$V=\\dfrac{1}{3}\\,S_{осн}\\cdot h$$</p>'
+ '<p><b>Боковая поверхность правильной пирамиды:</b></p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{бок}=\\dfrac{1}{2}\\,P_{осн}\\cdot l$$</p>'
+ '<p>где $l$ — апофема, $P_{осн}$ — периметр основания.</p>'
+ '<p><b>Полная поверхность:</b> $S_{полн}=S_{бок}+S_{осн}$.</p>'
+ '<p style="margin-top:10px"><b>Связь между элементами правильной $n$-угольной пирамиды.</b> Пусть $R$ — радиус описанной около основания окружности, $r$ — радиус вписанной, $b$ — боковое ребро, $l$ — апофема, $h$ — высота. Тогда:</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>$b^2=R^2+h^2$ — теорема Пифагора в треугольнике «апекс — центр — вершина основания».</li>'
+ '<li>$l^2=r^2+h^2$ — теорема Пифагора в треугольнике «апекс — центр — середина стороны».</li>'
+ '</ul>'
+ '<details class="spoiler"><summary>Пример: правильная 4-угольная пирамида $a=6$, $h=4$</summary><div class="spoiler-body">'
+ '<p>Основание — квадрат, $r=a/2=3$.</p>'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>$l=\\sqrt{r^2+h^2}=\\sqrt{9+16}=5$ — апофема.</li>'
+ '<li>$S_{осн}=a^2=36$, $P_{осн}=4a=24$.</li>'
+ '<li>$S_{бок}=\\dfrac{1}{2}\\cdot 24\\cdot 5=60$.</li>'
+ '<li>$S_{полн}=60+36=96$.</li>'
+ '<li>$V=\\dfrac{1}{3}\\cdot 36\\cdot 4=48$.</li>'
+ '</ul>'
+ '</div></details>');
html += makeCard('example', 'Усечённая пирамида', '§ 3.3',
'<p><b>Усечённая пирамида</b> — часть пирамиды, заключённая между основанием и плоскостью, параллельной основанию.</p>'
+ '<p>У неё <b>два основания</b> — подобные многоугольники в параллельных плоскостях. Боковые грани — <b>трапеции</b>.</p>'
+ '<p><b>Объём усечённой пирамиды:</b></p>'
+ '<p style="text-align:center;margin:8px 0">$$V=\\dfrac{1}{3}\\,h\\bigl(S_1+S_2+\\sqrt{S_1 S_2}\\bigr)$$</p>'
+ '<p>где $S_1,\\,S_2$ — площади оснований, $h$ — высота (расстояние между плоскостями оснований).</p>'
+ '<p><b>Боковая поверхность правильной усечённой пирамиды:</b></p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{бок}=\\dfrac{1}{2}(P_1+P_2)\\cdot l$$</p>'
+ '<p>где $l$ — апофема (высота боковой трапеции), $P_1,\\,P_2$ — периметры оснований.</p>'
+ '<details class="spoiler"><summary>Пример: правильная 4-угольная усечённая, $a_1=6$, $a_2=4$, $h=3$</summary><div class="spoiler-body">'
+ '<p>$S_1=36$, $S_2=16$.</p>'
+ '<p>Апофема: $l=\\sqrt{h^2+\\left(\\dfrac{a_1-a_2}{2}\\right)^2}=\\sqrt{9+1}=\\sqrt{10}$.</p>'
+ '<p>$V=\\dfrac{1}{3}\\cdot 3\\cdot(36+16+\\sqrt{576})=36+16+24=76$.</p>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — 3D-конструктор === */
html += '<div class="wg" id="p3-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · конструктор</span><div class="wg-title">Правильная $n$-угольная пирамида</div></div>'
+ '<div class="wg-help">Меняй число сторон основания $n$, радиус описанной окружности $R$ и высоту $h$. Вращай мышью или выбирай вид. После <b>4 разных конфигураций</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>$n$ (сторон):<b id="p3-iv1-n-v">4</b><input type="range" id="p3-iv1-n" min="3" max="6" step="1" value="4"></label>'
+ '<label>$R$ (радиус):<b id="p3-iv1-R-v">1.8</b><input type="range" id="p3-iv1-R" min="1" max="4" step="0.1" value="1.8"></label>'
+ '<label>$h$ (высота):<b id="p3-iv1-h-v">2.6</b><input type="range" id="p3-iv1-h" min="1" max="6" step="0.1" value="2.6"></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="p3-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>$a=$<b id="p3-iv1-a">—</b></span>'
+ '<span>$r=$<b id="p3-iv1-r">—</b></span>'
+ '<span>$P_{осн}=$<b id="p3-iv1-P">—</b></span>'
+ '<span>$S_{осн}=$<b id="p3-iv1-So">—</b></span>'
+ '<span>$l=$<b id="p3-iv1-l">—</b></span>'
+ '<span>$b=$<b id="p3-iv1-b">—</b></span>'
+ '<span>$S_{бок}=$<b id="p3-iv1-Sb">—</b></span>'
+ '<span>$S_{полн}=$<b id="p3-iv1-St">—</b></span>'
+ '<span>$V=$<b id="p3-iv1-V">—</b></span>'
+ '</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Конфигураций изучено: <b id="p3-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Калькулятор V и S === */
html += '<div class="wg" id="p3-iv2">'
+ '<div class="wg-header"><span class="wg-badge">калькулятор</span><div class="wg-title">$V$ и $S_{бок}$ правильной пирамиды</div></div>'
+ '<div class="wg-help">Введи $n$ (число сторон основания, $3..8$), сторону $a$ и высоту $h$. Калькулятор покажет пошаговый разбор.</div>'
+ '<div class="sliders">'
+ '<label>$n$:<b id="p3-iv2-n-v">4</b><input type="range" id="p3-iv2-n" min="3" max="8" step="1" value="4"></label>'
+ '<label>$a$ (сторона):<b id="p3-iv2-a-v">6.0</b><input type="range" id="p3-iv2-a" min="1" max="10" step="0.1" value="6"></label>'
+ '<label>$h$ (высота):<b id="p3-iv2-h-v">4.0</b><input type="range" id="p3-iv2-h" min="1" max="12" step="0.1" value="4"></label>'
+ '</div>'
+ '<div class="actions"><button class="btn primary" id="p3-iv2-calc">Вычислить</button></div>'
+ '<div id="p3-iv2-out" style="margin-top:10px;font-size:.92rem;line-height:1.7"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — «Найди элемент» (квикфайр) === */
html += '<div class="wg" id="p3-iv3">'
+ '<div class="wg-header"><span class="wg-badge">квикфайр · 6 заданий</span><div class="wg-title">Найди элемент пирамиды</div></div>'
+ '<div class="wg-help">Читай описание отрезка — выбирай, какой это элемент: высота $h$, апофема $l$ или боковое ребро $b$.</div>'
+ '<div id="p3-iv3-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Верно: <b id="p3-iv3-score">0</b> / 6</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 4 — Тренажёр V и S === */
html += '<div class="wg" id="p3-iv4">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 6 задач</span><div class="wg-title">$V$, $l$, $S_{бок}$ и усечённая</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$ для дробных значений.</div>'
+ '<div id="p3-iv4-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p3-iv4-score">0</b> / 6</div>'
+ '</div>';
html += secNav(null, 'p4');
html += readButton('p3');
box.innerHTML = html;
renderMath(box);
/* ====== JS-логика интерактивов ====== */
/* IV1 — 3D-конструктор */
(function(){
if(!window.G3D) return;
const svg = document.getElementById('p3-iv1-svg');
const elN = document.getElementById('p3-iv1-n');
const elR = document.getElementById('p3-iv1-R');
const elH = document.getElementById('p3-iv1-h');
const vN = document.getElementById('p3-iv1-n-v');
const vR = document.getElementById('p3-iv1-R-v');
const vH = document.getElementById('p3-iv1-h-v');
const oA = document.getElementById('p3-iv1-a');
const oR = document.getElementById('p3-iv1-r');
const oP = document.getElementById('p3-iv1-P');
const oSo = document.getElementById('p3-iv1-So');
const oL = document.getElementById('p3-iv1-l');
const oB = document.getElementById('p3-iv1-b');
const oSb = document.getElementById('p3-iv1-Sb');
const oSt = document.getElementById('p3-iv1-St');
const oV = document.getElementById('p3-iv1-V');
const oCnt = document.getElementById('p3-iv1-cnt');
if(!svg) return;
const scene = G3D.createScene({W:480, H:400, scale:50, camDist:8, rotX:-0.35, rotY:0.7});
const seen = new Set();
let xpGiven = false;
function draw(){
const n = +elN.value, R = +elR.value, h = +elH.value;
vN.textContent = n;
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
const mesh = G3D.pyramidMesh(n, R, h);
const M = G3D.buildRotMatrix(scene);
svg.innerHTML = G3D.renderMesh(mesh, M, scene, {
fillBase:'rgba(252,231,243,.55)',
fillSide:'rgba(219,234,254,.55)',
strokeVisible:'#0f172a',
strokeHidden:'#94a3b8'
});
const a = 2*R*Math.sin(Math.PI/n);
const r = R*Math.cos(Math.PI/n);
const P = n*a;
const So = 0.5*n*R*R*Math.sin(2*Math.PI/n);
const l = Math.sqrt(r*r + h*h);
const bs = Math.sqrt(R*R + h*h);
const Sb = 0.5*P*l;
const St = Sb + So;
const V = So*h/3;
oA.textContent = a.toFixed(2);
oR.textContent = r.toFixed(2);
oP.textContent = P.toFixed(2);
oSo.textContent = So.toFixed(2);
oL.textContent = l.toFixed(2);
oB.textContent = bs.toFixed(2);
oSb.textContent = Sb.toFixed(2);
oSt.textContent = St.toFixed(2);
oV.textContent = V.toFixed(2);
const key = n+'|'+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, 'p3-iv1');
bumpProgress('p3', 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('p3-iv1');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
G3D.attachOrbit(svg, scene, draw);
[elN, elR, elH].forEach(function(el){ el.addEventListener('input', draw); });
document.querySelectorAll('#p3-iv1 .g3d-tools .btn').forEach(function(b){
b.addEventListener('click', function(){ G3D.presetView(scene, b.dataset.view, draw); });
});
})();
/* IV2 — Калькулятор */
(function(){
const elN = document.getElementById('p3-iv2-n');
const elA = document.getElementById('p3-iv2-a');
const elH = document.getElementById('p3-iv2-h');
const labels = {'p3-iv2-n':'p3-iv2-n-v','p3-iv2-a':'p3-iv2-a-v','p3-iv2-h':'p3-iv2-h-v'};
Object.keys(labels).forEach(function(k){
const el = document.getElementById(k), lab = document.getElementById(labels[k]);
if(!el || !lab) return;
const upd = function(){ lab.textContent = el.step === '1' ? el.value : (+el.value).toFixed(1); };
el.addEventListener('input', upd); upd();
});
let xpGiven = false;
document.getElementById('p3-iv2-calc').addEventListener('click', function(){
const out = document.getElementById('p3-iv2-out');
const n = +elN.value, a = +elA.value, h = +elH.value;
const r = a / (2*Math.tan(Math.PI/n));
const P = n*a;
const So = 0.5*P*r;
const l = Math.sqrt(r*r + h*h);
const Sb = 0.5*P*l;
const St = Sb + So;
const V = So*h/3;
const html2 = '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 14px">'
+ '<p><b>Правильная '+n+'-угольная пирамида: $a='+a.toFixed(2)+'$, $h='+h.toFixed(2)+'$</b></p>'
+ '<p>$r=\\dfrac{a}{2\\tan(\\pi/n)}\\approx '+r.toFixed(3)+'$ — радиус вписанной окружности основания.</p>'
+ '<p>$P_{осн}=n\\cdot a='+n+'\\cdot '+a.toFixed(2)+'='+P.toFixed(2)+'$</p>'
+ '<p>$S_{осн}=\\dfrac{1}{2}P\\cdot r\\approx '+So.toFixed(2)+'$</p>'
+ '<p>$l=\\sqrt{r^2+h^2}=\\sqrt{'+(r*r).toFixed(2)+'+'+(h*h).toFixed(2)+'}\\approx '+l.toFixed(3)+'$ — апофема.</p>'
+ '<p>$S_{бок}=\\dfrac{1}{2}P_{осн}\\cdot l\\approx '+Sb.toFixed(2)+'$</p>'
+ '<p>$S_{полн}=S_{бок}+S_{осн}\\approx '+St.toFixed(2)+'$</p>'
+ '<p style="font-weight:700;color:var(--pri2)">$V=\\dfrac{1}{3}S_{осн}\\cdot h\\approx '+V.toFixed(2)+'$</p>'
+ '</div>';
out.innerHTML = html2;
renderMath(out);
if(!xpGiven){
xpGiven = true;
addXp(10, 'p3-iv2');
bumpProgress('p3', 15);
}
});
})();
/* IV3 — «Найди элемент» (квикфайр) */
(function(){
const tasks = [
{ q:'Отрезок от апекса к центру основания.', a:'h' },
{ q:'Отрезок от апекса к середине стороны основания.', a:'l' },
{ q:'Отрезок от апекса к вершине основания.', a:'b' },
{ q:'В равнобедренном треугольнике боковой грани — высота к стороне основания.', a:'l' },
{ q:'Отрезок из апекса перпендикулярно плоскости основания.', a:'h' },
{ q:'Длина отрезка апекс — вершина основания.', a:'b' }
];
const list = document.getElementById('p3-iv3-list');
const scoreEl = document.getElementById('p3-iv3-score');
const solved = new Set();
let xpGiven = false;
const NAMES = {h:'Высота $h$', l:'Апофема $l$', b:'Боковое ребро $b$'};
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="h">Высота $h$</button>'
+ '<button class="btn" data-i="'+i+'" data-v="l">Апофема $l$</button>'
+ '<button class="btn" data-i="'+i+'" data-v="b">Боковое ребро $b$</button>'
+ '</div>'
+ '<div class="feedback" id="p3-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('p3-iv3-fb-'+i);
if(solved.has(i)) return;
if(v === t.a){
feedback(fb, true, '&#10003; Верно — это '+NAMES[t.a]+'.');
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p3-iv3');
bumpProgress('p3', 25);
}
} else {
feedback(fb, false, '&#10007; Не то. Подумай ещё раз.');
}
});
});
})();
/* IV4 — Тренажёр V и S */
(function(){
const tasks = [
{ q:'Правильная 4-угольная пирамида: $a=6$, $h=4$. $V=\\,?$', a:48, tol:0.05 },
{ q:'Та же пирамида ($a=6$, $h=4$). Апофема $l=\\,?$', a:5, tol:0.05 },
{ q:'Та же пирамида. $S_{бок}=\\,?$', a:60, tol:0.05 },
{ q:'Правильная 3-угольная пирамида: $a=6$, $h=4$. $V=\\,?$ (точность 0,01)', a:20.78, tol:0.05 },
{ q:'Правильная 6-угольная пирамида: $a=4$, $h=3$. $V=\\,?$ (точность 0,01)', a:41.57, tol:0.05 },
{ q:'Правильная 4-угольная усечённая: $a_1=6$, $a_2=4$, $h=3$. $V=\\,?$', a:76, tol:0.05 }
];
const list = document.getElementById('p3-iv4-list');
const scoreEl = document.getElementById('p3-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="p3-iv4-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p3-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('p3-iv4-inp-'+i);
const fb = document.getElementById('p3-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, 'p3-iv4');
bumpProgress('p3', 25);
setTimeout(function(){ achievement('p3_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
wireReadBtn('p3');
}
/* ===== § 4 «Конус» — Wave 2 ===== */
function buildP4(){
const box = document.getElementById('p4-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Определение и элементы', '§ 4.1',
'<p><b>Конус</b> (прямой круговой) — тело, образованное вращением прямоугольного треугольника вокруг одного из катетов.</p>'
+ '<p><b>Элементы конуса:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Основание</b> — круг радиуса $R$ (получается вращением второго катета).</li>'
+ '<li><b>Вершина конуса</b> (апекс) — точка на оси вращения, не лежащая в основании.</li>'
+ '<li><b>Ось</b> — катет, вокруг которого вращали; совпадает с высотой $h$.</li>'
+ '<li><b>Образующие</b> — отрезки длины $l$ от апекса к точкам окружности основания (это гипотенузы при вращении). Все образующие равны.</li>'
+ '<li><b>Боковая поверхность</b> — поверхность вращения гипотенузы.</li>'
+ '<li><b>Высота</b> $h$ — расстояние от апекса до плоскости основания.</li>'
+ '</ul>'
+ '<p><b>Главная связь</b> (теорема Пифагора в осевом сечении):</p>'
+ '<p style="text-align:center;margin:8px 0">$$l^2 = R^2 + h^2$$</p>'
+ '<p>Конус — <b>«предельный»</b> случай пирамиды: если у правильной $n$-угольной пирамиды устремить число сторон основания $n\\to\\infty$ при фиксированном радиусе описанной окружности, то получится конус. Поэтому формулы для конуса можно получать «по аналогии» с пирамидой, заменяя $P_{осн}$ на $2\\pi R$, а $S_{осн}$ на $\\pi R^2$.</p>');
html += makeCard('rule', 'Площадь и объём. Развёртка', '§ 4.2',
'<p><b>Основные формулы:</b></p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{осн} = \\pi R^2$$</p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{бок} = \\pi R l$$</p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{полн} = \\pi R (R + l)$$</p>'
+ '<p style="text-align:center;margin:8px 0">$$V = \\dfrac{1}{3}\\pi R^2 h$$</p>'
+ '<p><b>Развёртка боковой поверхности конуса</b> — это <b>круговой сектор</b> радиуса $l$ (равного образующей), у которого длина дуги равна длине окружности основания $2\\pi R$.</p>'
+ '<p><b>Угол развёртки:</b></p>'
+ '<p style="text-align:center;margin:8px 0">$$\\varphi = \\dfrac{2\\pi R}{l}\\;\\text{рад} = \\dfrac{360°\\cdot R}{l}$$</p>'
+ '<details class="spoiler"><summary>Пример: конус $R=3$, $h=4$</summary><div class="spoiler-body">'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>$l=\\sqrt{R^2+h^2}=\\sqrt{9+16}=5$ — образующая.</li>'
+ '<li>$S_{осн}=\\pi R^2 = 9\\pi$.</li>'
+ '<li>$S_{бок}=\\pi R l = 15\\pi$.</li>'
+ '<li>$S_{полн}=\\pi R(R+l)= 3\\pi\\cdot 8 = 24\\pi\\approx 75{,}4$.</li>'
+ '<li>$V=\\dfrac{1}{3}\\pi R^2 h=\\dfrac{1}{3}\\cdot 9\\pi\\cdot 4 = 12\\pi\\approx 37{,}7$.</li>'
+ '<li>Угол развёртки: $\\varphi=\\dfrac{360°\\cdot 3}{5}=216°$.</li>'
+ '</ul>'
+ '</div></details>');
html += makeCard('example', 'Сечения. Усечённый конус', '§ 4.3',
'<p><b>Сечения конуса плоскостью:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Перпендикулярное оси</b> (параллельное основанию): <b>круг</b> радиуса меньше $R$. Если плоскость находится на расстоянии $h_1$ от апекса, то радиус сечения $r_1 = \\dfrac{R\\,h_1}{h}$.</li>'
+ '<li><b>Осевое</b> (плоскость проходит через ось): <b>равнобедренный треугольник</b> с основанием $2R$ и боковыми сторонами $l$.</li>'
+ '<li><b>Через апекс</b> (плоскость содержит апекс, но не ось): равнобедренный треугольник с боковыми сторонами $l$ и основанием — хордой окружности.</li>'
+ '</ul>'
+ '<p><b>Усечённый конус</b> — часть конуса, заключённая между основанием и плоскостью, параллельной основанию.</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>Два круглых основания радиусов $R_1$ (нижнее, большее) и $R_2$ (верхнее, меньшее).</li>'
+ '<li>Высота $h$ — расстояние между плоскостями оснований.</li>'
+ '<li>Образующая $l$ — отрезок между соответствующими точками окружностей. Связь: $l^2 = h^2 + (R_1-R_2)^2$.</li>'
+ '</ul>'
+ '<p style="text-align:center;margin:8px 0">$$V = \\dfrac{1}{3}\\pi h\\bigl(R_1^2 + R_2^2 + R_1 R_2\\bigr)$$</p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{бок} = \\pi (R_1 + R_2)\\,l$$</p>'
+ '<details class="spoiler"><summary>Пример: усечённый конус $R_1=5$, $R_2=3$, $h=4$</summary><div class="spoiler-body">'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>$l=\\sqrt{h^2+(R_1-R_2)^2}=\\sqrt{16+4}=\\sqrt{20}=2\\sqrt{5}\\approx 4{,}47$.</li>'
+ '<li>$S_{бок}=\\pi(R_1+R_2)l = 8\\pi\\cdot 2\\sqrt{5}=16\\pi\\sqrt{5}\\approx 112{,}4$.</li>'
+ '<li>$V=\\dfrac{1}{3}\\pi\\cdot 4\\cdot(25+9+15)=\\dfrac{196\\pi}{3}\\approx 205{,}3$.</li>'
+ '</ul>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — 3D-конструктор конуса === */
html += '<div class="wg" id="p4-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · конструктор</span><div class="wg-title">Конус: $R$, $h$ и все формулы</div></div>'
+ '<div class="wg-help">Меняй радиус основания $R$ и высоту $h$. Вращай мышью или выбирай вид. После <b>4 разных конфигураций</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>$R$ (радиус):<b id="p4-iv1-R-v">1.5</b><input type="range" id="p4-iv1-R" min="1" max="4" step="0.1" value="1.5"></label>'
+ '<label>$h$ (высота):<b id="p4-iv1-h-v">2.6</b><input type="range" id="p4-iv1-h" min="1" max="6" step="0.1" value="2.6"></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="p4-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>$l=$<b id="p4-iv1-l">—</b></span>'
+ '<span>$S_{осн}=$<b id="p4-iv1-So">—</b></span>'
+ '<span>$S_{бок}=$<b id="p4-iv1-Sb">—</b></span>'
+ '<span>$S_{полн}=$<b id="p4-iv1-St">—</b></span>'
+ '<span>$V=$<b id="p4-iv1-V">—</b></span>'
+ '<span>$\\varphi=$<b id="p4-iv1-phi">—</b></span>'
+ '</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Конфигураций изучено: <b id="p4-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Развёртка (2D-сектор) === */
html += '<div class="wg" id="p4-iv2">'
+ '<div class="wg-header"><span class="wg-badge">2D · развёртка</span><div class="wg-title">Развёртка боковой поверхности</div></div>'
+ '<div class="wg-help">Двигай $R$ и $h$ — увидишь, как меняется <b>круговой сектор</b> развёртки: его радиус равен $l$, длина дуги — $2\\pi R$, а угол — $\\varphi=\\dfrac{360°R}{l}$. После 4 конфигураций — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>$R$ (радиус):<b id="p4-iv2-R-v">1.5</b><input type="range" id="p4-iv2-R" min="1" max="4" step="0.1" value="1.5"></label>'
+ '<label>$h$ (высота):<b id="p4-iv2-h-v">2.6</b><input type="range" id="p4-iv2-h" min="1" max="6" step="0.1" value="2.6"></label>'
+ '</div>'
+ '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:8px;text-align:center"><svg id="p4-iv2-svg" viewBox="0 0 380 320" width="100%" style="max-width:380px;height:auto"></svg></div>'
+ '<div class="score-display" style="margin-top:10px;flex-wrap:wrap">'
+ '<span>$l=$<b id="p4-iv2-l">—</b></span>'
+ '<span>дуга $=2\\pi R=$<b id="p4-iv2-arc">—</b></span>'
+ '<span>угол $\\varphi=$<b id="p4-iv2-phi">—</b></span>'
+ '</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Конфигураций изучено: <b id="p4-iv2-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Квикфайр «какое сечение?» === */
html += '<div class="wg" id="p4-iv3">'
+ '<div class="wg-header"><span class="wg-badge">квикфайр · 6 заданий</span><div class="wg-title">Какое сечение конуса?</div></div>'
+ '<div class="wg-help">Читай описание плоскости — выбирай форму сечения: <b>круг</b>, <b>равнобедренный треугольник</b> или <b>эллипс</b>.</div>'
+ '<div id="p4-iv3-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Верно: <b id="p4-iv3-score">0</b> / 6</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 4 — Тренажёр V и S === */
html += '<div class="wg" id="p4-iv4">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 6 задач</span><div class="wg-title">$V$, $S$, $l$ и угол развёртки</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Используй $\\pi\\approx 3{,}14$. Допуск $\\pm 0{,}05$.</div>'
+ '<div id="p4-iv4-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p4-iv4-score">0</b> / 6</div>'
+ '</div>';
html += secNavFor('p4');
html += readButton('p4');
box.innerHTML = html;
renderMath(box);
/* ====== JS-логика интерактивов ====== */
/* IV1 — 3D-конструктор конуса */
(function(){
if(!window.G3D) return;
const svg = document.getElementById('p4-iv1-svg');
const elR = document.getElementById('p4-iv1-R');
const elH = document.getElementById('p4-iv1-h');
const vR = document.getElementById('p4-iv1-R-v');
const vH = document.getElementById('p4-iv1-h-v');
const oL = document.getElementById('p4-iv1-l');
const oSo = document.getElementById('p4-iv1-So');
const oSb = document.getElementById('p4-iv1-Sb');
const oSt = document.getElementById('p4-iv1-St');
const oV = document.getElementById('p4-iv1-V');
const oPhi = document.getElementById('p4-iv1-phi');
const oCnt = document.getElementById('p4-iv1-cnt');
if(!svg) return;
const PI = 3.14;
const scene = G3D.createScene({W:480, H:400, scale:50, camDist:8, rotX:-0.35, rotY:0.7});
const seen = new Set();
let xpGiven = false;
function draw(){
const R = +elR.value, h = +elH.value;
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
const mesh = G3D.coneMesh(R, h, 32);
const M = G3D.buildRotMatrix(scene);
svg.innerHTML = G3D.renderMesh(mesh, M, scene, {
fillBase:'rgba(252,231,243,.55)',
fillSide:'rgba(219,234,254,.55)',
strokeVisible:'#0f172a',
strokeHidden:'#94a3b8'
});
const l = Math.sqrt(R*R + h*h);
const So = PI*R*R;
const Sb = PI*R*l;
const St = PI*R*(R + l);
const V = PI*R*R*h/3;
const phi = 360*R/l;
oL.textContent = l.toFixed(2);
oSo.textContent = So.toFixed(2);
oSb.textContent = Sb.toFixed(2);
oSt.textContent = St.toFixed(2);
oV.textContent = V.toFixed(2);
oPhi.textContent = phi.toFixed(1) + '°';
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, 'p4-iv1');
bumpProgress('p4', 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('p4-iv1');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
G3D.attachOrbit(svg, scene, draw);
[elR, elH].forEach(function(el){ el.addEventListener('input', draw); });
document.querySelectorAll('#p4-iv1 .g3d-tools .btn').forEach(function(b){
b.addEventListener('click', function(){ G3D.presetView(scene, b.dataset.view, draw); });
});
})();
/* IV2 — Развёртка (2D-сектор) */
(function(){
const svg = document.getElementById('p4-iv2-svg');
const elR = document.getElementById('p4-iv2-R');
const elH = document.getElementById('p4-iv2-h');
const vR = document.getElementById('p4-iv2-R-v');
const vH = document.getElementById('p4-iv2-h-v');
const oL = document.getElementById('p4-iv2-l');
const oArc = document.getElementById('p4-iv2-arc');
const oPhi = document.getElementById('p4-iv2-phi');
const oCnt = document.getElementById('p4-iv2-cnt');
if(!svg) return;
const PI = 3.14;
const W = 380, H = 320;
const cx = 190, cy = 180;
const seen = new Set();
let xpGiven = false;
function draw(){
const R = +elR.value, h = +elH.value;
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
const l = Math.sqrt(R*R + h*h);
const arc = 2*PI*R;
const phiDeg = 360*R/l;
const phiRad = phiDeg * Math.PI / 180;
/* масштаб: подгоняем радиус сектора к ~120px на SVG */
const scale = 120 / l;
const rPix = l * scale;
/* строим сектор: центр (cx,cy), от угла -phi/2 до +phi/2 (отсчёт от вертикали вверх) */
const a0 = -Math.PI/2 - phiRad/2;
const a1 = -Math.PI/2 + phiRad/2;
const x0 = cx + rPix*Math.cos(a0), y0 = cy + rPix*Math.sin(a0);
const x1 = cx + rPix*Math.cos(a1), y1 = cy + rPix*Math.sin(a1);
const largeArc = phiRad > Math.PI ? 1 : 0;
let s = '';
/* сетка для контекста */
s += '<rect x="0" y="0" width="'+W+'" height="'+H+'" fill="#fafafa"/>';
/* пунктирный круг полного радиуса l (показывает «целое» откуда вырезан сектор) */
s += '<circle cx="'+cx+'" cy="'+cy+'" r="'+rPix.toFixed(2)+'" fill="none" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="3 3"/>';
/* заливка сектора */
s += '<path d="M '+cx+' '+cy+' L '+x0.toFixed(2)+' '+y0.toFixed(2)
+ ' A '+rPix.toFixed(2)+' '+rPix.toFixed(2)+' 0 '+largeArc+' 1 '+x1.toFixed(2)+' '+y1.toFixed(2)
+ ' Z" fill="rgba(5,150,105,.18)" stroke="#047857" stroke-width="2" stroke-linejoin="round"/>';
/* радиус-метка слева (l) */
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+x0.toFixed(2)+'" y2="'+y0.toFixed(2)+'" stroke="#0f172a" stroke-width="1.4"/>';
const midR1x = (cx + x0)/2, midR1y = (cy + y0)/2;
s += '<text x="'+(midR1x-18).toFixed(2)+'" y="'+(midR1y).toFixed(2)+'" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#047857">l='+l.toFixed(2)+'</text>';
/* радиус справа */
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+x1.toFixed(2)+'" y2="'+y1.toFixed(2)+'" stroke="#0f172a" stroke-width="1.4"/>';
/* подпись дуги */
const midA = -Math.PI/2;
const labelR = rPix + 16;
const labX = cx + labelR*Math.cos(midA), labY = cy + labelR*Math.sin(midA);
s += '<text x="'+labX.toFixed(2)+'" y="'+labY.toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="#0f172a">дуга = 2πR ≈ '+arc.toFixed(2)+'</text>';
/* центр и подпись угла φ */
s += '<circle cx="'+cx+'" cy="'+cy+'" r="3" fill="#0f172a"/>';
/* дуга индикатор угла маленькая */
const angR = 26;
const ax0 = cx + angR*Math.cos(a0), ay0 = cy + angR*Math.sin(a0);
const ax1 = cx + angR*Math.cos(a1), ay1 = cy + angR*Math.sin(a1);
s += '<path d="M '+ax0.toFixed(2)+' '+ay0.toFixed(2)
+ ' A '+angR+' '+angR+' 0 '+largeArc+' 1 '+ax1.toFixed(2)+' '+ay1.toFixed(2)
+ '" fill="none" stroke="#ec4899" stroke-width="2"/>';
s += '<text x="'+cx+'" y="'+(cy+14).toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="#ec4899">φ='+phiDeg.toFixed(1)+'°</text>';
svg.innerHTML = s;
oL.textContent = l.toFixed(2);
oArc.textContent = arc.toFixed(2);
oPhi.textContent = phiDeg.toFixed(1) + '°';
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, 'p4-iv2');
bumpProgress('p4', 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('p4-iv2');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
[elR, elH].forEach(function(el){ el.addEventListener('input', draw); });
})();
/* IV3 — Квикфайр сечений */
(function(){
const tasks = [
{ q:'Плоскость <b>перпендикулярна оси</b> конуса и пересекает его.', a:'круг' },
{ q:'<b>Осевое сечение</b> — плоскость проходит через ось конуса.', a:'треугольник' },
{ q:'Плоскость <b>параллельна основанию</b> и не совпадает с ним.', a:'круг' },
{ q:'Плоскость проходит через <b>апекс</b> и пересекает основание по хорде.', a:'треугольник' },
{ q:'Плоскость наклонена к основанию под углом 30°, не проходит через апекс, пересекает все образующие.', a:'эллипс' },
{ q:'Плоскость параллельна основанию и проходит через середину высоты.', a:'круг' }
];
const list = document.getElementById('p4-iv3-list');
const scoreEl = document.getElementById('p4-iv3-score');
const solved = new Set();
let xpGiven = false;
const NAMES = {'круг':'Круг', 'треугольник':'Равнобедренный треугольник', 'эллипс':'Эллипс'};
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="круг">Круг</button>'
+ '<button class="btn" data-i="'+i+'" data-v="треугольник">Равнобедренный треугольник</button>'
+ '<button class="btn" data-i="'+i+'" data-v="эллипс">Эллипс</button>'
+ '</div>'
+ '<div class="feedback" id="p4-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('p4-iv3-fb-'+i);
if(solved.has(i)) return;
if(v === t.a){
feedback(fb, true, '&#10003; Верно — это '+NAMES[t.a]+'.');
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p4-iv3');
bumpProgress('p4', 25);
}
} else {
feedback(fb, false, '&#10007; Не то. Подумай ещё раз — где плоскость пересекает образующие?');
}
});
});
})();
/* IV4 — Тренажёр V, S, l, угол развёртки */
(function(){
const tasks = [
{ q:'Конус: $R=3$, $h=4$. Найди $V$ (с $\\pi\\approx 3{,}14$).', a:37.68, tol:0.1 },
{ q:'Тот же конус ($R=3$, $h=4$). Образующая $l=\\,?$', a:5, tol:0.05 },
{ q:'Тот же конус ($R=3$, $h=4$). $S_{бок}=\\,?$ (с $\\pi\\approx 3{,}14$)', a:47.1, tol:0.1 },
{ q:'Тот же конус ($R=3$, $h=4$). $S_{полн}=\\,?$ (с $\\pi\\approx 3{,}14$)', a:75.36, tol:0.1 },
{ q:'Конус: $R=6$, $l=10$. Высота $h=\\,?$', a:8, tol:0.05 },
{ q:'Конус $R=3$, $l=6$. Угол развёртки $\\varphi=\\,?$ (в градусах)', a:180, tol:0.5 }
];
const list = document.getElementById('p4-iv4-list');
const scoreEl = document.getElementById('p4-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="p4-iv4-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p4-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('p4-iv4-inp-'+i);
const fb = document.getElementById('p4-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, 'p4-iv4');
bumpProgress('p4', 25);
setTimeout(function(){ achievement('p4_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно (используй $\\pi\\approx 3{,}14$).');
}
});
});
})();
wireReadBtn('p4');
}
/* ===== Финал Раздела 2 «Пирамида и конус» ===== */
function buildFinal2(){
const box = document.getElementById('final2-body');
if(!box) return;
let html = '';
/* Часть А — Шпаргалка раздела 2 (2 mini-карточки по числу § в разделе) */
html += '<div class="card">'
+ '<div class="card-header">'
+ '<div class="card-icon theory">' + ICONS.theory + '</div>'
+ '<div class="card-title">Шпаргалка раздела 2</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="#059669" stroke-width="2" style="width:18px;height:18px"><polygon points="12,3 21,20 3,20"/><line x1="12" y1="3" x2="12" y2="20"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 3 · Пирамида</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">$V=\\tfrac{1}{3}S_{осн}\\cdot h$ (любая). Правильная: $S_{бок}=\\tfrac{1}{2}P_{осн}\\cdot l$ ($l$ — апофема). Связи: $b^2=R^2+h^2$, $l^2=r^2+h^2$. Усечённая: $V=\\tfrac{1}{3}h(S_1+S_2+\\sqrt{S_1 S_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="#dc2626" stroke-width="2" style="width:18px;height:18px"><ellipse cx="12" cy="20" rx="8" ry="2.5"/><path d="M4 20L12 3L20 20"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 4 · Конус</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">$V=\\tfrac{1}{3}\\pi R^2 h$, $S_{бок}=\\pi R l$, $S_{полн}=\\pi R(R+l)$. $l^2=R^2+h^2$. Развёртка — сектор радиуса $l$ с углом $\\varphi=\\tfrac{360°\\,R}{l}$. Усечённый: $V=\\tfrac{1}{3}\\pi h(R_1^2+R_2^2+R_1 R_2)$.</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">Боссы раздела 2</div>'
+ '<div class="card-num">5</div>'
+ '</div>'
+ '<div class="card-body">'
+ '<p>5 интегрированных задач — каждая комбинирует темы § 3 и § 4. За каждого побеждённого босса: <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{,}1$).</p>'
+ '</div>'
+ '</div>';
html += '<div id="r2-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="r2-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="r2-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="r2-boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,#059669,#10b981)"></div>'
+ '</div>'
+ '<div id="r2-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">Раздел 2 пройден! Все 5 боссов повержены. +50 XP бонус.</div>'
+ '<a class="btn primary" href="/textbook/geometry-11-ch3" style="text-decoration:none">Дальше: Раздел 3 <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>'
+ '</div>'
+ '</div>';
html += secNav('p4', null);
box.innerHTML = html;
renderMath(box);
/* === Боссы === */
const BOSSES = [
{
n:1, color:'#059669',
title:'Циклоп Пирамиды',
tag:'§ 3',
q:'Правильная 4-угольная пирамида: сторона основания $a=6$, высота $h=4$. Найдите объём $V$.',
ans:48, tol:0.05,
hint:'$S_{осн}=a^2=36$, $V=\\tfrac{1}{3}\\cdot S_{осн}\\cdot h=\\tfrac{1}{3}\\cdot 36\\cdot 4=48$.'
},
{
n:2, color:'#dc2626',
title:'Минотавр Конуса',
tag:'§ 4',
q:'Конус с $R=5$, $h=12$. Найдите образующую $l$.',
ans:13, tol:0.05,
hint:'$l=\\sqrt{R^2+h^2}=\\sqrt{25+144}=\\sqrt{169}=13$.'
},
{
n:3, color:'#7c3aed',
title:'Гарпия Развёртки',
tag:'§ 4',
q:'Конус с $R=4$, $l=8$. Найдите угол развёртки боковой поверхности (в градусах).',
ans:180, tol:0.1,
hint:'$\\varphi=\\dfrac{360°\\cdot R}{l}=\\dfrac{360\\cdot 4}{8}=180°$.'
},
{
n:4, color:'#0891b2',
title:'Дракон Усечённой',
tag:'§ 3 + § 4',
q:'Усечённый конус: $R_1=5$, $R_2=2$, $h=4$. Найдите образующую $l$.',
ans:5, tol:0.05,
hint:'$l=\\sqrt{h^2+(R_1-R_2)^2}=\\sqrt{16+9}=\\sqrt{25}=5$.'
},
{
n:5, color:'#f59e0b',
title:'Мастер Тел Вращения',
tag:'синтез § 3 + § 4',
q:'В правильной 4-угольной пирамиде сторона основания $a=6$, высота $h=4$. Найдите длину апофемы $l$.',
ans:5, tol:0.05,
hint:'Радиус вписанной окружности квадрата: $r=a/2=3$. Апофема: $l=\\sqrt{r^2+h^2}=\\sqrt{9+16}=5$.'
}
];
const cont = document.getElementById('r2-bosses-container');
const STATE_KEY = 'geometry11_ch2_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="boss2-'+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"><polygon points="12,2 22,20 2,20"/></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="boss2-'+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="boss2-'+b.n+'-ans" class="tinp" style="width:140px;text-align:center" step="0.01" placeholder="число">'
+ '<button class="btn primary" id="boss2-'+b.n+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атаковать</button>'
+ '<button class="btn" id="boss2-'+b.n+'-hint">Подсказка</button>'
+ '</div>'
+ '<div class="feedback" id="boss2-'+b.n+'-fb"></div>'
+ '</div>';
}).join('');
renderMath(cont);
function refreshOverall(){
const won = BOSS_STATE.filter(s => s.defeated).length;
const txt = document.getElementById('r2-boss-overall');
const fill = document.getElementById('r2-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('r2-final-reward');
if(reward && reward.style.display === 'none'){
reward.style.display = 'block';
if(!STATE.achievements.has('r2_done')){
achievement('r2_done','Мастер пирамиды и конуса');
addXp(50, 'r2-bonus');
bumpProgress('final2', 30);
if(window.confetti){ try{ confetti(); }catch(e){} }
}
}
}
}
BOSSES.forEach((b, idx)=>{
const card = document.getElementById('boss2-'+b.n+'-card');
const goBtn = document.getElementById('boss2-'+b.n+'-go');
const hintBtn= document.getElementById('boss2-'+b.n+'-hint');
const ansInp = document.getElementById('boss2-'+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('boss2-'+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-r2-'+b.n);
bumpProgress('final2', 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('boss2-'+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>Это параграф раздела ' + 2 + '. Полное наполнение (теория + 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}));
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>