From 02846112635e0280966fb35f176d9cc0a0547f36 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Fri, 29 May 2026 14:37:07 +0300 Subject: [PATCH] =?UTF-8?q?feat(geom10=20W0):=20=D0=B8=D0=BD=D1=84=D1=80?= =?UTF-8?q?=D0=B0=20=E2=80=94=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=91=D0=94,=20stereo3d.js,=20hub=20+=204=20stub-?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Миграция 027: textbooks hub geometry-10 + 4 ребёнка (r1 blue, r2 emerald, r3 rose, r4 amber) - frontend/js/stereo3d.js: библиотека 3D-проекций (Scene, CABINET/ISOMETRIC, cube/box/prism/pyramid/tetrahedron/plane/arrow/angle, авто-видимость рёбер) - geometry_10_hub.html: 4 карточки разделов, общий прогресс, превью 4 тел через stereo3d - 4 stub-файла разделов (r1-r4) с list параграфов и плашкой 'в разработке' - backend/scripts/gen_geom10_stubs.js: генератор stub-файлов --- backend/scripts/gen_geom10_stubs.js | 224 ++++++ .../src/db/migrations/027_geometry_10_hub.sql | 32 + frontend/js/stereo3d.js | 717 ++++++++++++++++++ frontend/textbooks/geometry_10_hub.html | 370 +++++++++ frontend/textbooks/geometry_10_r1.html | 168 ++++ frontend/textbooks/geometry_10_r2.html | 168 ++++ frontend/textbooks/geometry_10_r3.html | 180 +++++ frontend/textbooks/geometry_10_r4.html | 180 +++++ 8 files changed, 2039 insertions(+) create mode 100644 backend/scripts/gen_geom10_stubs.js create mode 100644 backend/src/db/migrations/027_geometry_10_hub.sql create mode 100644 frontend/js/stereo3d.js create mode 100644 frontend/textbooks/geometry_10_hub.html create mode 100644 frontend/textbooks/geometry_10_r1.html create mode 100644 frontend/textbooks/geometry_10_r2.html create mode 100644 frontend/textbooks/geometry_10_r3.html create mode 100644 frontend/textbooks/geometry_10_r4.html diff --git a/backend/scripts/gen_geom10_stubs.js b/backend/scripts/gen_geom10_stubs.js new file mode 100644 index 0000000..5fd0e83 --- /dev/null +++ b/backend/scripts/gen_geom10_stubs.js @@ -0,0 +1,224 @@ +#!/usr/bin/env node +'use strict'; +// Генератор stub-файлов разделов Геометрии 10. W0. +// Запуск: node backend/scripts/gen_geom10_stubs.js + +const fs = require('fs'); +const path = require('path'); + +const sections = [ + { file:'geometry_10_r1.html', num:1, slug:'geometry-10-r1', + title:'Введение в стереометрию', + sub:'Пространственные фигуры · Аксиомы · Сечения', + range:'§1–§3 + Финал', wm:'△', + primary:'#2563eb', primaryD:'#1d4ed8', soft:'#dbeafe', dark:'#1e3a8a', + paras:[ + { n:1, title:'Пространственные фигуры', + sub:'Призма, пирамида, цилиндр, конус, шар. Грани, рёбра, вершины.' }, + { n:2, title:'Прямые и плоскости', + sub:'Аксиомы стереометрии (A1–A3) и их следствия. 4 способа задания плоскости.' }, + { n:3, title:'Построения сечений', + sub:'Метод следов. Сечения куба, призмы, пирамиды.' } + ] }, + { file:'geometry_10_r2.html', num:2, slug:'geometry-10-r2', + title:'Параллельность', + sub:'Прямые · Прямая и плоскость · Плоскости', + range:'§4–§6 + Финал', wm:'∥', + primary:'#059669', primaryD:'#047857', soft:'#d1fae5', dark:'#064e3b', + paras:[ + { n:4, title:'Расположение прямых в пространстве', + sub:'Пересекающиеся, параллельные, скрещивающиеся прямые. Угол между скрещивающимися.' }, + { n:5, title:'Прямая и плоскость', + sub:'Три случая. Признак параллельности прямой и плоскости.' }, + { n:6, title:'Две плоскости', + sub:'Пересекаются или параллельны. Признак параллельности плоскостей.' } + ] }, + { file:'geometry_10_r3.html', num:3, slug:'geometry-10-r3', + title:'Перпендикулярность', + sub:'Прямая ⊥ плоскость · Расстояния · Углы · Двугранный угол', + range:'§7–§10 + Финал', wm:'⊥', + primary:'#e11d48', primaryD:'#be123c', soft:'#fee2e2', dark:'#7f1d1d', + paras:[ + { n:7, title:'Перпендикулярность прямой и плоскости', + sub:'Определение, признак перпендикулярности.' }, + { n:8, title:'Расстояния', + sub:'От точки до плоскости, между параллельными плоскостями, между скрещивающимися.' }, + { n:9, title:'Угол между прямой и плоскостью', + sub:'Наклонная и её проекция. Теорема о трёх перпендикулярах.' }, + { n:10, title:'Перпендикулярность плоскостей', + sub:'Двугранный угол. Признак перпендикулярности плоскостей.' } + ] }, + { file:'geometry_10_r4.html', num:4, slug:'geometry-10-r4', + title:'Координаты и векторы', + sub:'ПДСК в пространстве · Векторы · Скалярное произведение', + range:'§11–§14 + Финал', wm:'→', + primary:'#d97706', primaryD:'#b45309', soft:'#fef3c7', dark:'#78350f', + paras:[ + { n:11, title:'Координаты в пространстве', + sub:'ПДСК. Точка (x; y; z). Расстояние между точками.' }, + { n:12, title:'Вектор. Действия над векторами', + sub:'Сложение, умножение на число, базис i, j, k. Разложение вектора.' }, + { n:13, title:'Скалярное произведение', + sub:'a · b = |a||b|cos α = x₁x₂ + y₁y₂ + z₁z₂. Условие перпендикулярности.' }, + { n:14, title:'Применение координат и векторов', + sub:'Уравнения, углы, расстояния. Векторно-координатный метод.' } + ] } +]; + +function html(s){ + const parasHtml = s.paras.map(p => ` +
+
§ ${p.n}
+
+

