/* 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:'' };
};
/* === Точка === */
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 = '';
/* Метки длины (ticks) */
if(opts.ticks){
const mid = G.midPoint(p1, p2);
const v = G.unit(G.vec(p1, p2));
const n = G.perp(v);
const tickLen = opts.tickLen || 5;
for(let i = 0; i < opts.ticks; i++){
const off = (i - (opts.ticks - 1)/2) * 4;
const cx = mid.x + v.x * off;
const cy = mid.y + v.y * off;
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 = '';
if(opts.center) s += G.point(c.x, c.y, opts.centerLabel || '', { color:color, dx:6, dy:-6 });
/* Радиус */
if(opts.radius){
const angle = (opts.radiusAngle !== undefined) ? opts.radiusAngle : Math.PI / 4;
const rEnd = { x:c.x + r * Math.cos(angle), y:c.y - r * Math.sin(angle) };
s += G.segment(c, rEnd, { color:color, width:1.5, dash:'4 3' });
if(opts.radiusLabel){
const mid = G.midPoint(c, rEnd);
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 = '';
if(opts.points) pts.forEach((p, i) => { 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){}
};
})();