diff --git a/backend/scripts/check_geom11_js.js b/backend/scripts/check_geom11_js.js new file mode 100644 index 0000000..db57d18 --- /dev/null +++ b/backend/scripts/check_geom11_js.js @@ -0,0 +1,49 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); + +const files = [ + '../../frontend/js/g3d.js', + '../../frontend/textbooks/geometry_11_hub.html', + '../../frontend/textbooks/geometry_11_ch1.html', + '../../frontend/textbooks/geometry_11_ch2.html', + '../../frontend/textbooks/geometry_11_ch3.html', + '../../frontend/textbooks/geometry_11_ch4.html', +]; + +let totalErrors = 0; + +for (const rel of files) { + const p = path.join(__dirname, rel); + const src = fs.readFileSync(p, 'utf8'); + if (rel.endsWith('.js')) { + // pure JS file + try { + new Function(src); + console.log('OK (parse) ' + rel); + } catch (e) { + totalErrors++; + console.error('FAIL ' + rel + ':\n' + e.message); + } + continue; + } + // Extract all inline bodies (skip src= scripts) + const re = /]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi; + let m, idx = 0; + while ((m = re.exec(src))) { + idx++; + try { + new Function(m[1]); + } catch (e) { + totalErrors++; + console.error('FAIL ' + rel + ' [inline script #' + idx + ']:\n' + e.message); + } + } + console.log('OK (' + idx + ' inline) ' + rel); +} + +if (totalErrors > 0) { + console.error('\nTOTAL ERRORS: ' + totalErrors); + process.exit(1); +} +console.log('\nAll OK.'); diff --git a/backend/scripts/gen_geom11_chapters.js b/backend/scripts/gen_geom11_chapters.js new file mode 100644 index 0000000..7d26177 --- /dev/null +++ b/backend/scripts/gen_geom11_chapters.js @@ -0,0 +1,985 @@ +// Generator for Geometry 11 chapter files (Phase 0 skeleton). +// Produces frontend/textbooks/geometry_11_ch{1..4}.html with all helpers + STUB builders. + +'use strict'; +const fs = require('fs'); +const path = require('path'); + +const OUT_DIR = path.join(__dirname, '..', '..', 'frontend', 'textbooks'); + +const CHAPTERS = [ + { + n: 1, + title: 'Призма и цилиндр', + hdr_sub: 'Призма (правильная, прямая, наклонная, параллелепипед, куб) · цилиндр и его сечения', + hero_h2: 'Призма и цилиндр — главные стереометрические тела', + hero_p: 'Изучаем призму и цилиндр — главные стереометрические тела. Сечения, развёртки, формулы площадей и объёмов в 3D.', + final_id: 'final1', + color: { + hdr_grad: 'linear-gradient(110deg,#92400e 0%,#d97706 55%,#fbbf24 100%)', + hdr_label: 'РАЗДЕЛ 1', + hdr_border: 'rgba(251,191,36,.2)', + pri: '#d97706', pri2: '#b45309', pri_soft: '#fef3c7', + acc: '#f59e0b', acc2: '#d97706', acc_soft: '#fef9c3', + dark_bg: '#0a0a0e', dark_card: '#13120a', dark_card_soft: '#18160a', dark_text: '#fef9e7', + dark_muted: '#a39070', dark_border: '#2a2512', + }, + paras: [ + { id: 'p1', num: '§ 1', name: 'Призма', sub: '$S_{бок}=Pl$, $V=S_{осн}h$', watermark: '\\triangle' }, + { id: 'p2', num: '§ 2', name: 'Цилиндр', sub: '$S_{бок}=2\\pi Rh$, $V=\\pi R^2h$', watermark: '\\bigcirc' }, + ], + }, + { + n: 2, + title: 'Пирамида и конус', + hdr_sub: 'Пирамида (правильная, усечённая) · конус (правильный, усечённый) · объёмы через 1/3', + hero_h2: 'Пирамида и конус — фигуры с вершиной', + hero_p: 'Пирамида и конус — фигуры с вершиной. Правильные и усечённые. Объём через одну треть основания на высоту.', + final_id: 'final2', + color: { + hdr_grad: 'linear-gradient(110deg,#064e3b 0%,#059669 55%,#34d399 100%)', + hdr_label: 'РАЗДЕЛ 2', + hdr_border: 'rgba(52,211,153,.2)', + pri: '#059669', pri2: '#047857', pri_soft: '#d1fae5', + acc: '#10b981', acc2: '#059669', acc_soft: '#a7f3d0', + dark_bg: '#04140e', dark_card: '#082017', dark_card_soft: '#0a2a1d', dark_text: '#d1fae5', + dark_muted: '#6ee7b7', dark_border: '#0f3a28', + }, + paras: [ + { id: 'p3', num: '§ 3', name: 'Пирамида', sub: '$V=\\frac{1}{3}S_{осн}h$', watermark: '\\triangledown' }, + { id: 'p4', num: '§ 4', name: 'Конус', sub: '$S_{бок}=\\pi Rl$', watermark: '\\nabla' }, + ], + }, + { + n: 3, + title: 'Сфера и шар', + hdr_sub: 'Сфера и её уравнение · шар, сегменты · 5 платоновых тел', + hero_h2: 'Сфера, шар, правильные многогранники', + hero_p: 'Сфера, шар, пять платоновых тел. Уравнение сферы в координатах, шаровые сегменты, вписанные и описанные многогранники.', + final_id: 'final3', + color: { + hdr_grad: 'linear-gradient(110deg,#3b0764 0%,#7c3aed 55%,#a78bfa 100%)', + hdr_label: 'РАЗДЕЛ 3', + hdr_border: 'rgba(167,139,250,.2)', + pri: '#7c3aed', pri2: '#6d28d9', pri_soft: '#ede9fe', + acc: '#a78bfa', acc2: '#7c3aed', acc_soft: '#f3e8ff', + dark_bg: '#0e0521', dark_card: '#1a0a30', dark_card_soft: '#220c3d', dark_text: '#ede9fe', + dark_muted: '#c4b5fd', dark_border: '#3a1d5e', + }, + paras: [ + { id: 'p5', num: '§ 5', name: 'Сфера', sub: '$(x-a)^2+(y-b)^2+(z-c)^2=R^2$', watermark: 'S^2' }, + { id: 'p6', num: '§ 6', name: 'Шар', sub: '$S=4\\pi R^2$, $V=\\frac{4}{3}\\pi R^3$', watermark: 'V' }, + { id: 'p7', num: '§ 7', name: 'Правильные многогранники', sub: '5 платоновых тел', watermark: '\\star' }, + ], + }, + { + n: 4, + title: 'Повторение', + hdr_sub: 'Планиметрия · величины · координаты и векторы в 3D · построения', + hero_h2: 'Повторение всей геометрии', + hero_p: 'Повторение всей геометрии: планиметрия, площади и объёмы, координаты и векторы в 3D, классические построения.', + final_id: 'final4', + color: { + hdr_grad: 'linear-gradient(110deg,#881337 0%,#e11d48 55%,#fb7185 100%)', + hdr_label: 'РАЗДЕЛ 4', + hdr_border: 'rgba(251,113,133,.2)', + pri: '#e11d48', pri2: '#be123c', pri_soft: '#ffe4e6', + acc: '#f43f5e', acc2: '#e11d48', acc_soft: '#fecdd3', + dark_bg: '#1a0510', dark_card: '#2a081a', dark_card_soft: '#36102a', dark_text: '#ffe4e6', + dark_muted: '#fda4af', dark_border: '#4a1029', + }, + paras: [ + { id: 'p8', num: '§ 8', name: 'Геометрические фигуры и их свойства', sub: 'планиметрия', watermark: '\\square' }, + { id: 'p9', num: '§ 9', name: 'Геометрические величины', sub: 'площади, объёмы', watermark: 'S=' }, + { id: 'p10', num: '§ 10', name: 'Координаты и векторы', sub: '3D: $\\vec{a}=(x;y;z)$', watermark: '\\vec{v}' }, + { id: 'p11', num: '§ 11', name: 'Геометрические построения', sub: 'циркуль и линейка', watermark: '\\circlearrowleft' }, + ], + }, +]; + +function chapterTpl(ch) { + const allParas = ch.paras.concat([{ id: ch.final_id, num: '★', name: 'Финал раздела', sub: 'Итоги · боссы раздела ' + ch.n, final: true, watermark: '\\star' }]); + const PARAS_JS = allParas.map(p => { + const sub = p.sub ? `, sub:'${p.sub.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'` : ''; + const fin = p.final ? `, final:true` : ''; + return ` { id:'${p.id}', num:'${p.num}', name:${JSON.stringify(p.name)}${sub}${fin} }`; + }).join(',\n'); + + // SIDEBARS (basic placeholder per para) + const SIDEBARS_JS = allParas.map(p => { + const t = p.final ? `Финал раздела ${ch.n}` : `Шпаргалка ${p.num}`; + const rows = p.final + ? `[["${ch.paras[0].num}–${ch.paras[ch.paras.length-1].num}","теория раздела ${ch.n}"],["Награда","+50 XP"]]` + : `[["Тема", ${JSON.stringify(p.name)}],["Формула","${(p.sub || '').replace(/\\/g, '\\\\\\\\').replace(/"/g, '\\"')}"]]`; + return ` ${p.id}:{title:${JSON.stringify(t)}, rows:${rows}}`; + }).join(',\n'); + + const TIPS_JS = allParas.map(p => { + const html = p.final + ? `Финал раздела ${ch.n} — интегрированные задачи по разделу.` + : `${p.num} «${p.name}» — содержание в разработке. ${(p.sub || '').replace(/\\/g, '\\\\\\\\')}`; + return ` {sec:'${p.id}',html:${JSON.stringify(html)}}`; + }).join(',\n'); + + const ACH_LABELS_JS = ch.paras.map(p => ` ${p.id}_done:"${p.name} освоено!"`).concat([ + ` start:"Начало раздела ${ch.n}!"`, + ` ch${ch.n}_done:"Раздел ${ch.n} пройден!"` + ]).join(',\n'); + + // sec[id="sec-pX"] rules for accent colors + const SEC_COLOR_RULES = allParas.map(p => { + return `.sec[id="sec-${p.id}"]{ --sec-acc:${ch.color.pri}; --sec-acc-d:${ch.color.pri2}; --sec-acc-soft:${ch.color.pri_soft}; }`; + }).join('\n'); + + // sec elements in body + const SEC_HTML = allParas.map(p => { + const w = p.final ? '★' : p.watermark; + return `
${p.final ? '★' : p.num}

${p.name}

`; + }).join('\n'); + + // builders map + const BUILDERS_JS = allParas.map(p => `${p.id}:()=>buildStub('${p.id}')`).join(', '); + + // search NAMES + const NAMES_JS = allParas.map(p => `${p.id}:'${p.final ? 'Финал' : p.num.replace('§', '\\xA7').replace(' ', '')}'`).join(','); + + const TOTAL_PARAS = allParas.length; + const SLUG = `geometry-11-ch${ch.n}`; + const TITLE = `Геометрия 11 · Раздел ${ch.n} · «${ch.title}»`; + const HDR_H1 = `Геометрия 11 · Раздел ${ch.n}`; + const HDR_LABEL = ch.color.hdr_label; + const HDR_GRAD = ch.color.hdr_grad; + const HDR_BORDER = ch.color.hdr_border; + + return ` + + + + + + +${TITLE} + + + + + + + + + + + + +
+
+
+

${HDR_H1}

+
${ch.hdr_sub}
+
+
+ К геометрии 11 + + + +
+
+
+ +
+
+ +
+

${ch.hero_h2}

+

${ch.hero_p}

+
+ +
+ Прогресс по разделу +
+ 0% +
+
+
+
+ +
+
Параграфы раздела
+
+
+ +${SEC_HTML} + +
+ +
+
+ + + +
Достижение!
+ + + + + + +`; +} + +// === Main === +for (const ch of CHAPTERS) { + const out = path.join(OUT_DIR, `geometry_11_ch${ch.n}.html`); + const content = chapterTpl(ch); + fs.writeFileSync(out, content, 'utf8'); + console.log(`Wrote ${out} (${content.length} bytes)`); +} +console.log('Done.'); diff --git a/backend/src/db/migrations/026_geometry_11_hub.sql b/backend/src/db/migrations/026_geometry_11_hub.sql new file mode 100644 index 0000000..1c420dc --- /dev/null +++ b/backend/src/db/migrations/026_geometry_11_hub.sql @@ -0,0 +1,33 @@ +-- Geometry 11 hub migration. +-- Adds hub row + 4 chapter children for Геометрия 11 (Латотин, Чеботаревский и др., 2020). +-- Pattern mirrors 025_algebra_11_hub.sql. + +-- 1. Hub row. +INSERT INTO textbooks + (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active) +VALUES + ('geometry-11', 'math', 11, 'Геометрия — 11 класс', + 'Л. А. Латотин, Б. Д. Чеботаревский, И. В. Горбунова, О. Е. Цыбулько', + 'Полный курс стереометрии 11 класса по учебнику Латотина и Чеботаревского: призма, цилиндр, пирамида, конус, сфера, шар, правильные многогранники, повторение всей геометрии. 4 раздела, 11 параграфов.', + 'geometry_11_hub.html', 11, 'cyan', 10, 1); + +-- 2. Chapter children (разделы). +INSERT INTO textbooks + (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug) +VALUES + ('geometry-11-ch1', 'math', 11, 'Геометрия 11 · Призма и цилиндр', + '', + '§1–§2: призма (правильная, прямая, наклонная, параллелепипед, куб); цилиндр, сечения, развёртка, касательная плоскость.', + 'geometry_11_ch1.html', 2, 'amber', 1, 1, 'geometry-11'), + ('geometry-11-ch2', 'math', 11, 'Геометрия 11 · Пирамида и конус', + '', + '§3–§4: пирамида (правильная, усечённая); конус (правильный, усечённый). Объём через одну треть основания на высоту.', + 'geometry_11_ch2.html', 2, 'emerald', 2, 1, 'geometry-11'), + ('geometry-11-ch3', 'math', 11, 'Геометрия 11 · Сфера и шар', + '', + '§5–§7: сфера (уравнение, касательная плоскость); шар (площадь, объём, сегменты); правильные многогранники (5 платоновых тел).', + 'geometry_11_ch3.html', 3, 'violet', 3, 1, 'geometry-11'), + ('geometry-11-ch4', 'math', 11, 'Геометрия 11 · Повторение', + '', + '§8–§11: геометрические фигуры и их свойства (планиметрия); геометрические величины (площади, объёмы); координаты и векторы в 3D; геометрические построения.', + 'geometry_11_ch4.html', 4, 'rose', 4, 1, 'geometry-11'); diff --git a/frontend/js/g3d.js b/frontend/js/g3d.js new file mode 100644 index 0000000..450d414 --- /dev/null +++ b/frontend/js/g3d.js @@ -0,0 +1,341 @@ +// g3d.js — мини-3D движок для стереометрии. SVG-рендеринг, изометрическая проекция, drag-to-rotate. +(function(){ +'use strict'; + +// === Векторная математика === +function vAdd(a, b) { return {x:a.x+b.x, y:a.y+b.y, z:a.z+b.z}; } +function vSub(a, b) { return {x:a.x-b.x, y:a.y-b.y, z:a.z-b.z}; } +function vScale(a, k) { return {x:a.x*k, y:a.y*k, z:a.z*k}; } +function vDot(a, b) { return a.x*b.x + a.y*b.y + a.z*b.z; } +function vCross(a, b) { + return { + x: a.y*b.z - a.z*b.y, + y: a.z*b.x - a.x*b.z, + z: a.x*b.y - a.y*b.x + }; +} +function vLen(a) { return Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z); } +function vNorm(a) { var l = vLen(a) || 1; return vScale(a, 1/l); } + +// === Матрицы 3x3 === +function matRotX(t) { + var c = Math.cos(t), s = Math.sin(t); + return [[1,0,0],[0,c,-s],[0,s,c]]; +} +function matRotY(t) { + var c = Math.cos(t), s = Math.sin(t); + return [[c,0,s],[0,1,0],[-s,0,c]]; +} +function matMul(A, B) { + var R = [[0,0,0],[0,0,0],[0,0,0]]; + for (var i=0;i<3;i++) for (var j=0;j<3;j++) + R[i][j] = A[i][0]*B[0][j] + A[i][1]*B[1][j] + A[i][2]*B[2][j]; + return R; +} +function vApply(M, v) { + return { + x: M[0][0]*v.x + M[0][1]*v.y + M[0][2]*v.z, + y: M[1][0]*v.x + M[1][1]*v.y + M[1][2]*v.z, + z: M[2][0]*v.x + M[2][1]*v.y + M[2][2]*v.z + }; +} + +// === Проекция (перспективная с камерой по оси Z) === +function projectPersp(v, camDist, cx, cy, scale) { + var z = camDist - v.z; + if (z < 0.1) return null; + var k = scale * camDist / z; + return { x: cx + v.x * k, y: cy - v.y * k, depth: z }; +} + +// === Изометрическая проекция (без перспективы) === +function projectIso(v, cx, cy, scale) { + return { + x: cx + scale * (v.x - v.z) * 0.866, + y: cy - scale * (v.y - (v.x + v.z) * 0.5), + depth: -v.z + v.x*0.5 + v.y*0.5 + }; +} + +// === Сцена === +function createScene(opts) { + opts = opts || {}; + return { + W: opts.W || 480, + H: opts.H || 360, + cx: opts.cx !== undefined ? opts.cx : (opts.W || 480) / 2, + cy: opts.cy !== undefined ? opts.cy : (opts.H || 360) / 2, + scale: opts.scale || 50, + camDist: opts.camDist || 8, + rotX: opts.rotX !== undefined ? opts.rotX : -0.35, + rotY: opts.rotY !== undefined ? opts.rotY : 0.7, + proj: opts.proj || 'persp', + bg: opts.bg || 'transparent' + }; +} + +// === Вершины фигур === + +function prismPolygonBase(n, R, theta0) { + theta0 = theta0 || -Math.PI/2; + var verts = []; + for (var i = 0; i < n; i++) { + var a = theta0 + 2*Math.PI*i/n; + verts.push({ x: R*Math.cos(a), z: R*Math.sin(a) }); + } + return verts; +} + +function prismMesh(n, R, h) { + var baseXZ = prismPolygonBase(n, R); + var verts = []; + for (var i = 0; i < baseXZ.length; i++) verts.push({ x: baseXZ[i].x, y: -h/2, z: baseXZ[i].z }); + for (var j = 0; j < baseXZ.length; j++) verts.push({ x: baseXZ[j].x, y: h/2, z: baseXZ[j].z }); + var faces = []; + var bottom = []; + for (var k = n - 1; k >= 0; k--) bottom.push(k); + faces.push({ indices: bottom, kind: 'base' }); + var top = []; + for (var t = 0; t < n; t++) top.push(n + t); + faces.push({ indices: top, kind: 'base' }); + for (var s = 0; s < n; s++) { + var ss = (s + 1) % n; + faces.push({ indices: [s, ss, n + ss, n + s], kind: 'lateral' }); + } + return { verts: verts, faces: faces }; +} + +function pyramidMesh(n, R, h) { + var baseXZ = prismPolygonBase(n, R); + var verts = baseXZ.map(function(p){ return { x: p.x, y: -h/2, z: p.z }; }); + verts.push({ x: 0, y: h/2, z: 0 }); + var faces = []; + var apex = n; + var bottom = []; + for (var i = n - 1; i >= 0; i--) bottom.push(i); + faces.push({ indices: bottom, kind: 'base' }); + for (var j = 0; j < n; j++) { + var jj = (j + 1) % n; + faces.push({ indices: [j, jj, apex], kind: 'lateral' }); + } + return { verts: verts, faces: faces }; +} + +function cylinderMesh(R, h, segments) { + segments = segments || 32; + var bottom = [], top = []; + for (var i = 0; i < segments; i++) { + var a = 2*Math.PI*i/segments; + bottom.push({ x: R*Math.cos(a), y: -h/2, z: R*Math.sin(a) }); + top.push({ x: R*Math.cos(a), y: h/2, z: R*Math.sin(a) }); + } + var verts = bottom.concat(top); + var faces = []; + var bIdx = []; for (var k = segments - 1; k >= 0; k--) bIdx.push(k); + faces.push({ indices: bIdx, kind: 'base', isRound: true }); + var tIdx = []; for (var t = 0; t < segments; t++) tIdx.push(segments + t); + faces.push({ indices: tIdx, kind: 'base', isRound: true }); + for (var s = 0; s < segments; s++) { + var ss = (s + 1) % segments; + faces.push({ indices: [s, ss, segments + ss, segments + s], kind: 'lateral' }); + } + return { verts: verts, faces: faces, isRound: true }; +} + +function coneMesh(R, h, segments) { + segments = segments || 32; + var base = []; + for (var i = 0; i < segments; i++) { + var a = 2*Math.PI*i/segments; + base.push({ x: R*Math.cos(a), y: -h/2, z: R*Math.sin(a) }); + } + var verts = base.concat([{ x: 0, y: h/2, z: 0 }]); + var apex = segments; + var faces = []; + var bIdx = []; for (var k = segments - 1; k >= 0; k--) bIdx.push(k); + faces.push({ indices: bIdx, kind: 'base', isRound: true }); + for (var s = 0; s < segments; s++) { + var ss = (s + 1) % segments; + faces.push({ indices: [s, ss, apex], kind: 'lateral' }); + } + return { verts: verts, faces: faces, isRound: true }; +} + +function sphereWireframe(R, lat, lon) { + lat = lat || 6; + lon = lon || 12; + var lines = []; + for (var i = 1; i < lat; i++) { + var phi = -Math.PI/2 + Math.PI * i/lat; + var r = R * Math.cos(phi); + var y = R * Math.sin(phi); + var pts = []; + for (var j = 0; j <= 48; j++) { + var t = 2*Math.PI*j/48; + pts.push({ x: r*Math.cos(t), y: y, z: r*Math.sin(t) }); + } + lines.push({ pts: pts, kind: 'parallel' }); + } + for (var k = 0; k < lon; k++) { + var theta = 2*Math.PI*k/lon; + var pts2 = []; + for (var ii = 0; ii <= 48; ii++) { + var phi2 = -Math.PI/2 + Math.PI * ii/48; + var r2 = R * Math.cos(phi2); + pts2.push({ x: r2*Math.cos(theta), y: R*Math.sin(phi2), z: r2*Math.sin(theta) }); + } + lines.push({ pts: pts2, kind: 'meridian' }); + } + return { lines: lines, R: R }; +} + +// === Рендеринг === + +function renderMesh(mesh, M, scene, opts) { + opts = opts || {}; + var fillBase = opts.fillBase || 'rgba(252,231,243,.55)'; + var fillSide = opts.fillSide || 'rgba(219,234,254,.55)'; + var strokeVisible = opts.strokeVisible || '#0f172a'; + var strokeHidden = opts.strokeHidden || '#94a3b8'; + + var rotated = mesh.verts.map(function(v){ return vApply(M, v); }); + var projector; + if (scene.proj === 'iso') { + projector = function(v){ return projectIso(v, scene.cx, scene.cy, scene.scale); }; + } else { + projector = function(v){ return projectPersp(v, scene.camDist, scene.cx, scene.cy, scene.scale); }; + } + var projected = rotated.map(projector); + + var facesWithDepth = mesh.faces.map(function(face){ + var idx = face.indices; + var normal = null; + if (idx.length >= 3) { + var v0 = rotated[idx[0]], v1 = rotated[idx[1]], v2 = rotated[idx[2]]; + var e1 = vSub(v1, v0), e2 = vSub(v2, v0); + normal = vCross(e1, e2); + } + var avgZ = 0; + for (var i = 0; i < idx.length; i++) avgZ += rotated[idx[i]].z; + avgZ /= idx.length; + var visible = normal ? (normal.z > -1e-6) : true; + return { face: face, visible: visible, avgZ: avgZ, projected: idx.map(function(j){ return projected[j]; }) }; + }); + facesWithDepth.sort(function(a, b){ return a.avgZ - b.avgZ; }); + + var svg = ''; + for (var f = 0; f < facesWithDepth.length; f++) { + var fd = facesWithDepth[f]; + var anyMissing = false; + for (var p = 0; p < fd.projected.length; p++) if (!fd.projected[p]) { anyMissing = true; break; } + if (anyMissing) continue; + var points = fd.projected.map(function(pp){ return pp.x.toFixed(1) + ',' + pp.y.toFixed(1); }).join(' '); + var fill = fd.face.kind === 'base' ? fillBase : fillSide; + if (fd.visible) { + svg += ''; + } else { + var idx2 = fd.face.indices; + for (var ii = 0; ii < idx2.length; ii++) { + var a = projected[idx2[ii]], b = projected[idx2[(ii+1) % idx2.length]]; + if (a && b) { + svg += ''; + } + } + } + } + return svg; +} + +// === Рендеринг wireframe сферы === +function renderSphereWireframe(sphere, M, scene, opts) { + opts = opts || {}; + var strokeParallel = opts.strokeParallel || '#0891b2'; + var strokeMeridian = opts.strokeMeridian || '#0e7490'; + var projector; + if (scene.proj === 'iso') { + projector = function(v){ return projectIso(v, scene.cx, scene.cy, scene.scale); }; + } else { + projector = function(v){ return projectPersp(v, scene.camDist, scene.cx, scene.cy, scene.scale); }; + } + var svg = ''; + // outline circle: проецируем экватор после поворота (приближённо радиус как scale*R) + var R = sphere.R; + var origin = projector({x:0,y:0,z:0}); + if (origin) { + svg += ''; + } + for (var i = 0; i < sphere.lines.length; i++) { + var line = sphere.lines[i]; + var col = line.kind === 'parallel' ? strokeParallel : strokeMeridian; + var d = ''; + var prev = false; + for (var j = 0; j < line.pts.length; j++) { + var rv = vApply(M, line.pts[j]); + var pp = projector(rv); + if (!pp) { prev = false; continue; } + // back-face: если z вершины > 0 (после поворота), рисуем сплошной, иначе пунктир + var solid = rv.z > -1e-6; + // упрощённо: рисуем непрерывную полилинию, цвет в зависимости от middle z + d += (prev ? ' L' : ' M') + pp.x.toFixed(1) + ',' + pp.y.toFixed(1); + prev = true; + } + svg += ''; + } + return svg; +} + +// === Drag-to-rotate === +function attachOrbit(svgEl, scene, onChange) { + var dragging = false, lastX = 0, lastY = 0; + svgEl.style.touchAction = 'none'; + svgEl.style.cursor = 'grab'; + function onDown(e) { + dragging = true; + svgEl.style.cursor = 'grabbing'; + var p = e.touches ? e.touches[0] : e; + lastX = p.clientX; lastY = p.clientY; + e.preventDefault(); + } + function onMove(e) { + if (!dragging) return; + var p = e.touches ? e.touches[0] : e; + var dx = p.clientX - lastX, dy = p.clientY - lastY; + scene.rotY += dx * 0.012; + scene.rotX += dy * 0.012; + scene.rotX = Math.max(-1.4, Math.min(1.4, scene.rotX)); + lastX = p.clientX; lastY = p.clientY; + if (onChange) onChange(); + e.preventDefault(); + } + function onUp() { dragging = false; svgEl.style.cursor = 'grab'; } + svgEl.addEventListener('mousedown', onDown, { passive: false }); + svgEl.addEventListener('touchstart', onDown, { passive: false }); + window.addEventListener('mousemove', onMove, { passive: false }); + window.addEventListener('touchmove', onMove, { passive: false }); + window.addEventListener('mouseup', onUp); + window.addEventListener('touchend', onUp); +} + +function buildRotMatrix(scene) { + return matMul(matRotX(scene.rotX), matRotY(scene.rotY)); +} + +function presetView(scene, name, onChange) { + if (name === 'front') { scene.rotX = 0; scene.rotY = 0; } + else if (name === 'top') { scene.rotX = -Math.PI/2 + 0.01; scene.rotY = 0; } + else if (name === 'side') { scene.rotX = 0; scene.rotY = Math.PI/2; } + else if (name === 'iso') { scene.rotX = -0.35; scene.rotY = 0.7; } + if (onChange) onChange(); +} + +// === Экспорт === +window.G3D = { + vAdd: vAdd, vSub: vSub, vScale: vScale, vDot: vDot, vCross: vCross, vLen: vLen, vNorm: vNorm, + matRotX: matRotX, matRotY: matRotY, matMul: matMul, vApply: vApply, + projectPersp: projectPersp, projectIso: projectIso, + createScene: createScene, buildRotMatrix: buildRotMatrix, attachOrbit: attachOrbit, presetView: presetView, + prismMesh: prismMesh, pyramidMesh: pyramidMesh, cylinderMesh: cylinderMesh, coneMesh: coneMesh, + sphereWireframe: sphereWireframe, + renderMesh: renderMesh, renderSphereWireframe: renderSphereWireframe +}; +})(); diff --git a/frontend/textbooks/geometry_11_ch1.html b/frontend/textbooks/geometry_11_ch1.html new file mode 100644 index 0000000..c6f0800 --- /dev/null +++ b/frontend/textbooks/geometry_11_ch1.html @@ -0,0 +1,834 @@ + + + + + + + +Геометрия 11 · Раздел 1 · «Призма и цилиндр» + + + + + + + + + + + + +
+
+
+

Геометрия 11 · Раздел 1

+
Призма (правильная, прямая, наклонная, параллелепипед, куб) · цилиндр и его сечения
+
+
+ К геометрии 11 + + + +
+
+
+ +
+
+ +
+

Призма и цилиндр — главные стереометрические тела

+

Изучаем призму и цилиндр — главные стереометрические тела. Сечения, развёртки, формулы площадей и объёмов в 3D.

+
+ +
+ Прогресс по разделу +
+ 0% +
+
+
+
+ +
+
Параграфы раздела
+
+
+ +
§ 1

Призма

+
§ 2

Цилиндр

+

Финал раздела

+ +
+ +
+
+ +
Интерактивный учебник «Геометрия 11» · Раздел 1 · «Призма и цилиндр» · LearnSpace
+ +
Достижение!
+ + + + + + diff --git a/frontend/textbooks/geometry_11_ch2.html b/frontend/textbooks/geometry_11_ch2.html new file mode 100644 index 0000000..a157435 --- /dev/null +++ b/frontend/textbooks/geometry_11_ch2.html @@ -0,0 +1,834 @@ + + + + + + + +Геометрия 11 · Раздел 2 · «Пирамида и конус» + + + + + + + + + + + + +
+
+
+

Геометрия 11 · Раздел 2

+
Пирамида (правильная, усечённая) · конус (правильный, усечённый) · объёмы через 1/3
+
+
+ К геометрии 11 + + + +
+
+
+ +
+
+ +
+

Пирамида и конус — фигуры с вершиной

+

Пирамида и конус — фигуры с вершиной. Правильные и усечённые. Объём через одну треть основания на высоту.

+
+ +
+ Прогресс по разделу +
+ 0% +
+
+
+
+ +
+
Параграфы раздела
+
+
+ +
§ 3

Пирамида

+
§ 4

Конус

+

Финал раздела

+ +
+ +
+
+ +
Интерактивный учебник «Геометрия 11» · Раздел 2 · «Пирамида и конус» · LearnSpace
+ +
Достижение!
+ + + + + + diff --git a/frontend/textbooks/geometry_11_ch3.html b/frontend/textbooks/geometry_11_ch3.html new file mode 100644 index 0000000..d5e6632 --- /dev/null +++ b/frontend/textbooks/geometry_11_ch3.html @@ -0,0 +1,840 @@ + + + + + + + +Геометрия 11 · Раздел 3 · «Сфера и шар» + + + + + + + + + + + + +
+
+
+

Геометрия 11 · Раздел 3

+
Сфера и её уравнение · шар, сегменты · 5 платоновых тел
+
+
+ К геометрии 11 + + + +
+
+
+ +
+
+ +
+

Сфера, шар, правильные многогранники

+

Сфера, шар, пять платоновых тел. Уравнение сферы в координатах, шаровые сегменты, вписанные и описанные многогранники.

+
+ +
+ Прогресс по разделу +
+ 0% +
+
+
+
+ +
+
Параграфы раздела
+
+
+ +
§ 5

Сфера

+
§ 6

Шар

+
§ 7

Правильные многогранники

+

Финал раздела

+ +
+ +
+
+ +
Интерактивный учебник «Геометрия 11» · Раздел 3 · «Сфера и шар» · LearnSpace
+ +
Достижение!
+ + + + + + diff --git a/frontend/textbooks/geometry_11_ch4.html b/frontend/textbooks/geometry_11_ch4.html new file mode 100644 index 0000000..111e503 --- /dev/null +++ b/frontend/textbooks/geometry_11_ch4.html @@ -0,0 +1,846 @@ + + + + + + + +Геометрия 11 · Раздел 4 · «Повторение» + + + + + + + + + + + + +
+
+
+

Геометрия 11 · Раздел 4

+
Планиметрия · величины · координаты и векторы в 3D · построения
+
+
+ К геометрии 11 + + + +
+
+
+ +
+
+ +
+

Повторение всей геометрии

+

Повторение всей геометрии: планиметрия, площади и объёмы, координаты и векторы в 3D, классические построения.

+
+ +
+ Прогресс по разделу +
+ 0% +
+
+
+
+ +
+
Параграфы раздела
+
+
+ +
§ 8

Геометрические фигуры и их свойства

+
§ 9

Геометрические величины

+
§ 10

Координаты и векторы

+
§ 11

Геометрические построения

+

Финал раздела

+ +
+ +
+
+ +
Интерактивный учебник «Геометрия 11» · Раздел 4 · «Повторение» · LearnSpace
+ +
Достижение!
+ + + + + + diff --git a/frontend/textbooks/geometry_11_hub.html b/frontend/textbooks/geometry_11_hub.html new file mode 100644 index 0000000..9c75c8c --- /dev/null +++ b/frontend/textbooks/geometry_11_hub.html @@ -0,0 +1,433 @@ + + + + + + + + +Геометрия 11 класс — учебник + + + + + + + + + + +
+
+ +
+

Геометрия — 11 класс

+
Полный курс стереометрии: призмы, цилиндры, пирамиды, конусы, сферы и шары, правильные многогранники
+
+
+ +
+
+
+ +
+ +
+
+ +
+
+
Общий прогресс по курсу
+
Загрузка...
+
+
+ +
+ +
+ + +
+
+
Раздел 1
+
Призма и цилиндр
+
§1–§2 + Финал
+
+
+
Призма (правильная, прямая, наклонная, параллелепипед, куб). Цилиндр, его сечения, развёртка, касательная плоскость. Площади и объёмы.
+
+
Прогресс0%
+
+
+
+ Открыть раздел + +
+
+
+ + +
+
+
Раздел 2
+
Пирамида и конус
+
§3–§4 + Финал
+
+
+
Пирамида (правильная, усечённая) и конус (правильный, усечённый). Объём через одну треть основания на высоту. Боковая поверхность.
+
+
Прогресс0%
+
+
+
+ Открыть раздел + +
+
+
+ + +
+
+
Раздел 3
+
Сфера и шар
+
§5–§7 + Финал
+
+
+
Сфера: уравнение, касательная плоскость. Шар: площадь, объём, сегменты, вписанные/описанные многогранники. Правильные многогранники — 5 платоновых тел.
+
+
Прогресс0%
+
+
+
+ Открыть раздел + +
+
+
+ + +
+
+
Раздел 4
+
Повторение
+
§8–§11 + Финал
+
+
+
Повторение всей геометрии: планиметрия, геометрические величины (площади и объёмы), координаты и векторы в 3D, классические построения.
+
+
Прогресс0%
+
+
+
+ Открыть раздел + +
+
+
+ +
+ +
+ + +
+
+

В разработке (Phase 5)

+

Финал курса появится после реализации всех 11 параграфов. Здесь будут: итоговая шпаргалка по 4 разделам и 7 интегрированных боссов на синтез стереометрии и планиметрии.

+
+
+
+ +
+
+ + + +
+
+
Магистр геометрии 11
+
Прочитайте все 11 параграфов курса, чтобы получить достижение
+
+
+ +
+ +
+ Интерактивный учебник «Геометрия — 11 класс» · Л. А. Латотин и соавторы · LearnSpace +
+ + + + +