${p.title}

+

${p.sub}

+
+ + Будет добавлено в следующей волне реализации +
+
+
`).join('\n'); + + return ` + + + + + +Геометрия 10 · ${s.title} + + + + + + + + + + + +
+
+
+ + + К курсу геометрии 10 + +
+
+

Раздел ${s.num}. ${s.title}

+
${s.sub} · ${s.range}
+
+
+ +
+
+
+ +
+ +
+ Раздел ${s.num} +

${s.title}

+

${s.sub}. Раздел содержит ${s.paras.length} параграф${s.paras.length===1?'':(s.paras.length<5?'а':'ов')} и финальный этап с боссами.

+
+ +
+${parasHtml} +
+ + + +
+ + + + + + + +`; +} + +const outDir = path.join(__dirname, '..', '..', 'frontend', 'textbooks'); +for (const s of sections){ + const fp = path.join(outDir, s.file); + fs.writeFileSync(fp, html(s), 'utf8'); + console.log('Wrote:', fp); +} +console.log('Done.'); diff --git a/backend/src/db/migrations/027_geometry_10_hub.sql b/backend/src/db/migrations/027_geometry_10_hub.sql new file mode 100644 index 0000000..d1c19b3 --- /dev/null +++ b/backend/src/db/migrations/027_geometry_10_hub.sql @@ -0,0 +1,32 @@ +-- Geometry 10 hub migration. +-- Adds hub row + 4 section children for Геометрия 10 (Латотин/Чеботаревский/Горбунова, 2020). +-- Pattern mirrors 023_algebra_10_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-10', 'math', 10, 'Геометрия — 10 класс', '', + 'Полный курс стереометрии 10 класса по учебнику Л. А. Латотина и Б. Д. Чеботаревского: введение в стереометрию (аксиомы, сечения), параллельность прямых и плоскостей, перпендикулярность, координаты и векторы в пространстве. 4 раздела, 14 параграфов, ~140 интерактивов, 24 босса. Все 3D-фигуры — через библиотеку stereo3d.js.', + 'geometry_10_hub.html', 14, 'blue', 9, 1); + +-- 2. Section children. +INSERT INTO textbooks + (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug) +VALUES + ('geometry-10-r1', 'math', 10, 'Геометрия 10 · Введение в стереометрию', + '', + '§1–§3: пространственные фигуры (призма, пирамида, цилиндр, конус, шар), аксиомы стереометрии и их следствия, построение сечений многогранников методом следов.', + 'geometry_10_r1.html', 3, 'blue', 1, 1, 'geometry-10'), + ('geometry-10-r2', 'math', 10, 'Геометрия 10 · Параллельность', + '', + '§4–§6: взаимное расположение прямых в пространстве (пересекающиеся, параллельные, скрещивающиеся), взаимное расположение прямой и плоскости, взаимное расположение двух плоскостей, признаки параллельности.', + 'geometry_10_r2.html', 3, 'emerald', 2, 1, 'geometry-10'), + ('geometry-10-r3', 'math', 10, 'Геометрия 10 · Перпендикулярность', + '', + '§7–§10: перпендикулярность прямой и плоскости, расстояния в пространстве, угол между прямой и плоскостью (теорема о трёх перпендикулярах), перпендикулярность плоскостей (двугранный угол).', + 'geometry_10_r3.html', 4, 'rose', 3, 1, 'geometry-10'), + ('geometry-10-r4', 'math', 10, 'Геометрия 10 · Координаты и векторы', + '', + '§11–§14: прямоугольная система координат в пространстве, векторы и действия над ними, скалярное произведение, применение векторно-координатного метода к решению задач.', + 'geometry_10_r4.html', 4, 'amber', 4, 1, 'geometry-10'); diff --git a/frontend/js/stereo3d.js b/frontend/js/stereo3d.js new file mode 100644 index 0000000..89373e9 --- /dev/null +++ b/frontend/js/stereo3d.js @@ -0,0 +1,717 @@ +/* stereo3d.js — библиотека 3D-проекций для Геометрии 10 (стереометрия). + * + * Чистый SVG-движок: проекция 3D → 2D через cabinet / isometric. + * Не зависит от Three.js, Canvas, WebGL. Возвращает строки SVG. + * + * Публичный API: window.STEREO3D = { Scene, views, util }. + * + * Использование: + * const scene = new STEREO3D.Scene(380, 320, {view:'CABINET', scale:50}); + * scene.addCube({center:[0,0,0], size:2, label:'ABCDA1B1C1D1'}); + * scene.addLabel('M', [0, 0, 1.2]); + * const svgHtml = scene.render(); + * + * Соглашение об осях: x → вправо, y → вглубь, z → вверх. + * В SVG y растёт вниз, мы инвертируем при проекции. + */ +(function(){ +'use strict'; + +if (window.STEREO3D && window.STEREO3D.__installed) return; +const S = window.STEREO3D = window.STEREO3D || {}; +S.__installed = true; + +/* ============================================================ */ +/* ВЕКТОРНАЯ АЛГЕБРА */ +/* ============================================================ */ + +function v3(a,b,c){ return [a,b,c]; } +function add(a,b){ return [a[0]+b[0], a[1]+b[1], a[2]+b[2]]; } +function sub(a,b){ return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]; } +function scale3(a,k){ return [a[0]*k, a[1]*k, a[2]*k]; } +function dot(a,b){ return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; } +function cross(a,b){ + return [ + a[1]*b[2] - a[2]*b[1], + a[2]*b[0] - a[0]*b[2], + a[0]*b[1] - a[1]*b[0] + ]; +} +function len3(a){ return Math.hypot(a[0], a[1], a[2]); } +function norm3(a){ const L = len3(a)||1; return [a[0]/L, a[1]/L, a[2]/L]; } + +function rotX(p, a){ + const c = Math.cos(a), s = Math.sin(a); + return [p[0], p[1]*c - p[2]*s, p[1]*s + p[2]*c]; +} +function rotY(p, a){ + const c = Math.cos(a), s = Math.sin(a); + return [p[0]*c + p[2]*s, p[1], -p[0]*s + p[2]*c]; +} +function rotZ(p, a){ + const c = Math.cos(a), s = Math.sin(a); + return [p[0]*c - p[1]*s, p[0]*s + p[1]*c, p[2]]; +} + +S.util = { v3, add, sub, scale3, dot, cross, len3, norm3, rotX, rotY, rotZ }; + +/* ============================================================ */ +/* ПРОЕКЦИИ */ +/* ============================================================ */ + +const VIEWS = S.views = { + CABINET: { + project: function(p){ + // x → вправо, y → вглубь под 30° со сжатием 0.5, z → вверх + const a = Math.PI / 6; + return [ + p[0] + 0.5 * p[1] * Math.cos(a), + -p[2] + 0.5 * p[1] * Math.sin(a) + ]; + }, + cameraDir: [0.5*Math.cos(Math.PI/6), 1, 0.5*Math.sin(Math.PI/6)] + }, + ISOMETRIC: { + project: function(p){ + // классическая изометрия: оси под 30° к горизонтали + const c = Math.cos(Math.PI/6), s = Math.sin(Math.PI/6); + return [ + (p[0] - p[1]) * c, + -p[2] + (p[0] + p[1]) * s + ]; + }, + cameraDir: [1, 1, 1] + } +}; + +/* ============================================================ */ +/* МАТЕРИАЛЫ */ +/* ============================================================ */ + +const MAT = { + edge: { stroke:'#1e293b', width:1.8 }, + edgeHidden:{ stroke:'#94a3b8', width:1.2, dash:'4 3' }, + edgeHi: { stroke:'#dc2626', width:2.8 }, + edgePar: { stroke:'#10b981', width:2.4 }, + edgePerp: { stroke:'#7c3aed', width:2.4 }, + face: { fill:'#dbeafe', opacity:0.35, stroke:'#1e3a8a', strokeWidth:1.4 }, + plane: { fill:'#3b82f6', opacity:0.18, stroke:'#1e3a8a', strokeWidth:1.2, dash:'6 4' }, + vertex: { fill:'#1e293b', r:3.5 } +}; + +/* ============================================================ */ +/* Scene */ +/* ============================================================ */ + +class Scene { + constructor(W, H, opts){ + opts = opts || {}; + this.W = W; + this.H = H; + this.scale = opts.scale || 40; + this.center = opts.center || [W/2, H/2]; + this.view = (typeof opts.view === 'string') ? (VIEWS[opts.view] || VIEWS.CABINET) : (opts.view || VIEWS.CABINET); + this.rotX = opts.rotX || 0; + this.rotY = opts.rotY || 0; + this.bg = opts.bg || '#fafafa'; + this.border = (opts.border !== undefined) ? opts.border : '1px solid #e2e8f0'; + this.radius = opts.radius || 10; + this.items = []; // отрисовка в порядке Z (заполняется в render) + this._id = opts.id || ('s3d-' + Math.random().toString(36).slice(2,7)); + } + + // Применить повороты + проекцию + project3(p){ + let q = p; + if (this.rotX) q = rotX(q, this.rotX); + if (this.rotY) q = rotY(q, this.rotY); + const [px, py] = this.view.project(q); + return [this.center[0] + this.scale * px, this.center[1] + this.scale * py]; + } + + // То же, но возвращает все три величины (для сортировки по глубине) + project3Depth(p){ + let q = p; + if (this.rotX) q = rotX(q, this.rotX); + if (this.rotY) q = rotY(q, this.rotY); + const [px, py] = this.view.project(q); + // глубина = проекция на направление камеры + const cam = this.view.cameraDir; + const depth = dot(q, cam); + return { x: this.center[0] + this.scale * px, y: this.center[1] + this.scale * py, depth }; + } + + /* === Добавление элементов === */ + + addEdge(p1, p2, opts){ + this.items.push({ kind:'edge', p1, p2, opts: opts || {} }); + return this; + } + addFace(points, opts){ + this.items.push({ kind:'face', points, opts: opts || {} }); + return this; + } + addVertex(p, label, opts){ + this.items.push({ kind:'vertex', p, label, opts: opts || {} }); + return this; + } + addLabel(label, p, opts){ + this.items.push({ kind:'label', p, label, opts: opts || {} }); + return this; + } + addArrow(p1, p2, opts){ + this.items.push({ kind:'arrow', p1, p2, opts: opts || {} }); + return this; + } + addPlane(point, normal, opts){ + this.items.push({ kind:'plane', point, normal, opts: opts || {} }); + return this; + } + addAngleMark(vertex, p1, p2, opts){ + this.items.push({ kind:'angle', vertex, p1, p2, opts: opts || {} }); + return this; + } + addRightAngleMark(vertex, p1, p2, opts){ + this.items.push({ kind:'rightAngle', vertex, p1, p2, opts: opts || {} }); + return this; + } + addDashedSegment(p1, p2, opts){ + return this.addEdge(p1, p2, Object.assign({hidden:true}, opts||{})); + } + + /* === Готовые тела === */ + + // Куб ABCDA1B1C1D1 — стандартное соглашение учебника + addCube(params){ + params = params || {}; + const c = params.center || [0,0,0]; + const s = (params.size || 2) / 2; + const labels = params.labels !== false; + const labelMap = params.labelMap || ['A','B','C','D','A_1','B_1','C_1','D_1']; + // Нижняя грань (z = -s): A B C D против часовой при взгляде сверху + const A = add(c, [-s, -s, -s]); + const B = add(c, [+s, -s, -s]); + const C = add(c, [+s, +s, -s]); + const D = add(c, [-s, +s, -s]); + const A1 = add(c, [-s, -s, +s]); + const B1 = add(c, [+s, -s, +s]); + const C1 = add(c, [+s, +s, +s]); + const D1 = add(c, [-s, +s, +s]); + const verts = [A,B,C,D,A1,B1,C1,D1]; + const faces = [ + [0,1,2,3], // нижняя + [4,5,6,7], // верхняя + [0,1,5,4], // передняя + [1,2,6,5], // правая + [2,3,7,6], // задняя + [3,0,4,7] // левая + ]; + const edges = [ + [0,1],[1,2],[2,3],[3,0], // низ + [4,5],[5,6],[6,7],[7,4], // верх + [0,4],[1,5],[2,6],[3,7] // вертикали + ]; + return this._addPolyhedron(verts, faces, edges, + labels ? labelMap.map((L,i)=>({label:L, point:verts[i]})) : [], + params); + } + + // Параллелепипед с произвольными размерами + addBox(params){ + params = params || {}; + const c = params.center || [0,0,0]; + const sz = params.size || [2,2,2]; + const [ax, ay, az] = [sz[0]/2, sz[1]/2, sz[2]/2]; + const labelMap = params.labelMap || ['A','B','C','D','A_1','B_1','C_1','D_1']; + const A = add(c, [-ax, -ay, -az]); + const B = add(c, [+ax, -ay, -az]); + const C = add(c, [+ax, +ay, -az]); + const D = add(c, [-ax, +ay, -az]); + const A1 = add(c, [-ax, -ay, +az]); + const B1 = add(c, [+ax, -ay, +az]); + const C1 = add(c, [+ax, +ay, +az]); + const D1 = add(c, [-ax, +ay, +az]); + const verts = [A,B,C,D,A1,B1,C1,D1]; + const faces = [ + [0,1,2,3],[4,5,6,7],[0,1,5,4],[1,2,6,5],[2,3,7,6],[3,0,4,7] + ]; + const edges = [ + [0,1],[1,2],[2,3],[3,0], + [4,5],[5,6],[6,7],[7,4], + [0,4],[1,5],[2,6],[3,7] + ]; + return this._addPolyhedron(verts, faces, edges, + params.labels !== false ? labelMap.map((L,i)=>({label:L, point:verts[i]})) : [], + params); + } + + // Тетраэдр (правильный или по 4 вершинам) + addTetrahedron(params){ + params = params || {}; + let verts; + if (params.vertices){ + verts = params.vertices; + } else { + const c = params.center || [0,0,0]; + const r = params.size || 1.6; + // правильный тетраэдр: вершины на сфере + verts = [ + add(c, [ r, r, r]), + add(c, [ r, -r, -r]), + add(c, [-r, r, -r]), + add(c, [-r, -r, r]) + ]; + } + const labelMap = params.labelMap || ['A','B','C','D']; + const faces = [[0,1,2],[0,1,3],[0,2,3],[1,2,3]]; + const edges = [[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]]; + return this._addPolyhedron(verts, faces, edges, + params.labels !== false ? labelMap.map((L,i)=>({label:L, point:verts[i]})) : [], + params); + } + + // n-угольная призма (база — правильный n-угольник в плоскости xy) + addPrism(params){ + params = params || {}; + const n = params.n || 4; + const r = params.baseRadius || 1.4; + const h = params.height || 2; + const c = params.center || [0,0,0]; + const bottom = []; + const top = []; + for (let i = 0; i < n; i++){ + const a = 2 * Math.PI * i / n - Math.PI / 2; + bottom.push(add(c, [r * Math.cos(a), r * Math.sin(a), -h/2])); + top.push(add(c, [r * Math.cos(a), r * Math.sin(a), +h/2])); + } + const verts = bottom.concat(top); + const faces = []; + const bottomIdx = []; for (let i = 0; i < n; i++) bottomIdx.push(i); + const topIdx = []; for (let i = 0; i < n; i++) topIdx.push(i + n); + faces.push(bottomIdx); + faces.push(topIdx.slice().reverse()); + for (let i = 0; i < n; i++){ + const j = (i + 1) % n; + faces.push([i, j, j+n, i+n]); + } + const edges = []; + for (let i = 0; i < n; i++){ + const j = (i + 1) % n; + edges.push([i, j]); + edges.push([i+n, j+n]); + edges.push([i, i+n]); + } + return this._addPolyhedron(verts, faces, edges, [], params); + } + + // n-угольная пирамида + addPyramid(params){ + params = params || {}; + const n = params.n || 4; + const r = params.baseRadius || 1.4; + const h = params.height || 2; + const c = params.center || [0,0,0]; + const base = []; + for (let i = 0; i < n; i++){ + const a = 2 * Math.PI * i / n - Math.PI / 2; + base.push(add(c, [r * Math.cos(a), r * Math.sin(a), -h/2])); + } + const apex = add(c, [0, 0, h/2]); + const verts = base.concat([apex]); + const apexIdx = n; + const faces = []; + faces.push(base.map((_,i)=>i)); + for (let i = 0; i < n; i++){ + const j = (i + 1) % n; + faces.push([i, j, apexIdx]); + } + const edges = []; + for (let i = 0; i < n; i++){ + const j = (i + 1) % n; + edges.push([i, j]); + edges.push([i, apexIdx]); + } + return this._addPolyhedron(verts, faces, edges, [], params); + } + + // Конус (приближение многогранником) + addCone(params){ + params = params || {}; + const n = params.segments || 24; + return this.addPyramid(Object.assign({n}, params)); + } + + // Цилиндр (приближение призмой) + addCylinder(params){ + params = params || {}; + const n = params.segments || 24; + return this.addPrism(Object.assign({n}, params)); + } + + // Сфера (упрощённо — окружность по силуэту) + addSphere(params){ + params = params || {}; + const c = params.center || [0,0,0]; + const r = params.radius || 1.4; + this.items.push({ kind:'sphere', center:c, radius:r, opts:params }); + return this; + } + + // Общий конструктор многогранника + _addPolyhedron(verts, faces, edges, vertLabels, params){ + params = params || {}; + const color = params.color || '#dbeafe'; + const opacity = (params.opacity !== undefined) ? params.opacity : 0.35; + const showFaces = params.showFaces !== false; + const showHidden = params.showHidden !== false; + this.items.push({ + kind:'polyhedron', + verts, faces, edges, vertLabels, + color, opacity, showFaces, showHidden, + hiOptsByEdge: params.highlightEdges || null, + hiOptsByFace: params.highlightFaces || null, + labels: vertLabels, + labelStyle: params.labelStyle || {} + }); + return this; + } + + /* === Рендер === */ + + render(){ + const W = this.W, H = this.H; + const open = ''; + const defs = this._defs(); + + // Собираем «плоские» элементы для отрисовки: faces (back), edges (hidden), faces (front), edges (visible), verts, labels. + const out = []; + + // Сначала — обрабатываем все объекты в two passes: + // pass 1: задние грани + невидимые рёбра + // pass 2: передние грани + видимые рёбра + // pass 3: вершины + // pass 4: подписи + декораторы (углы, стрелки) + + const flat = []; // {type, depth, svg} + + for (const it of this.items){ + if (it.kind === 'polyhedron'){ + this._polyhedronToFlat(it, flat); + } else if (it.kind === 'edge'){ + const a = this.project3Depth(it.p1); + const b = this.project3Depth(it.p2); + const mid = (a.depth + b.depth) / 2; + flat.push({ pass: it.opts.hidden ? 1 : 4, depth: mid, svg: this._edgeSvg(a, b, it.opts) }); + } else if (it.kind === 'face'){ + const pts = it.points.map(p => this.project3Depth(p)); + const mid = pts.reduce((s,p)=>s+p.depth, 0)/pts.length; + flat.push({ pass: 2, depth: mid, svg: this._faceSvg(pts, it.opts) }); + } else if (it.kind === 'vertex'){ + const a = this.project3Depth(it.p); + flat.push({ pass: 5, depth: a.depth, svg: this._vertexSvg(a, it.label, it.opts) }); + } else if (it.kind === 'label'){ + const a = this.project3Depth(it.p); + flat.push({ pass: 6, depth: a.depth, svg: this._labelSvg(a, it.label, it.opts) }); + } else if (it.kind === 'arrow'){ + const a = this.project3Depth(it.p1); + const b = this.project3Depth(it.p2); + flat.push({ pass: 4, depth: (a.depth+b.depth)/2, svg: this._arrowSvg(a, b, it.opts) }); + } else if (it.kind === 'plane'){ + flat.push({ pass: 2, depth: dot(it.point, this.view.cameraDir), svg: this._planeSvg(it.point, it.normal, it.opts) }); + } else if (it.kind === 'angle'){ + const v = this.project3Depth(it.vertex); + flat.push({ pass: 6, depth: v.depth, svg: this._angleMarkSvg(it.vertex, it.p1, it.p2, it.opts) }); + } else if (it.kind === 'rightAngle'){ + const v = this.project3Depth(it.vertex); + flat.push({ pass: 6, depth: v.depth, svg: this._rightAngleMarkSvg(it.vertex, it.p1, it.p2, it.opts) }); + } else if (it.kind === 'sphere'){ + const c = this.project3Depth(it.center); + const r = this.scale * it.radius; + flat.push({ pass: 2, depth: c.depth, svg: + '' + + '' + }); + } + } + + // Сортируем по pass, потом по depth (меньшая глубина = дальше). + flat.sort((a,b)=> (a.pass - b.pass) || (a.depth - b.depth)); + + let body = ''; + for (const f of flat) body += f.svg; + + return open + defs + body + ''; + } + + _defs(){ + return '' + + '' + + '' + + '' + + ''; + } + + _polyhedronToFlat(it, flat){ + const proj = it.verts.map(p => this.project3Depth(p)); + // Для каждой грани вычисляем нормаль в 3D и определяем — обращена к камере или нет. + const cam = this.view.cameraDir; + const faceData = it.faces.map((idxs)=>{ + const p0 = it.verts[idxs[0]], p1 = it.verts[idxs[1]], p2 = it.verts[idxs[2]]; + // нормаль через cross product. Для выпуклых тел нужно учесть центр тела. + const n = norm3(cross(sub(p1, p0), sub(p2, p0))); + // центр грани + let cx=0, cy=0, cz=0; + for (const i of idxs){ cx+=it.verts[i][0]; cy+=it.verts[i][1]; cz+=it.verts[i][2]; } + const ctr = [cx/idxs.length, cy/idxs.length, cz/idxs.length]; + // если нормаль смотрит ВНУТРЬ тела (к началу), инвертируем + // (примерное предположение что центр тела — среднее всех вершин) + let bx=0, by=0, bz=0; + for (const v of it.verts){ bx+=v[0]; by+=v[1]; bz+=v[2]; } + const body = [bx/it.verts.length, by/it.verts.length, bz/it.verts.length]; + const outward = sub(ctr, body); + if (dot(n, outward) < 0){ n[0]=-n[0]; n[1]=-n[1]; n[2]=-n[2]; } + // повернуть нормаль теми же поворотами что и точки + let rn = n; + if (this.rotX) rn = rotX(rn, this.rotX); + if (this.rotY) rn = rotY(rn, this.rotY); + const visible = dot(rn, cam) > 0; + const meanDepth = idxs.reduce((s,i)=>s+proj[i].depth, 0)/idxs.length; + return { idxs, visible, meanDepth, n: rn }; + }); + + // Какие рёбра видимые (хоть одна смежная грань видима) + const edgeFaceMap = new Map(); + function edgeKey(a, b){ return Math.min(a,b)+'-'+Math.max(a,b); } + for (let fi = 0; fi < it.faces.length; fi++){ + const fc = it.faces[fi]; + for (let i = 0; i < fc.length; i++){ + const k = edgeKey(fc[i], fc[(i+1)%fc.length]); + if (!edgeFaceMap.has(k)) edgeFaceMap.set(k, []); + edgeFaceMap.get(k).push(fi); + } + } + const edgeVisible = new Map(); + for (const [k, fis] of edgeFaceMap){ + const vis = fis.some(fi => faceData[fi].visible); + edgeVisible.set(k, vis); + } + + // 1) задние грани (полупрозрачные) — если showFaces + if (it.showFaces){ + for (const fd of faceData){ + if (!fd.visible){ + const pts = fd.idxs.map(i => proj[i]); + flat.push({ pass:2, depth:fd.meanDepth, svg: this._faceSvg(pts, { fill: it.color, opacity: it.opacity * 0.5, stroke:'none' }) }); + } + } + } + + // 2) невидимые рёбра — пунктир + if (it.showHidden){ + for (const e of it.edges){ + const k = edgeKey(e[0], e[1]); + if (!edgeVisible.get(k)){ + const a = proj[e[0]], b = proj[e[1]]; + flat.push({ pass:1, depth:(a.depth+b.depth)/2, svg: this._edgeSvg(a, b, { hidden:true }) }); + } + } + } + + // 3) передние грани (полупрозрачные) — поверх задних рёбер + if (it.showFaces){ + for (const fd of faceData){ + if (fd.visible){ + const pts = fd.idxs.map(i => proj[i]); + flat.push({ pass:3, depth:fd.meanDepth, svg: this._faceSvg(pts, { fill: it.color, opacity: it.opacity, stroke:'none' }) }); + } + } + } + + // 4) видимые рёбра — сплошные + for (const e of it.edges){ + const k = edgeKey(e[0], e[1]); + if (edgeVisible.get(k)){ + const a = proj[e[0]], b = proj[e[1]]; + let style = {}; + if (it.hiOptsByEdge){ + for (const h of it.hiOptsByEdge){ + if ((h.edge[0] === e[0] && h.edge[1] === e[1]) || (h.edge[0] === e[1] && h.edge[1] === e[0])){ + style = h; break; + } + } + } + flat.push({ pass:4, depth:(a.depth+b.depth)/2, svg: this._edgeSvg(a, b, style) }); + } + } + + // 5) подписи вершин + if (it.labels && it.labels.length){ + for (let vi = 0; vi < it.labels.length; vi++){ + const lab = it.labels[vi]; + const proj1 = proj[vi]; + flat.push({ pass:6, depth:proj1.depth, svg: this._vertexLabelSvg(proj1, lab.label, it.labelStyle) }); + } + } + } + + /* === SVG примитивы === */ + + _edgeSvg(a, b, opts){ + opts = opts || {}; + let stroke, width, dash; + if (opts.hidden){ stroke = opts.stroke || MAT.edgeHidden.stroke; width = opts.width || MAT.edgeHidden.width; dash = opts.dash || MAT.edgeHidden.dash; } + else if (opts.highlight === 'par'){ stroke = MAT.edgePar.stroke; width = MAT.edgePar.width; } + else if (opts.highlight === 'perp'){ stroke = MAT.edgePerp.stroke; width = MAT.edgePerp.width; } + else if (opts.highlight){ stroke = MAT.edgeHi.stroke; width = MAT.edgeHi.width; } + else { stroke = opts.stroke || MAT.edge.stroke; width = opts.width || MAT.edge.width; dash = opts.dash; } + let s = ' p.x.toFixed(2)+','+p.y.toFixed(2)).join(' '); + const fill = opts.fill || MAT.face.fill; + const op = (opts.opacity !== undefined) ? opts.opacity : MAT.face.opacity; + const stroke = opts.stroke || 'none'; + const sw = opts.strokeWidth || MAT.face.strokeWidth; + return ''; + } + + _vertexSvg(p, label, opts){ + opts = opts || {}; + const r = opts.r || MAT.vertex.r; + const fill = opts.fill || MAT.vertex.fill; + let s = ''; + if (label) s += this._vertexLabelSvg(p, label, opts); + return s; + } + + _vertexLabelSvg(p, label, opts){ + opts = opts || {}; + const dx = (opts.dx !== undefined) ? opts.dx : 8; + const dy = (opts.dy !== undefined) ? opts.dy : -6; + const fs = opts.fontSize || 14; + const fill = opts.color || '#1e293b'; + // если в label есть _, форматируем подстрочный индекс + const html = formatLabel(label); + return ''+html+''; + } + + _labelSvg(p, label, opts){ + opts = opts || {}; + const dx = (opts.dx !== undefined) ? opts.dx : 0; + const dy = (opts.dy !== undefined) ? opts.dy : 0; + const fs = opts.fontSize || 13; + const fill = opts.color || '#475569'; + const html = formatLabel(label); + const anchor = opts.anchor || 'middle'; + return ''+html+''; + } + + _arrowSvg(a, b, opts){ + opts = opts || {}; + const stroke = opts.color || '#1e293b'; + const w = opts.width || 2; + let s = ''+formatLabel(opts.label)+''; + } + return s; + } + + _planeSvg(point, normal, opts){ + opts = opts || {}; + const sz = opts.size || 3; + const fill = opts.fill || MAT.plane.fill; + const op = (opts.opacity !== undefined) ? opts.opacity : MAT.plane.opacity; + // строим два ортогональных вектора в плоскости нормали + const n = norm3(normal); + let u; + if (Math.abs(n[0]) < 0.9) u = norm3(cross(n, [1,0,0])); + else u = norm3(cross(n, [0,1,0])); + const w = norm3(cross(n, u)); + const corners = [ + add(point, add(scale3(u, +sz), scale3(w, +sz))), + add(point, add(scale3(u, +sz), scale3(w, -sz))), + add(point, add(scale3(u, -sz), scale3(w, -sz))), + add(point, add(scale3(u, -sz), scale3(w, +sz))) + ].map(p => this.project3(p)); + const d = corners.map(p => p[0].toFixed(2)+','+p[1].toFixed(2)).join(' '); + let s = ''+formatLabel(opts.label)+''; + } + return s; + } + + _angleMarkSvg(vertex, p1, p2, opts){ + opts = opts || {}; + const r3 = opts.r || 0.35; + const u = norm3(sub(p1, vertex)); + const v = norm3(sub(p2, vertex)); + // строим дугу в 3D через интерполяцию по углу + const steps = 12; + const dotUV = Math.max(-1, Math.min(1, dot(u, v))); + const ang = Math.acos(dotUV); + const w = norm3(sub(v, scale3(u, dotUV))); + const pts = []; + for (let i = 0; i <= steps; i++){ + const t = ang * i / steps; + const dir = add(scale3(u, Math.cos(t)), scale3(w, Math.sin(t))); + pts.push(this.project3(add(vertex, scale3(dir, r3)))); + } + const d = 'M ' + pts.map(p => p[0].toFixed(2)+','+p[1].toFixed(2)).join(' L '); + let s = ''; + if (opts.label){ + const mid = pts[Math.floor(pts.length/2)]; + s += ''+formatLabel(opts.label)+''; + } + return s; + } + + _rightAngleMarkSvg(vertex, p1, p2, opts){ + opts = opts || {}; + const sz = opts.size || 0.2; + const u = norm3(sub(p1, vertex)); + const v = norm3(sub(p2, vertex)); + const a = add(vertex, scale3(u, sz)); + const b = add(vertex, scale3(add(u, v), sz)); + const c = add(vertex, scale3(v, sz)); + const A = this.project3(a), B = this.project3(b), C = this.project3(c); + return ''; + } +} + +/* === Форматирование подписей: A_1, B_1, alpha -> греческие, и т.д. === */ +function formatLabel(s){ + if (s == null) return ''; + let out = String(s); + // греческие + out = out.replace(/\balpha\b/g, 'α') + .replace(/\bbeta\b/g, 'β') + .replace(/\bgamma\b/g, 'γ') + .replace(/\bphi\b/g, 'φ') + .replace(/\btheta\b/g, 'θ') + .replace(/\bpi\b/g, 'π'); + // одиночные индексы _1, _2 — превращаем в + out = out.replace(/_(\d)/g, '$1'); + // _{abc} — тоже + out = out.replace(/_\{([^}]+)\}/g, '$1'); + return out; +} + +S.Scene = Scene; +S.formatLabel = formatLabel; + +})(); diff --git a/frontend/textbooks/geometry_10_hub.html b/frontend/textbooks/geometry_10_hub.html new file mode 100644 index 0000000..42389e7 --- /dev/null +++ b/frontend/textbooks/geometry_10_hub.html @@ -0,0 +1,370 @@ + + + + + + + + +Геометрия 10 класс — учебник + + + + + + + + + + + +
+
+ +
+

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

+
Стереометрия · 3D-фигуры · Координаты и векторы
+
+
+ +
+
+
+ +
+ +
+
+
+
Общий прогресс по курсу
+
Загрузка...
+
+
+ +
+ +
+
Куб
+
Пирамида
+
Призма
+
Тетраэдр
+
+ +
+ + +
+
+
Раздел 1
+
Введение в стереометрию
+
§1–§3 + Финал
+
+
+
Пространственные фигуры (призма, пирамида, цилиндр, конус, шар), аксиомы стереометрии и их следствия, метод сечений многогранников.
+
+
Прогресс0%
+
+
+
+ Открыть раздел + +
+
+
+ + +
+
+
Раздел 2
+
Параллельность
+
§4–§6 + Финал
+
+
+
Взаимное расположение прямых в пространстве (скрещивающиеся), прямой и плоскости, двух плоскостей. Признаки параллельности.
+
+
Прогресс0%
+
+
+
+ Открыть раздел + +
+
+
+ + +
+
+
Раздел 3
+
Перпендикулярность
+
§7–§10 + Финал
+
+
+
Перпендикулярность прямой и плоскости, расстояния в пространстве, угол между прямой и плоскостью (теорема о трёх перпендикулярах), двугранный угол.
+
+
Прогресс0%
+
+
+
+ Открыть раздел + +
+
+
+ + +
+
+
Раздел 4
+
Координаты и векторы
+
§11–§14 + Финал
+
+
+
Прямоугольная система координат в пространстве, векторы и действия над ними, скалярное произведение, применение векторно-координатного метода.
+
+
Прогресс0%
+
+
+
+ Открыть раздел + +
+
+
+ +
+ +
+
+ + + +
+
+
Магистр геометрии 10
+
Прочитайте все 14 параграфов четырёх разделов, чтобы получить достижение
+
+
+ +
+ +
+ Интерактивный учебник «Геометрия — 10 класс» · Л. А. Латотин, Б. Д. Чеботаревский, И. В. Горбунова · LearnSpace +
+ + + + + diff --git a/frontend/textbooks/geometry_10_r1.html b/frontend/textbooks/geometry_10_r1.html new file mode 100644 index 0000000..7544f98 --- /dev/null +++ b/frontend/textbooks/geometry_10_r1.html @@ -0,0 +1,168 @@ + + + + + + +Геометрия 10 · Введение в стереометрию + + + + + + + + + + + +
+
+ +
+

Раздел 1. Введение в стереометрию

+
Пространственные фигуры · Аксиомы · Сечения · §1–§3 + Финал
+
+
+ +
+
+
+ +
+ +
+ Раздел 1 +

Введение в стереометрию

+

Пространственные фигуры · Аксиомы · Сечения. Раздел содержит 3 параграфа и финальный этап с боссами.

+
+ +
+ +
+
§ 1
+
+

Пространственные фигуры

+

Призма, пирамида, цилиндр, конус, шар. Грани, рёбра, вершины.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 2
+
+

Прямые и плоскости

+

Аксиомы стереометрии (A1–A3) и их следствия. 4 способа задания плоскости.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 3
+
+

Построения сечений

+

Метод следов. Сечения куба, призмы, пирамиды.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+
+ + + +
+ +
+ Геометрия — 10 класс · Раздел 1 · LearnSpace +
+ + + + + diff --git a/frontend/textbooks/geometry_10_r2.html b/frontend/textbooks/geometry_10_r2.html new file mode 100644 index 0000000..5172521 --- /dev/null +++ b/frontend/textbooks/geometry_10_r2.html @@ -0,0 +1,168 @@ + + + + + + +Геометрия 10 · Параллельность + + + + + + + + + + + +
+
+ +
+

Раздел 2. Параллельность

+
Прямые · Прямая и плоскость · Плоскости · §4–§6 + Финал
+
+
+ +
+
+
+ +
+ +
+ Раздел 2 +

Параллельность

+

Прямые · Прямая и плоскость · Плоскости. Раздел содержит 3 параграфа и финальный этап с боссами.

+
+ +
+ +
+
§ 4
+
+

Расположение прямых в пространстве

+

Пересекающиеся, параллельные, скрещивающиеся прямые. Угол между скрещивающимися.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 5
+
+

Прямая и плоскость

+

Три случая. Признак параллельности прямой и плоскости.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 6
+
+

Две плоскости

+

Пересекаются или параллельны. Признак параллельности плоскостей.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+
+ + + +
+ +
+ Геометрия — 10 класс · Раздел 2 · LearnSpace +
+ + + + + diff --git a/frontend/textbooks/geometry_10_r3.html b/frontend/textbooks/geometry_10_r3.html new file mode 100644 index 0000000..6036e0d --- /dev/null +++ b/frontend/textbooks/geometry_10_r3.html @@ -0,0 +1,180 @@ + + + + + + +Геометрия 10 · Перпендикулярность + + + + + + + + + + + +
+
+ +
+

Раздел 3. Перпендикулярность

+
Прямая ⊥ плоскость · Расстояния · Углы · Двугранный угол · §7–§10 + Финал
+
+
+ +
+
+
+ +
+ +
+ Раздел 3 +

Перпендикулярность

+

Прямая ⊥ плоскость · Расстояния · Углы · Двугранный угол. Раздел содержит 4 параграфа и финальный этап с боссами.

+
+ +
+ +
+
§ 7
+
+

Перпендикулярность прямой и плоскости

+

Определение, признак перпендикулярности.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 8
+
+

Расстояния

+

От точки до плоскости, между параллельными плоскостями, между скрещивающимися.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 9
+
+

Угол между прямой и плоскостью

+

Наклонная и её проекция. Теорема о трёх перпендикулярах.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 10
+
+

Перпендикулярность плоскостей

+

Двугранный угол. Признак перпендикулярности плоскостей.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+
+ + + +
+ +
+ Геометрия — 10 класс · Раздел 3 · LearnSpace +
+ + + + + diff --git a/frontend/textbooks/geometry_10_r4.html b/frontend/textbooks/geometry_10_r4.html new file mode 100644 index 0000000..47984fa --- /dev/null +++ b/frontend/textbooks/geometry_10_r4.html @@ -0,0 +1,180 @@ + + + + + + +Геометрия 10 · Координаты и векторы + + + + + + + + + + + +
+
+ +
+

Раздел 4. Координаты и векторы

+
ПДСК в пространстве · Векторы · Скалярное произведение · §11–§14 + Финал
+
+
+ +
+
+
+ +
+ +
+ Раздел 4 +

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

+

ПДСК в пространстве · Векторы · Скалярное произведение. Раздел содержит 4 параграфа и финальный этап с боссами.

+
+ +
+ +
+
§ 11
+
+

Координаты в пространстве

+

ПДСК. Точка (x; y; z). Расстояние между точками.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 12
+
+

Вектор. Действия над векторами

+

Сложение, умножение на число, базис i, j, k. Разложение вектора.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 13
+
+

Скалярное произведение

+

a · b = |a||b|cos α = x₁x₂ + y₁y₂ + z₁z₂. Условие перпендикулярности.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+ +
+
§ 14
+
+

Применение координат и векторов

+

Уравнения, углы, расстояния. Векторно-координатный метод.

+
+ + Будет добавлено в следующей волне реализации +
+
+
+
+ + + +
+ +
+ Геометрия — 10 класс · Раздел 4 · LearnSpace +
+ + + + +