/* geom7_svg.js — библиотека SVG-хелперов для всей Геометрии 7 * Используется во всех 5 главах. Намеренно без зависимостей. * * Координатная система SVG: y растёт вниз — мы учитываем это в хелперах углов. * Все функции возвращают строку SVG-фрагмента, готовую к вставке в innerHTML. * * Публичный API: window.GEOM7.{point,segment,ray,line,circle,arc,angle, * rightAngleMark,protractor,polyline,polygon,parallelMark, * midPoint,distance,vec,unit,perp,rotate,svgBox} */ (function(){ 'use strict'; if (window.GEOM7 && window.GEOM7.__installed) return; const G = window.GEOM7 = window.GEOM7 || {}; G.__installed = true; /* === Математика === */ G.distance = (p1, p2) => Math.hypot(p2.x - p1.x, p2.y - p1.y); G.midPoint = (p1, p2) => ({ x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 }); G.vec = (p1, p2) => ({ x:p2.x-p1.x, y:p2.y-p1.y }); G.unit = (v) => { const L = Math.hypot(v.x, v.y) || 1; return { x:v.x/L, y:v.y/L }; }; G.perp = (v) => ({ x:-v.y, y:v.x }); G.rotate = (p, a) => ({ x:p.x*Math.cos(a) - p.y*Math.sin(a), y:p.x*Math.sin(a) + p.y*Math.cos(a) }); /* === SVG-обёртка (даёт область чертежа с фоном/сеткой) === */ G.svgBox = function(w, h, opts){ opts = opts || {}; const grid = opts.grid !== false; const bg = opts.bg || '#fafafa'; const cellSize = opts.cell || 20; let gridSvg = ''; if(grid){ gridSvg = '' +'' +'' +''; } return { open:''+gridSvg, close:'' }; }; /* === Точка === */ G.point = function(x, y, label, opts){ opts = opts || {}; const r = opts.r || 4; const color = opts.color || '#d97706'; const lx = x + (opts.dx !== undefined ? opts.dx : 8); const ly = y + (opts.dy !== undefined ? opts.dy : -6); let s = ''; if(label) s += ''+label+''; return s; }; /* === Отрезок === */ G.segment = function(p1, p2, opts){ opts = opts || {}; const color = opts.color || '#0891b2'; const w = opts.width || 2.5; let s = ''; } } /* Подпись длины */ if(opts.label){ const mid = G.midPoint(p1, p2); const n = G.perp(G.unit(G.vec(p1, p2))); const off = opts.labelOffset || 14; s += ''+opts.label+''; } return s; }; /* === Луч (от p1 в направлении p2, продолжается за p2 до края — приблизительно) === */ G.ray = function(p1, p2, opts){ opts = opts || {}; const v = G.unit(G.vec(p1, p2)); const extend = opts.extend || 400; /* продолжить на N px за p2 */ const end = { x:p2.x + v.x * extend, y:p2.y + v.y * extend }; const color = opts.color || '#0891b2'; const w = opts.width || 2.5; return ''; }; /* === Прямая (бесконечная — продолжается в обе стороны) === */ G.line = function(p1, p2, opts){ opts = opts || {}; const v = G.unit(G.vec(p1, p2)); const extend = opts.extend || 400; const start = { x:p1.x - v.x * extend, y:p1.y - v.y * extend }; const end = { x:p2.x + v.x * extend, y:p2.y + v.y * extend }; return G.segment(start, end, opts); }; /* === Окружность === */ G.circle = function(c, r, opts){ opts = opts || {}; const color = opts.color || '#7c3aed'; let s = ''+opts.radiusLabel+''; } } return s; }; /* === Дуга угла (от V с двумя направлениями) === * vAng1, vAng2 — углы в радианах (стандартная мат-конвенция: 0 = вправо, ↑ положительный CCW) * НО: в SVG y вниз, поэтому работаем напрямую с углами SVG (0 = вправо, ↑ против часовой если y вверх). * Мы передаём углы в SVG-конвенции (для нашей системы — y вниз). */ G.arc = function(c, r, a1, a2, opts){ opts = opts || {}; const x1 = c.x + r * Math.cos(a1), y1 = c.y + r * Math.sin(a1); const x2 = c.x + r * Math.cos(a2), y2 = c.y + r * Math.sin(a2); let delta = a2 - a1; while(delta < 0) delta += 2 * Math.PI; while(delta >= 2 * Math.PI) delta -= 2 * Math.PI; const large = delta > Math.PI ? 1 : 0; const sweep = opts.sweep !== undefined ? opts.sweep : 1; const color = opts.color || '#dc2626'; return ''; }; /* === Угол: вершина V, на которой угол; точки A, B — на сторонах. Возвращает дугу + (опционально) подпись. === */ G.angle = function(V, A, B, opts){ opts = opts || {}; const r = opts.r || 24; const a1 = Math.atan2(A.y - V.y, A.x - V.x); const a2 = Math.atan2(B.y - V.y, B.x - V.x); /* Берём кратчайшую дугу */ let delta = a2 - a1; while(delta > Math.PI) delta -= 2 * Math.PI; while(delta < -Math.PI) delta += 2 * Math.PI; const sweep = delta > 0 ? 1 : 0; const large = Math.abs(delta) > Math.PI ? 1 : 0; const x1 = V.x + r * Math.cos(a1), y1 = V.y + r * Math.sin(a1); const x2 = V.x + r * Math.cos(a2), y2 = V.y + r * Math.sin(a2); const color = opts.color || '#dc2626'; let s = ''; if(opts.label){ /* Центр подписи — середина ДУГИ, в направлении sweep */ const midA = a1 + delta / 2; const lr = r + (opts.labelOffset || 12); const lx = V.x + lr * Math.cos(midA); const ly = V.y + lr * Math.sin(midA); s += ''+opts.label+''; } return s; }; /* === Маркер прямого угла (L-форма ВНУТРЬ угла) === */ G.rightAngleMark = function(V, P1, P2, opts){ opts = opts || {}; const s = opts.size || 12; const u1 = G.unit(G.vec(V, P1)); const u2 = G.unit(G.vec(V, P2)); const p1 = { x:V.x + s * u1.x, y:V.y + s * u1.y }; const c = { x:p1.x + s * u2.x, y:p1.y + s * u2.y }; const p2 = { x:V.x + s * u2.x, y:V.y + s * u2.y }; const color = opts.color || '#dc2626'; return ''; }; /* === Транспортир (полукруг 180°) === */ G.protractor = function(c, R, opts){ opts = opts || {}; const color = opts.color || '#0891b2'; const fill = opts.fill || '#fef3c7'; /* Полукруг по горизонтали (повернуть на 180°) */ let s = ''; /* Деления */ for(let deg = 0; deg <= 180; deg += 10){ const a = Math.PI - deg * Math.PI / 180; /* 180° → слева, 0° → справа */ const r2 = (deg % 30 === 0) ? R - 12 : R - 6; const x1 = c.x + R * Math.cos(a), y1 = c.y - R * Math.sin(a); const x2 = c.x + r2 * Math.cos(a), y2 = c.y - r2 * Math.sin(a); s += ''; if(deg % 30 === 0){ const xl = c.x + (R - 22) * Math.cos(a), yl = c.y - (R - 22) * Math.sin(a); s += ''+deg+''; } } /* Центральная точка */ s += G.point(c.x, c.y, '', { r:3, color:color }); return s; }; /* === Ломаная === */ G.polyline = function(pts, opts){ opts = opts || {}; const color = opts.color || '#0891b2'; const ptsStr = pts.map(p => p.x + ',' + p.y).join(' '); let s = ' { s += G.point(p.x, p.y, opts.labels ? opts.labels[i] : '', { color:color }); }); return s; }; /* === Полигон (замкнутый) === */ G.polygon = function(pts, opts){ opts = opts || {}; const color = opts.color || '#0891b2'; const ptsStr = pts.map(p => p.x + ',' + p.y).join(' '); let s = ''; if(opts.points) pts.forEach((p, i) => { s += G.point(p.x, p.y, opts.labels ? opts.labels[i] : '', { color:color }); }); return s; }; /* === Маркер параллельности (стрелки на отрезках) === */ G.parallelMark = function(p1, p2, opts){ opts = opts || {}; const count = opts.count || 1; const v = G.unit(G.vec(p1, p2)); const n = G.perp(v); const mid = G.midPoint(p1, p2); const color = opts.color || '#7c3aed'; let s = ''; const arrow = 6; for(let i = 0; i < count; i++){ const off = (i - (count - 1)/2) * 5; const cx = mid.x + v.x * off; const cy = mid.y + v.y * off; /* Стрелка → > указывает в направлении v */ s += ''; } return s; }; /* === KaTeX render (если доступен) — с правильными делимитерами === */ G.renderMath = function(root){ if(!root || !window.renderMathInElement) return; try{ window.renderMathInElement(root, { delimiters: [ { left:'$$', right:'$$', display:true }, { left:'$', right:'$', display:false }, { left:'\\[', right:'\\]', display:true }, { left:'\\(', right:'\\)', display:false } ], throwOnError: false }); }catch(e){} }; })();