'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-строка (внутренности ). 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, '>'); } // Аккуратное число для подписи (целое без хвоста, иначе до 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 ''; } function pgon(ptsScreen, opt) { opt = opt || {}; var d = ptsScreen.map(function (p) { return r1(p.x) + ',' + r1(p.y); }).join(' '); return ''; } function dot(p, rr) { return ''; } // Текст с тёмным гало (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 '' + esc(s) + ''; } // Подпись величины у середины ребра 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 = ''; 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 ''; } 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 }; /* ════════════════ ТИПЫ ФИГУР ════════════════ */ 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, fmt(w), {}); body += edgeLabel(Bs, Cs, cen, fmt(h), {}); 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) : num(p, spec.side); 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') { body += txt(P(c1.x, c1.y), (known != null ? 'P=' + fmt(known) : 'P'), { 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, '?', { fill: UNK, size: 15, weight: 800 }); } // коэффициент подобия между фигурами 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 = ''; 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 = ''; var seg = 28, ap = []; for (var i = 0; i <= seg; i++) ap.push(onC(nAng * i / seg)); body += ''; 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; } }; // Опции подписи: искомая величина — амбер/крупнее. 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 ''; } global.TrainerFigures = { render: render, has: function (type) { return typeof TYPES[type] === 'function'; }, TYPES: TYPES, _util: U }; })(typeof window !== 'undefined' ? window : globalThis);