16ddb27013
Финальная, шестая волна плана v4. Все compute «корень-вперёд» (пифагоровы тройки/четвёрки, целые/конечные ответы) с чертежами. figures.js — 10 НОВЫХ типов: parallel-lines-transversal, isosceles, vertical-angles, points-distance, space-diagonal-box, l-shape, thales-parallel, inscribed-central-angle, chord-circle, tangent-circle; + расширены rectangle (diagonal/unknown/area-метка) и two-similar (side2/mode area/hideK); хелперы tick (равные стороны) и circleSvg. Генераторы: - Углы: ang-parallel-transversal (соответственные), ang-isosceles-base, ang-vertical. - Пифагор: pyth-perimeter, pyth-distance (расстояние между точками), pyth-rect-diagonal, pyth-space-diagonal (3D, пифагорова четвёрка ×s). - Площади: area-rect-inverse (сторона по площади), area-l-shape (составная), area-sector (π≈3,14). - Многоугольники: poly-diagonals (n(n−3)/2), poly-find-n (по углу), poly-exterior-sum (360/n). - Подобие: sim-scale-factor (k по сторонам), sim-area-ratio (k²), sim-thales, sim-map-scale. - Окружность: circ-inscribed-angle (вписанный=½центрального), circ-chord-pyth (хорда через радиус), circ-tangent-len (касательная). Итого 175 генераторов (V4.1 закрыта: 91 новый генератор за фазу). Смоук v41 71386 проверок (вкл. рендер всех фигур: нет NaN/<script>, «?» на искомой); figures-смоук 13846 проверок / 2280 рендеров на 38 геом-генераторах. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
713 lines
41 KiB
JavaScript
713 lines
41 KiB
JavaScript
'use strict';
|
||
/* ════════════════════════════════════════════════════════════════════════
|
||
TrainerFigures — чертежи геометрических задач тренажёра. ДАННЫЕ, не код.
|
||
|
||
Идея (та же модель безопасности, что у SimForge «объекты — это данные» и
|
||
math6-svg): генератор задачи НЕ содержит SVG/кода — он лишь ссылается на
|
||
ИМЕНОВАННЫЙ тип фигуры и привязки её размеров к параметрам задачи:
|
||
|
||
figure: { type:'right-triangle', a:'a', b:'b', c:'c', unknown:'c' }
|
||
|
||
Здесь 'a'/'b'/'c' — имена параметров уже материализованной задачи
|
||
(problem.params, числа). Рендерер сам строит SVG ИЗ ЧИСЕЛ → ⛔ без eval/
|
||
new Function, без пользовательских строк в разметке (текст-подписи
|
||
экранируются). Чертёж — ИЛЛЮСТРАЦИЯ рядом с условием: показывает данные
|
||
величины и «?» на искомой; математику по-прежнему считает движок (SimExpr).
|
||
|
||
Цвета подобраны под индиго-сцену героя (белые штрихи на тёмном фоне; при
|
||
верном ответе сцена зеленеет, при неверном — краснеет, белое читается на всех).
|
||
|
||
API (window.TrainerFigures):
|
||
render(figureSpec, params) -> svgString | null (null, если тип неизвестен)
|
||
has(type) -> bool
|
||
TYPES -> { type: fn }
|
||
|
||
Контракт типа: fn(spec, params, U) -> body-строка (внутренности <svg>).
|
||
U — утилиты (num/lbl/fit/ln/pgon/txt/arc/rightAngle/…).
|
||
════════════════════════════════════════════════════════════════════════ */
|
||
(function (global) {
|
||
|
||
// ── размер холста и поля под подписи ──
|
||
var VB_W = 268, VB_H = 184, MARGIN = 38;
|
||
|
||
// ── палитра (только в SVG-стоки: stroke/fill — мусор не исполняется) ──
|
||
var STROKE = 'rgba(255,255,255,.94)'; // основные линии
|
||
var FILLSH = 'rgba(255,255,255,.10)'; // лёгкая заливка фигуры
|
||
var DASH = 'rgba(255,255,255,.78)'; // вспомогательные (высоты, диагонали)
|
||
var ARC = '#fde68a'; // дуги углов (тёплый, виден на всех фонах)
|
||
var UNK = '#fde68a'; // подпись искомой величины «?»
|
||
var VERTEX = 'rgba(255,255,255,.96)'; // точки-вершины
|
||
|
||
function esc(s) {
|
||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
// Аккуратное число для подписи (целое без хвоста, иначе до 2 знаков).
|
||
function fmt(v) {
|
||
if (typeof v !== 'number' || !isFinite(v)) return '';
|
||
if (Math.abs(v - Math.round(v)) < 1e-9) return String(Math.round(v));
|
||
return String(Math.round(v * 100) / 100);
|
||
}
|
||
|
||
// Разрешение привязки: число → как есть; строка-имя параметра → params[name];
|
||
// числовая строка → parseFloat; иначе undefined.
|
||
function num(params, ref) {
|
||
if (typeof ref === 'number') return isFinite(ref) ? ref : undefined;
|
||
if (typeof ref === 'string') {
|
||
if (params && Object.prototype.hasOwnProperty.call(params, ref)) {
|
||
var v = params[ref];
|
||
return (typeof v === 'number' && isFinite(v)) ? v : undefined;
|
||
}
|
||
var f = parseFloat(ref);
|
||
if (isFinite(f)) return f;
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
// Подпись величины: «?» (искомая) или число. unknownKey сравнивается с key.
|
||
function lbl(val, key, unknownKey) {
|
||
if (unknownKey != null && key === unknownKey) return '?';
|
||
return fmt(val);
|
||
}
|
||
|
||
// ── геометрия в МАТЕМАТИЧЕСКИХ координатах (y вверх), затем fit→экран ──
|
||
function P(x, y) { return { x: x, y: y }; }
|
||
function add(a, b) { return P(a.x + b.x, a.y + b.y); }
|
||
function sub(a, b) { return P(a.x - b.x, a.y - b.y); }
|
||
function mul(a, k) { return P(a.x * k, a.y * k); }
|
||
function len(a) { return Math.hypot(a.x, a.y); }
|
||
function norm(a) { var l = len(a) || 1; return P(a.x / l, a.y / l); }
|
||
function mid(a, b) { return P((a.x + b.x) / 2, (a.y + b.y) / 2); }
|
||
function deg2rad(d) { return d * Math.PI / 180; }
|
||
|
||
// Подгонка набора мат-точек в холст: возвращает f.px(p) → экранная точка (y вниз).
|
||
function fit(pts) {
|
||
var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
|
||
for (var i = 0; i < pts.length; i++) {
|
||
var p = pts[i];
|
||
if (p.x < minx) minx = p.x; if (p.x > maxx) maxx = p.x;
|
||
if (p.y < miny) miny = p.y; if (p.y > maxy) maxy = p.y;
|
||
}
|
||
var w = Math.max(1e-6, maxx - minx), h = Math.max(1e-6, maxy - miny);
|
||
var availW = VB_W - 2 * MARGIN, availH = VB_H - 2 * MARGIN;
|
||
var s = Math.min(availW / w, availH / h);
|
||
var drawW = w * s, drawH = h * s;
|
||
var ox = (VB_W - drawW) / 2, oy = (VB_H - drawH) / 2;
|
||
return {
|
||
s: s,
|
||
px: function (p) { return P(ox + (p.x - minx) * s, oy + (maxy - p.y) * s); }
|
||
};
|
||
}
|
||
|
||
// ── примитивы рисования (принимают ЭКРАННЫЕ точки) ──
|
||
function ln(a, b, opt) {
|
||
opt = opt || {};
|
||
return '<line x1="' + r1(a.x) + '" y1="' + r1(a.y) + '" x2="' + r1(b.x) + '" y2="' + r1(b.y) +
|
||
'" stroke="' + (opt.stroke || STROKE) + '" stroke-width="' + (opt.w || 2.4) +
|
||
'" stroke-linecap="round"' + (opt.dash ? ' stroke-dasharray="5 5"' : '') + '/>';
|
||
}
|
||
function pgon(ptsScreen, opt) {
|
||
opt = opt || {};
|
||
var d = ptsScreen.map(function (p) { return r1(p.x) + ',' + r1(p.y); }).join(' ');
|
||
return '<polygon points="' + d + '" fill="' + (opt.fill || FILLSH) + '" stroke="' +
|
||
(opt.stroke || STROKE) + '" stroke-width="' + (opt.w || 2.4) + '" stroke-linejoin="round"/>';
|
||
}
|
||
function dot(p, rr) {
|
||
return '<circle cx="' + r1(p.x) + '" cy="' + r1(p.y) + '" r="' + (rr || 2.6) + '" fill="' + VERTEX + '"/>';
|
||
}
|
||
// Текст с тёмным гало (paint-order) для читаемости на любом фоне сцены.
|
||
function txt(p, s, opt) {
|
||
opt = opt || {};
|
||
var fill = opt.fill || '#fff';
|
||
var size = opt.size || 13.5;
|
||
var anchor = opt.anchor || 'middle';
|
||
var weight = opt.weight || 700;
|
||
return '<text x="' + r1(p.x) + '" y="' + r1(p.y) + '" text-anchor="' + anchor +
|
||
'" dominant-baseline="middle" font-family="Manrope, system-ui, sans-serif" font-size="' + size +
|
||
'" font-weight="' + weight + '" fill="' + fill +
|
||
'" style="paint-order:stroke;stroke:rgba(15,23,42,.45);stroke-width:3.2px;stroke-linejoin:round">' +
|
||
esc(s) + '</text>';
|
||
}
|
||
// Подпись величины у середины ребра a-b, отодвинутая НАРУЖУ от точки away.
|
||
function edgeLabel(a, b, away, text, opt) {
|
||
opt = opt || {};
|
||
var m = mid(a, b);
|
||
var n = norm(sub(m, away)); // от центра наружу
|
||
var off = opt.off || 16;
|
||
var pos = P(m.x + n.x * off, m.y + n.y * off);
|
||
return txt(pos, text, opt);
|
||
}
|
||
function r1(n) { return Math.round(n * 10) / 10; }
|
||
|
||
// Дуга угла в вершине V между лучами на A и B; рисует короткую (внутреннюю) дугу
|
||
// радиуса rad экранных px. Возвращает { path, labelPos } (labelPos — для подписи).
|
||
function angleArc(Vs, As, Bs, rad) {
|
||
var a0 = Math.atan2(As.y - Vs.y, As.x - Vs.x);
|
||
var a1 = Math.atan2(Bs.y - Vs.y, Bs.x - Vs.x);
|
||
var d = a1 - a0;
|
||
while (d > Math.PI) d -= 2 * Math.PI;
|
||
while (d < -Math.PI) d += 2 * Math.PI;
|
||
var n = 14, pts = [];
|
||
for (var i = 0; i <= n; i++) {
|
||
var a = a0 + d * (i / n);
|
||
pts.push(P(Vs.x + Math.cos(a) * rad, Vs.y + Math.sin(a) * rad));
|
||
}
|
||
var path = '<path d="M ' + pts.map(function (p) { return r1(p.x) + ' ' + r1(p.y); }).join(' L ') +
|
||
'" fill="none" stroke="' + ARC + '" stroke-width="2.4" stroke-linecap="round"/>';
|
||
var amid = a0 + d / 2;
|
||
var labelPos = P(Vs.x + Math.cos(amid) * (rad + 14), Vs.y + Math.sin(amid) * (rad + 14));
|
||
return { path: path, labelPos: labelPos };
|
||
}
|
||
|
||
// Маркер прямого угла в вершине Vs, стороны к As и Bs (экранные), размер m px.
|
||
function rightAngle(Vs, As, Bs, m) {
|
||
m = m || 13;
|
||
var u = norm(sub(As, Vs)), w = norm(sub(Bs, Vs));
|
||
var p1 = add(Vs, mul(u, m)), p3 = add(Vs, mul(w, m)), p2 = add(p1, mul(w, m));
|
||
return '<path d="M ' + r1(p1.x) + ' ' + r1(p1.y) + ' L ' + r1(p2.x) + ' ' + r1(p2.y) +
|
||
' L ' + r1(p3.x) + ' ' + r1(p3.y) + '" fill="none" stroke="' + STROKE +
|
||
'" stroke-width="2"/>';
|
||
}
|
||
|
||
var U = {
|
||
num: num, lbl: lbl, fmt: fmt, P: P, add: add, sub: sub, mul: mul, len: len,
|
||
norm: norm, mid: mid, deg2rad: deg2rad, fit: fit, ln: ln, pgon: pgon, dot: dot,
|
||
txt: txt, edgeLabel: edgeLabel, angleArc: angleArc, rightAngle: rightAngle,
|
||
STROKE: STROKE, FILLSH: FILLSH, DASH: DASH, ARC: ARC, UNK: UNK
|
||
};
|
||
|
||
/* ════════════════ ТИПЫ ФИГУР ════════════════ */
|
||
// Штрих равенства на середине отрезка a-b (метка равных сторон).
|
||
function tick(a, b) {
|
||
var m = mid(a, b), u = norm(sub(b, a)), nrm = P(-u.y, u.x);
|
||
return '<line x1="' + r1(m.x + nrm.x * 5) + '" y1="' + r1(m.y + nrm.y * 5) +
|
||
'" x2="' + r1(m.x - nrm.x * 5) + '" y2="' + r1(m.y - nrm.y * 5) +
|
||
'" stroke="' + STROKE + '" stroke-width="2"/>';
|
||
}
|
||
// Окружность как строка SVG (центр-экран Cs, радиус rad px).
|
||
function circleSvg(Cs, rad, opt) {
|
||
opt = opt || {};
|
||
return '<circle cx="' + r1(Cs.x) + '" cy="' + r1(Cs.y) + '" r="' + r1(rad) +
|
||
'" fill="' + (opt.fill || 'none') + '" stroke="' + (opt.stroke || STROKE) +
|
||
'" stroke-width="' + (opt.w || 2.4) + '"/>';
|
||
}
|
||
|
||
var TYPES = {
|
||
|
||
/* Прямоугольный треугольник (Пифагор).
|
||
a — вертикальный катет, b — горизонтальный катет, c — гипотенуза.
|
||
unknown ∈ {a,b,c} — какая величина искомая (рисуется «?»). */
|
||
'right-triangle': function (spec, p) {
|
||
var a = num(p, spec.a), b = num(p, spec.b), c = num(p, spec.c);
|
||
if (!(a > 0) || !(b > 0)) return null;
|
||
// мат-координаты: прямой угол в A (0,0), горизонт. катет B(b,0), верт. катет C(0,a)
|
||
var A = P(0, 0), B = P(b, 0), C = P(0, a);
|
||
var f = fit([A, B, C]);
|
||
var As = f.px(A), Bs = f.px(B), Cs = f.px(C);
|
||
var body = pgon([As, Bs, Cs]);
|
||
body += rightAngle(As, Bs, Cs, 13);
|
||
body += dot(As) + dot(Bs) + dot(Cs);
|
||
// подписи: горизонт. катет (A-B) ← b; верт. катет (A-C) ← a; гипотенуза (B-C) ← c
|
||
body += edgeLabel(As, Bs, Cs, lbl(b, 'b', spec.unknown), unkOpt('b', spec.unknown));
|
||
body += edgeLabel(As, Cs, Bs, lbl(a, 'a', spec.unknown), unkOpt('a', spec.unknown));
|
||
body += edgeLabel(Bs, Cs, As, (c != null ? lbl(c, 'c', spec.unknown) : '?'), unkOpt('c', spec.unknown));
|
||
return body;
|
||
},
|
||
|
||
/* Углы треугольника. angA — левый, angB — правый базовые углы.
|
||
Без ext: вершина (апекс) — искомый угол «?».
|
||
ext:true — внешний угол при правой вершине = angA+angB (рисуется «?»),
|
||
апекс получает angB, правый внутренний = 180−angA−angB. */
|
||
'triangle-angles': function (spec, p) {
|
||
var ax = num(p, spec.angA), bx = num(p, spec.angB);
|
||
if (!(ax > 0) || !(bx > 0)) return null;
|
||
var ext = !!spec.ext;
|
||
var alpha = ax; // левый внутренний угол
|
||
var gamma = ext ? (180 - ax - bx) : bx; // правый внутренний угол
|
||
if (!(gamma > 0) || alpha + gamma >= 179.5) return null;
|
||
// апекс T из L,R по внутренним углам alpha (слева), gamma (справа)
|
||
var L = P(0, 0), R = P(1, 0);
|
||
var s = Math.sin(deg2rad(alpha)) / Math.sin(deg2rad(alpha + gamma)); // s = |R→…| масштаб
|
||
var T = add(R, mul(P(-Math.cos(deg2rad(gamma)), Math.sin(deg2rad(gamma))), s));
|
||
var basePts = [L, R, T];
|
||
var E = null;
|
||
if (ext) { E = P(R.x + (R.x - L.x) * 0.55, 0); basePts.push(E); } // продолжение базы за R
|
||
var f = fit(basePts);
|
||
var Ls = f.px(L), Rs = f.px(R), Ts = f.px(T);
|
||
var body = pgon([Ls, Rs, Ts]);
|
||
if (ext) {
|
||
var Es = f.px(E);
|
||
body += ln(Rs, Es, { dash: false, w: 2.2 }); // продолжение стороны (базы)
|
||
}
|
||
body += dot(Ls) + dot(Rs) + dot(Ts);
|
||
// дуги+подписи углов
|
||
var arcL = angleArc(Ls, Rs, Ts, 22);
|
||
body += arcL.path + txt(arcL.labelPos, fmt(ax) + '°', { fill: '#fff', size: 12.5 });
|
||
if (ext) {
|
||
var arcT = angleArc(Ts, Ls, Rs, 22);
|
||
body += arcT.path + txt(arcT.labelPos, fmt(bx) + '°', { fill: '#fff', size: 12.5 });
|
||
// внешний угол при R: между продолжением базы (Es) и стороной R→T
|
||
var arcE = angleArc(Rs, f.px(E), Ts, 22);
|
||
body += arcE.path + txt(arcE.labelPos, '?', { fill: UNK, size: 16, weight: 800 });
|
||
} else {
|
||
var arcR = angleArc(Rs, Ts, Ls, 22);
|
||
body += arcR.path + txt(arcR.labelPos, fmt(bx) + '°', { fill: '#fff', size: 12.5 });
|
||
var arcTa = angleArc(Ts, Ls, Rs, 24);
|
||
body += arcTa.path + txt(arcTa.labelPos, '?', { fill: UNK, size: 16, weight: 800 });
|
||
}
|
||
return body;
|
||
},
|
||
|
||
/* Смежные углы: прямая через O, луч вверх под углом ang к правой части.
|
||
Известный угол ang (справа от луча) и искомый «?» (слева, = 180−ang). */
|
||
'adjacent-angles': function (spec, p) {
|
||
var ang = num(p, spec.ang);
|
||
if (!(ang > 0) || ang >= 180) return null;
|
||
var O = P(0, 0), Lp = P(-1, 0), Rp = P(1, 0);
|
||
var ray = P(Math.cos(deg2rad(ang)), Math.sin(deg2rad(ang))); // луч вверх
|
||
var f = fit([Lp, Rp, O, ray]);
|
||
var Os = f.px(O), Ls = f.px(Lp), Rs = f.px(Rp), Rays = f.px(ray);
|
||
var body = ln(Ls, Rs, { w: 2.6 }); // прямая
|
||
body += ln(Os, Rays, { w: 2.6 }); // луч
|
||
body += dot(Os);
|
||
var arcR = angleArc(Os, Rs, Rays, 26); // известный угол (справа)
|
||
body += arcR.path + txt(arcR.labelPos, fmt(ang) + '°', { fill: '#fff', size: 12.5 });
|
||
var arcL = angleArc(Os, Rays, Ls, 26); // искомый (слева)
|
||
body += arcL.path + txt(arcL.labelPos, '?', { fill: UNK, size: 16, weight: 800 });
|
||
return body;
|
||
},
|
||
|
||
/* Прямоугольник: w (горизонталь), h (вертикаль). Обе стороны подписаны. */
|
||
'rectangle': function (spec, p) {
|
||
var w = num(p, spec.w), h = num(p, spec.h);
|
||
if (!(w > 0) || !(h > 0)) return null;
|
||
var A = P(0, 0), B = P(w, 0), C = P(w, h), D = P(0, h);
|
||
var f = fit([A, B, C, D]);
|
||
var As = f.px(A), Bs = f.px(B), Cs = f.px(C), Ds = f.px(D), cen = f.px(P(w / 2, h / 2));
|
||
var body = pgon([As, Bs, Cs, Ds]);
|
||
body += edgeLabel(As, Bs, cen, lbl(w, 'w', spec.unknown), unkOpt('w', spec.unknown));
|
||
body += edgeLabel(Bs, Cs, cen, lbl(h, 'h', spec.unknown), unkOpt('h', spec.unknown));
|
||
if (spec.diagonal) { // пунктирная диагональ с «?»
|
||
body += ln(As, Cs, { dash: true, stroke: DASH, w: 1.8 });
|
||
body += txt(P((As.x + Cs.x) / 2 + 8, (As.y + Cs.y) / 2 - 8), '?', { fill: UNK, size: 15, weight: 800, anchor: 'start' });
|
||
}
|
||
var area = num(p, spec.area); // подпись площади в центре (для обратной задачи)
|
||
if (area != null) body += txt(cen, 'S = ' + fmt(area), { fill: '#fff', size: 12.5 });
|
||
return body;
|
||
},
|
||
|
||
/* Квадрат со стороной a (подписаны две смежные стороны). */
|
||
'square': function (spec, p) {
|
||
var a = num(p, spec.a);
|
||
if (!(a > 0)) return null;
|
||
var A = P(0, 0), B = P(a, 0), C = P(a, a), D = P(0, a);
|
||
var f = fit([A, B, C, D]);
|
||
var As = f.px(A), Bs = f.px(B), Cs = f.px(C), Ds = f.px(D), cen = f.px(P(a / 2, a / 2));
|
||
var body = pgon([As, Bs, Cs, Ds]);
|
||
body += edgeLabel(As, Bs, cen, fmt(a), {});
|
||
body += edgeLabel(As, Ds, cen, fmt(a), {});
|
||
return body;
|
||
},
|
||
|
||
/* Треугольник по основанию и высоте: base (горизонт.), height (пунктирная высота). */
|
||
'triangle-base-height': function (spec, p) {
|
||
var base = num(p, spec.base), h = num(p, spec.height);
|
||
if (!(base > 0) || !(h > 0)) return null;
|
||
// апекс смещён (не равнобедренный, чтобы высота была наглядна), но проекция внутри основания
|
||
var apexX = base * 0.62;
|
||
var A = P(0, 0), B = P(base, 0), T = P(apexX, h), F = P(apexX, 0); // F — основание высоты
|
||
var f = fit([A, B, T]);
|
||
var As = f.px(A), Bs = f.px(B), Ts = f.px(T), Fs = f.px(F), cen = f.px(P(base / 2, h / 3));
|
||
var body = pgon([As, Bs, Ts]);
|
||
body += ln(Ts, Fs, { dash: true, stroke: DASH, w: 2 }); // высота
|
||
body += rightAngle(Fs, Bs, Ts, 11); // прямой угол у основания высоты
|
||
body += dot(As) + dot(Bs) + dot(Ts);
|
||
body += edgeLabel(As, Bs, Ts, fmt(base), {}); // основание
|
||
body += txt(P((Ts.x + Fs.x) / 2 + 14, (Ts.y + Fs.y) / 2), fmt(h), { fill: '#fff', size: 12.5, anchor: 'start' });
|
||
return body;
|
||
},
|
||
|
||
/* Трапеция: основания spec.bottom и spec.top (порядок неважен — оба основания),
|
||
height (пунктирная высота). Длинное основание рисуем СНИЗУ → ножка высоты
|
||
всегда внутри, подписи = реальные длины. */
|
||
'trapezoid': function (spec, p) {
|
||
var x1 = num(p, spec.bottom), x2 = num(p, spec.top), h = num(p, spec.height);
|
||
if (!(x1 > 0) || !(x2 > 0) || !(h > 0)) return null;
|
||
var bot = Math.max(x1, x2), top = Math.min(x1, x2);
|
||
var offset = (bot - top) / 2; // верхнее основание по центру
|
||
var A = P(0, 0), B = P(bot, 0), C = P(offset + top, h), D = P(offset, h);
|
||
var f = fit([A, B, C, D]);
|
||
var As = f.px(A), Bs = f.px(B), Cs = f.px(C), Ds = f.px(D), cen = f.px(P(bot / 2, h / 2));
|
||
var body = pgon([As, Bs, Cs, Ds]);
|
||
var Fs = f.px(P(offset, 0)); // ножка высоты на нижнем основании
|
||
body += ln(Ds, Fs, { dash: true, stroke: DASH, w: 2 });
|
||
body += rightAngle(Fs, Bs, Ds, 10);
|
||
body += edgeLabel(As, Bs, cen, fmt(bot), {}); // нижнее основание
|
||
body += edgeLabel(Ds, Cs, cen, fmt(top), {}); // верхнее основание
|
||
body += txt(P((Ds.x + Fs.x) / 2 - 13, (Ds.y + Fs.y) / 2), fmt(h), { fill: '#fff', size: 12.5, anchor: 'end' });
|
||
return body;
|
||
},
|
||
|
||
/* Параллелограмм: base (нижняя сторона), height (пунктирная высота к ней). */
|
||
'parallelogram': function (spec, p) {
|
||
var base = num(p, spec.base), h = num(p, spec.height);
|
||
if (!(base > 0) || !(h > 0)) return null;
|
||
var skew = base * 0.32;
|
||
var A = P(0, 0), B = P(base, 0), C = P(base + skew, h), D = P(skew, h);
|
||
var f = fit([A, B, C, D]);
|
||
var As = f.px(A), Bs = f.px(B), Cs = f.px(C), Ds = f.px(D), cen = f.px(P(base / 2 + skew / 2, h / 2));
|
||
var body = pgon([As, Bs, Cs, Ds]);
|
||
// высота от D перпендикулярно основанию (на проекцию D=skew)
|
||
var Fs = f.px(P(skew, 0));
|
||
body += ln(Ds, Fs, { dash: true, stroke: DASH, w: 2 });
|
||
body += rightAngle(Fs, Bs, Ds, 10);
|
||
body += edgeLabel(As, Bs, cen, fmt(base), {});
|
||
body += txt(P((Ds.x + Fs.x) / 2 - 13, (Ds.y + Fs.y) / 2), fmt(h), { fill: '#fff', size: 12.5, anchor: 'end' });
|
||
return body;
|
||
},
|
||
|
||
/* Ромб по диагоналям d1 (горизонт.), d2 (верт.) — диагонали пунктиром. */
|
||
'rhombus': function (spec, p) {
|
||
var d1 = num(p, spec.d1), d2 = num(p, spec.d2);
|
||
if (!(d1 > 0) || !(d2 > 0)) return null;
|
||
var R = P(d1 / 2, 0), L = P(-d1 / 2, 0), Tp = P(0, d2 / 2), Bt = P(0, -d2 / 2);
|
||
var f = fit([R, L, Tp, Bt]);
|
||
var Rs = f.px(R), Ls = f.px(L), Ts = f.px(Tp), Bs = f.px(Bt), Os = f.px(P(0, 0));
|
||
var body = pgon([Rs, Ts, Ls, Bs]);
|
||
body += ln(Ls, Rs, { dash: true, stroke: DASH, w: 1.8 }); // горизонтальная диагональ
|
||
body += ln(Bs, Ts, { dash: true, stroke: DASH, w: 1.8 }); // вертикальная диагональ
|
||
body += dot(Os, 2.2);
|
||
body += txt(P((Os.x + Rs.x) / 2, Os.y - 11), fmt(d1), { fill: '#fff', size: 12.5 });
|
||
body += txt(P(Os.x + 13, (Os.y + Ts.y) / 2), fmt(d2), { fill: '#fff', size: 12.5, anchor: 'start' });
|
||
return body;
|
||
},
|
||
|
||
/* Правильный n-угольник; markAngle:true — отметить один внутренний угол «?». */
|
||
'regular-polygon': function (spec, p) {
|
||
var n = Math.round(num(p, spec.n));
|
||
if (!(n >= 3) || n > 24) return null;
|
||
var pts = [];
|
||
var start = Math.PI / 2 + (n % 2 === 0 ? Math.PI / n : 0); // плоской стороной вниз
|
||
for (var i = 0; i < n; i++) {
|
||
var a = start + i * 2 * Math.PI / n;
|
||
pts.push(P(Math.cos(a), Math.sin(a)));
|
||
}
|
||
var f = fit(pts);
|
||
var sp = pts.map(function (pt) { return f.px(pt); });
|
||
var body = pgon(sp);
|
||
for (var j = 0; j < sp.length; j++) body += dot(sp[j], 2.3);
|
||
body += txt(f.px(P(0, 0)), 'n = ' + n, { fill: 'rgba(255,255,255,.92)', size: 12, weight: 700 }); // число сторон — читается с чертежа
|
||
if (spec.markAngle) {
|
||
var v = sp[0], prev = sp[(n - 1) % n], next = sp[1];
|
||
var arc = angleArc(v, prev, next, 16);
|
||
body += arc.path + txt(arc.labelPos, '?', { fill: UNK, size: 15, weight: 800 });
|
||
}
|
||
return body;
|
||
},
|
||
|
||
/* Две подобные фигуры (треугольники): слева — оригинал, справа — увеличенный в k раз.
|
||
mode:'side' — подписаны сходственные стороны (a и «?»);
|
||
mode:'perimeter' — подписаны периметры (P и «?»). k подписан между ними. */
|
||
'two-similar': function (spec, p) {
|
||
var k = num(p, spec.k);
|
||
if (!(k > 0)) return null;
|
||
var mode = spec.mode || 'side';
|
||
var known = (mode === 'perimeter') ? num(p, spec.perim) : (mode === 'area' ? num(p, spec.area) : num(p, spec.side));
|
||
var side2 = num(p, spec.side2); // если задана — правая сторона подписана числом, а не «?»
|
||
var vk = Math.min(1.85, Math.max(1.15, k)); // визуальный масштаб (не буквальный k)
|
||
// базовый треугольник (форма), две копии бок о бок
|
||
var shape = [P(0, 0), P(1.0, 0), P(0.35, 0.85)];
|
||
function place(scale, dx) {
|
||
return shape.map(function (pt) { return P(pt.x * scale + dx, pt.y * scale); });
|
||
}
|
||
var t1 = place(1, 0);
|
||
var gap = 0.6;
|
||
var t2 = place(vk, 1.0 + gap);
|
||
var f = fit(t1.concat(t2));
|
||
var s1 = t1.map(function (pt) { return f.px(pt); });
|
||
var s2 = t2.map(function (pt) { return f.px(pt); });
|
||
var body = pgon(s1) + pgon(s2);
|
||
var c1 = f.px(P((t1[0].x + t1[1].x + t1[2].x) / 3, (t1[0].y + t1[1].y + t1[2].y) / 3));
|
||
var c2 = f.px(P((t2[0].x + t2[1].x + t2[2].x) / 3, (t2[0].y + t2[1].y + t2[2].y) / 3));
|
||
if (mode === 'perimeter' || mode === 'area') {
|
||
var pref = (mode === 'area') ? 'S=' : 'P=';
|
||
body += txt(P(c1.x, c1.y), (known != null ? pref + fmt(known) : pref.charAt(0)), { fill: '#fff', size: 12.5 });
|
||
body += txt(P(c2.x, c2.y), '?', { fill: UNK, size: 16, weight: 800 });
|
||
} else {
|
||
// подпись нижней (сходственной) стороны каждого треугольника
|
||
body += edgeLabel(s1[0], s1[1], c1, (known != null ? fmt(known) : ''), {});
|
||
body += edgeLabel(s2[0], s2[1], c2, (side2 != null ? fmt(side2) : '?'),
|
||
side2 != null ? { fill: '#fff', size: 13 } : { fill: UNK, size: 15, weight: 800 });
|
||
}
|
||
// коэффициент подобия между фигурами (скрывается, если k — искомая величина)
|
||
if (!spec.hideK) {
|
||
var between = P((c1.x + c2.x) / 2, Math.min(c1.y, c2.y) - 6);
|
||
body += txt(between, 'k = ' + fmt(k), { fill: ARC, size: 12.5, weight: 800 });
|
||
}
|
||
return body;
|
||
},
|
||
|
||
/* Окружность/круг. r ИЛИ d задают размер. show:
|
||
'radius' — отрезок-радиус, подпись r; 'diameter' — отрезок-диаметр, подпись d;
|
||
'area' — лёгкая заливка круга + радиус. (Искомая величина — длина/площадь — на
|
||
чертеже не отмечается, как у площадей: фигура показывает данные.) */
|
||
'circle': function (spec, p) {
|
||
var r = num(p, spec.r), d = num(p, spec.d);
|
||
var radius = (r != null) ? r : (d != null ? d / 2 : null);
|
||
if (!(radius > 0)) return null;
|
||
var show = spec.show || 'radius';
|
||
var f = fit([P(-1, 0), P(1, 0), P(0, -1), P(0, 1)]);
|
||
var Cs = f.px(P(0, 0)), rad = f.s;
|
||
var body = '<circle cx="' + r1(Cs.x) + '" cy="' + r1(Cs.y) + '" r="' + r1(rad) +
|
||
'" fill="' + (show === 'area' ? FILLSH : 'none') + '" stroke="' + STROKE + '" stroke-width="2.6"/>';
|
||
body += dot(Cs, 2.6);
|
||
if (show === 'diameter') {
|
||
var Ld = P(Cs.x - rad, Cs.y), Rd = P(Cs.x + rad, Cs.y);
|
||
body += ln(Ld, Rd, { dash: true, stroke: DASH, w: 1.8 });
|
||
body += dot(Ld, 2.2) + dot(Rd, 2.2);
|
||
body += txt(P(Cs.x, Cs.y - 11), 'd = ' + fmt(d != null ? d : radius * 2), { fill: '#fff', size: 12.5 });
|
||
} else {
|
||
var ang = -Math.PI / 4; // радиус в верхне-правый сектор
|
||
var E = P(Cs.x + Math.cos(ang) * rad, Cs.y + Math.sin(ang) * rad);
|
||
body += ln(Cs, E, { w: 2 });
|
||
body += txt(P((Cs.x + E.x) / 2 + 5, (Cs.y + E.y) / 2 - 7), 'r = ' + fmt(radius), { fill: '#fff', size: 12.5, anchor: 'start' });
|
||
}
|
||
return body;
|
||
},
|
||
|
||
/* Сектор/дуга: окружность (бледная) + два радиуса под центральным углом angle°,
|
||
дуга выделена; подписаны угол и радиус r. (Длина дуги — искомая, на чертеже нет.) */
|
||
'circle-arc': function (spec, p) {
|
||
var r = num(p, spec.r), nAng = num(p, spec.angle);
|
||
if (!(r > 0) || !(nAng > 0) || nAng >= 360) return null;
|
||
var f = fit([P(-1, 0), P(1, 0), P(0, -1), P(0, 1)]);
|
||
var Cs = f.px(P(0, 0)), rad = f.s;
|
||
function onC(deg) { var a = deg2rad(deg); return P(Cs.x + Math.cos(a) * rad, Cs.y - Math.sin(a) * rad); }
|
||
var P0 = onC(0), P1 = onC(nAng);
|
||
var body = '<circle cx="' + r1(Cs.x) + '" cy="' + r1(Cs.y) + '" r="' + r1(rad) +
|
||
'" fill="none" stroke="rgba(255,255,255,.4)" stroke-width="1.8"/>';
|
||
var seg = 28, ap = [];
|
||
for (var i = 0; i <= seg; i++) ap.push(onC(nAng * i / seg));
|
||
body += '<path d="M ' + ap.map(function (q) { return r1(q.x) + ' ' + r1(q.y); }).join(' L ') +
|
||
'" fill="none" stroke="' + ARC + '" stroke-width="3.4" stroke-linecap="round"/>';
|
||
body += ln(Cs, P0, { w: 2 }) + ln(Cs, P1, { w: 2 });
|
||
body += dot(Cs, 2.6);
|
||
var amid = deg2rad(nAng / 2);
|
||
body += txt(P(Cs.x + Math.cos(amid) * rad * 0.36, Cs.y - Math.sin(amid) * rad * 0.36), fmt(nAng) + '°', { fill: '#fff', size: 12 });
|
||
body += txt(P((Cs.x + P0.x) / 2, (Cs.y + P0.y) / 2 - 9), 'r = ' + fmt(r), { fill: '#fff', size: 12 });
|
||
return body;
|
||
},
|
||
|
||
/* ── V4.1 группа 6: новые геометрические фигуры ── */
|
||
|
||
/* Параллельные прямые и секущая; данный угол a° сверху, искомый «?» снизу. */
|
||
'parallel-lines-transversal': function (spec, p) {
|
||
var a = num(p, spec.given);
|
||
if (!(a > 0)) return null;
|
||
var rel = spec.rel || 'corresponding';
|
||
var L1a = P(-1.3, 1), L1b = P(1.3, 1), L2a = P(-1.3, -1), L2b = P(1.3, -1);
|
||
var Sx = P(-0.8, -1.7), Ex = P(0.8, 1.7);
|
||
var T1 = P(0.5, 1), T2 = P(-0.5, -1);
|
||
var f = fit([L1a, L1b, L2a, L2b, Sx, Ex]);
|
||
var body = ln(f.px(L1a), f.px(L1b), { w: 2.4 }) + ln(f.px(L2a), f.px(L2b), { w: 2.4 }) + ln(f.px(Sx), f.px(Ex), { w: 2.2 });
|
||
var t1 = f.px(T1), t2 = f.px(T2);
|
||
body += dot(t1) + dot(t2);
|
||
var a1 = angleArc(t1, f.px(P(1.3, 1)), f.px(Ex), 20);
|
||
body += a1.path + txt(a1.labelPos, fmt(a) + '°', { fill: '#fff', size: 12 });
|
||
var arm = (rel === 'alternate') ? f.px(P(-1.3, -1)) : f.px(P(1.3, -1));
|
||
var arm2 = (rel === 'cointerior') ? f.px(Sx) : f.px(Ex);
|
||
var a2 = angleArc(t2, arm, arm2, 20);
|
||
body += a2.path + txt(a2.labelPos, '?', { fill: UNK, size: 15, weight: 800 });
|
||
return body;
|
||
},
|
||
|
||
/* Равнобедренный треугольник: угол при вершине apex°, базовый угол «?», штрихи на равных сторонах. */
|
||
'isosceles': function (spec, p) {
|
||
var apex = num(p, spec.apex);
|
||
if (!(apex > 0)) return null;
|
||
var T = P(0, 1), Lp = P(-0.75, -0.7), Rp = P(0.75, -0.7);
|
||
var f = fit([T, Lp, Rp]);
|
||
var Ts = f.px(T), Ls = f.px(Lp), Rs = f.px(Rp);
|
||
var body = pgon([Ts, Ls, Rs]) + dot(Ts) + dot(Ls) + dot(Rs);
|
||
var aA = angleArc(Ts, Ls, Rs, 20);
|
||
body += aA.path + txt(aA.labelPos, fmt(apex) + '°', { fill: '#fff', size: 12 });
|
||
var aL = angleArc(Ls, Rs, Ts, 18);
|
||
body += aL.path + txt(aL.labelPos, '?', { fill: UNK, size: 15, weight: 800 });
|
||
body += tick(Ts, Ls) + tick(Ts, Rs);
|
||
return body;
|
||
},
|
||
|
||
/* Вертикальные углы: две пересекающиеся прямые; данный угол a°, противоположный «?». */
|
||
'vertical-angles': function (spec, p) {
|
||
var a = num(p, spec.given);
|
||
if (!(a > 0)) return null;
|
||
var A1 = P(-1, -0.55), A2 = P(1, 0.55), B1 = P(-1, 0.55), B2 = P(1, -0.55);
|
||
var f = fit([A1, A2, B1, B2]);
|
||
var a1 = f.px(A1), a2 = f.px(A2), b1 = f.px(B1), b2 = f.px(B2), O = f.px(P(0, 0));
|
||
var body = ln(a1, a2, { w: 2.4 }) + ln(b1, b2, { w: 2.4 }) + dot(O);
|
||
var arcR = angleArc(O, a2, b2, 20);
|
||
body += arcR.path + txt(arcR.labelPos, fmt(a) + '°', { fill: '#fff', size: 12 });
|
||
var arcL = angleArc(O, a1, b1, 20);
|
||
body += arcL.path + txt(arcL.labelPos, '?', { fill: UNK, size: 15, weight: 800 });
|
||
return body;
|
||
},
|
||
|
||
/* Расстояние между точками A,B на координатной сетке: катеты пунктиром, гипотенуза «?». */
|
||
'points-distance': function (spec, p) {
|
||
var x1 = num(p, spec.x1), y1 = num(p, spec.y1), x2 = num(p, spec.x2), y2 = num(p, spec.y2);
|
||
if (x1 == null || y1 == null || x2 == null || y2 == null) return null;
|
||
var pad = 1;
|
||
var minx = Math.min(x1, x2, 0) - pad, maxx = Math.max(x1, x2, 0) + pad;
|
||
var miny = Math.min(y1, y2, 0) - pad, maxy = Math.max(y1, y2, 0) + pad;
|
||
var f = fit([P(minx, miny), P(maxx, maxy)]);
|
||
var A = f.px(P(x1, y1)), B = f.px(P(x2, y2)), K = f.px(P(x2, y1));
|
||
var body = ln(f.px(P(minx, 0)), f.px(P(maxx, 0)), { stroke: 'rgba(255,255,255,.45)', w: 1.5 });
|
||
body += ln(f.px(P(0, miny)), f.px(P(0, maxy)), { stroke: 'rgba(255,255,255,.45)', w: 1.5 });
|
||
body += ln(A, K, { dash: true, stroke: DASH, w: 1.8 }) + ln(K, B, { dash: true, stroke: DASH, w: 1.8 });
|
||
body += ln(A, B, { w: 2.6 }) + rightAngle(K, A, B, 10) + dot(A) + dot(B);
|
||
body += txt(P(A.x - 6, A.y + 12), 'A', { fill: '#fff', size: 12, anchor: 'end' });
|
||
body += txt(P(B.x + 6, B.y - 10), 'B', { fill: '#fff', size: 12, anchor: 'start' });
|
||
body += txt(P((A.x + B.x) / 2 + 8, (A.y + B.y) / 2 - 8), '?', { fill: UNK, size: 14, weight: 800, anchor: 'start' });
|
||
return body;
|
||
},
|
||
|
||
/* Прямоугольный параллелепипед (кабинетная проекция): рёбра a,b,c; диагональ «?». */
|
||
'space-diagonal-box': function (spec, p) {
|
||
var a = num(p, spec.a), b = num(p, spec.b), c = num(p, spec.c);
|
||
if (!(a > 0) || !(b > 0) || !(c > 0)) return null;
|
||
var ox = b * 0.5, oy = b * 0.42;
|
||
var A = P(0, 0), B = P(a, 0), C = P(a, c), D = P(0, c);
|
||
var A2 = P(ox, oy), B2 = P(a + ox, oy), C2 = P(a + ox, c + oy), D2 = P(ox, c + oy);
|
||
var f = fit([A, B, C, D, A2, B2, C2, D2]);
|
||
function g(q) { return f.px(q); }
|
||
var as = g(A), bs = g(B), cs = g(C), ds = g(D), a2 = g(A2), b2 = g(B2), c2 = g(C2), d2 = g(D2);
|
||
var body = pgon([as, bs, cs, ds]);
|
||
body += ln(bs, b2, { w: 2 }) + ln(cs, c2, { w: 2 }) + ln(ds, d2, { w: 2 });
|
||
body += ln(b2, c2, { w: 2 }) + ln(c2, d2, { w: 2 });
|
||
body += ln(a2, b2, { w: 1.8, stroke: DASH }) + ln(a2, d2, { w: 1.8, stroke: DASH }) + ln(as, a2, { w: 1.8, stroke: DASH });
|
||
body += ln(as, c2, { dash: true, stroke: ARC, w: 2.4 });
|
||
body += dot(as) + dot(c2);
|
||
body += edgeLabel(as, bs, cs, fmt(a), {});
|
||
body += edgeLabel(as, ds, bs, fmt(c), {});
|
||
body += txt(P((as.x + a2.x) / 2 - 6, (as.y + a2.y) / 2 + 8), fmt(b), { fill: '#fff', size: 12, anchor: 'end' });
|
||
body += txt(P((as.x + c2.x) / 2 + 8, (as.y + c2.y) / 2), '?', { fill: UNK, size: 14, weight: 800, anchor: 'start' });
|
||
return body;
|
||
},
|
||
|
||
/* L-образная фигура: размеры W,H и вырез cw×ch (правый верхний угол). */
|
||
'l-shape': function (spec, p) {
|
||
var W = num(p, spec.W), H = num(p, spec.H), cw = num(p, spec.cw), ch = num(p, spec.ch);
|
||
if (!(W > 0) || !(H > 0) || !(cw > 0) || !(ch > 0) || cw >= W || ch >= H) return null;
|
||
var pts = [P(0, 0), P(W, 0), P(W, H - ch), P(W - cw, H - ch), P(W - cw, H), P(0, H)];
|
||
var f = fit(pts);
|
||
var sp = pts.map(function (q) { return f.px(q); });
|
||
var cen = f.px(P(W * 0.32, H * 0.4));
|
||
var body = pgon(sp);
|
||
body += edgeLabel(sp[0], sp[1], cen, fmt(W), {}); // низ
|
||
body += edgeLabel(sp[0], sp[5], cen, fmt(H), {}); // лево
|
||
body += edgeLabel(sp[2], sp[3], cen, fmt(cw), {}); // вырез — горизонталь
|
||
body += edgeLabel(sp[3], sp[4], cen, fmt(ch), {}); // вырез — вертикаль
|
||
return body;
|
||
},
|
||
|
||
/* Теорема Фалеса: треугольник + параллельная база; отрезки a,b слева, c и «?» справа. */
|
||
'thales-parallel': function (spec, p) {
|
||
var a = num(p, spec.a), b = num(p, spec.b), c = num(p, spec.c);
|
||
if (!(a > 0) || !(b > 0) || !(c > 0)) return null;
|
||
var T = P(0, 1.1), Lp = P(-0.95, -0.7), Rp = P(0.95, -0.7), fr = 0.45;
|
||
var PL = P(T.x + (Lp.x - T.x) * fr, T.y + (Lp.y - T.y) * fr);
|
||
var PR = P(T.x + (Rp.x - T.x) * fr, T.y + (Rp.y - T.y) * fr);
|
||
var f = fit([T, Lp, Rp]);
|
||
var ts = f.px(T), ls = f.px(Lp), rs = f.px(Rp), pl = f.px(PL), pr = f.px(PR);
|
||
var body = pgon([ts, ls, rs]) + ln(pl, pr, { w: 2.4, stroke: ARC }) + dot(pl) + dot(pr);
|
||
body += txt(P((ts.x + pl.x) / 2 - 8, (ts.y + pl.y) / 2), fmt(a), { fill: '#fff', size: 12, anchor: 'end' });
|
||
body += txt(P((pl.x + ls.x) / 2 - 8, (pl.y + ls.y) / 2), fmt(b), { fill: '#fff', size: 12, anchor: 'end' });
|
||
body += txt(P((ts.x + pr.x) / 2 + 8, (ts.y + pr.y) / 2), fmt(c), { fill: '#fff', size: 12, anchor: 'start' });
|
||
body += txt(P((pr.x + rs.x) / 2 + 8, (pr.y + rs.y) / 2), '?', { fill: UNK, size: 14, weight: 800, anchor: 'start' });
|
||
return body;
|
||
},
|
||
|
||
/* Вписанный и центральный угол: центральный a° при O, вписанный «?» при M. */
|
||
'inscribed-central-angle': function (spec, p) {
|
||
var a = num(p, spec.central);
|
||
if (!(a > 0) || a >= 320) return null;
|
||
var f = fit([P(-1, 0), P(1, 0), P(0, -1), P(0, 1)]);
|
||
var O = f.px(P(0, 0)), rad = f.s;
|
||
function onC(deg) { var t = deg2rad(deg); return P(O.x + Math.cos(t) * rad, O.y - Math.sin(t) * rad); }
|
||
var Pp = onC(90 - a / 2), Qp = onC(90 + a / 2), M = onC(-90);
|
||
var body = circleSvg(O, rad);
|
||
body += ln(O, Pp, { w: 2 }) + ln(O, Qp, { w: 2 }) + ln(M, Pp, { w: 2 }) + ln(M, Qp, { w: 2 });
|
||
body += dot(O, 2.6) + dot(Pp) + dot(Qp) + dot(M);
|
||
var arcO = angleArc(O, Pp, Qp, 18);
|
||
body += arcO.path + txt(arcO.labelPos, fmt(a) + '°', { fill: '#fff', size: 12 });
|
||
var arcM = angleArc(M, Pp, Qp, 22);
|
||
body += arcM.path + txt(arcM.labelPos, '?', { fill: UNK, size: 14, weight: 800 });
|
||
return body;
|
||
},
|
||
|
||
/* Хорда и расстояние до центра: перпендикуляр (пунктир) + прямой угол; хорда «?». */
|
||
'chord-circle': function (spec, p) {
|
||
var r = num(p, spec.r), dd = num(p, spec.dd);
|
||
if (!(r > 0) || !(dd > 0) || dd >= r) return null;
|
||
var f = fit([P(-1, 0), P(1, 0), P(0, -1), P(0, 1)]);
|
||
var O = f.px(P(0, 0)), rad = f.s;
|
||
var dpx = rad * dd / r, half = Math.sqrt(Math.max(0, rad * rad - dpx * dpx));
|
||
var Fc = P(O.x, O.y + dpx), Ch1 = P(Fc.x - half, Fc.y), Ch2 = P(Fc.x + half, Fc.y);
|
||
var body = circleSvg(O, rad);
|
||
body += ln(Ch1, Ch2, { w: 2.6 }) + ln(O, Fc, { dash: true, stroke: DASH, w: 1.8 });
|
||
body += ln(O, Ch2, { w: 1.8, stroke: 'rgba(255,255,255,.7)' }) + rightAngle(Fc, Ch2, O, 9);
|
||
body += dot(O, 2.6) + dot(Ch1) + dot(Ch2);
|
||
body += txt(P((O.x + Fc.x) / 2 + 7, (O.y + Fc.y) / 2), fmt(dd), { fill: '#fff', size: 11, anchor: 'start' });
|
||
body += txt(P((O.x + Ch2.x) / 2 + 6, (O.y + Ch2.y) / 2 - 6), 'r=' + fmt(r), { fill: '#fff', size: 11, anchor: 'start' });
|
||
body += txt(P(Fc.x, Ch2.y + 13), '?', { fill: UNK, size: 14, weight: 800 });
|
||
return body;
|
||
},
|
||
|
||
/* Касательная из внешней точки: радиус ⟂ касательной; касательная «?». */
|
||
'tangent-circle': function (spec, p) {
|
||
var r = num(p, spec.r), op = num(p, spec.op);
|
||
if (!(r > 0) || !(op > 0) || op <= r) return null;
|
||
var f = fit([P(-1, 0), P(1.6, 0), P(0, -1), P(0, 1)]);
|
||
var O = f.px(P(0, 0)), rad = f.s;
|
||
var Tang = P(O.x + Math.cos(deg2rad(60)) * rad, O.y - Math.sin(deg2rad(60)) * rad);
|
||
var ur = norm(sub(Tang, O)), tdir = P(-ur.y, ur.x);
|
||
var Pt = P(Tang.x + tdir.x * rad * 1.3, Tang.y + tdir.y * rad * 1.3);
|
||
var body = circleSvg(O, rad);
|
||
body += ln(O, Tang, { w: 1.8, stroke: 'rgba(255,255,255,.7)' }) + ln(Tang, Pt, { w: 2.6 });
|
||
body += ln(O, Pt, { dash: true, stroke: DASH, w: 1.8 }) + rightAngle(Tang, O, Pt, 9);
|
||
body += dot(O, 2.6) + dot(Tang) + dot(Pt);
|
||
body += txt(P((O.x + Tang.x) / 2 - 6, (O.y + Tang.y) / 2), 'r=' + fmt(r), { fill: '#fff', size: 11, anchor: 'end' });
|
||
body += txt(P((O.x + Pt.x) / 2, (O.y + Pt.y) / 2 + 13), fmt(op), { fill: '#fff', size: 11 });
|
||
body += txt(P((Tang.x + Pt.x) / 2 + 4, (Tang.y + Pt.y) / 2 - 8), '?', { fill: UNK, size: 14, weight: 800, anchor: 'start' });
|
||
return body;
|
||
}
|
||
};
|
||
|
||
// Опции подписи: искомая величина — амбер/крупнее.
|
||
function unkOpt(key, unknownKey) {
|
||
if (unknownKey != null && key === unknownKey) return { fill: UNK, size: 16, weight: 800 };
|
||
return { fill: '#fff', size: 13 };
|
||
}
|
||
|
||
function render(figureSpec, params) {
|
||
if (!figureSpec || typeof figureSpec !== 'object') return null;
|
||
var fn = TYPES[figureSpec.type];
|
||
if (typeof fn !== 'function') return null;
|
||
var body;
|
||
try { body = fn(figureSpec, params || {}, U); }
|
||
catch (e) { return null; }
|
||
if (!body) return null;
|
||
return '<svg class="tr-fig-svg" viewBox="0 0 ' + VB_W + ' ' + VB_H +
|
||
'" role="img" aria-hidden="true" preserveAspectRatio="xMidYMid meet" ' +
|
||
'style="overflow:visible">' + body + '</svg>';
|
||
}
|
||
|
||
global.TrainerFigures = {
|
||
render: render,
|
||
has: function (type) { return typeof TYPES[type] === 'function'; },
|
||
TYPES: TYPES,
|
||
_util: U
|
||
};
|
||
|
||
})(typeof window !== 'undefined' ? window : globalThis);
|