feat(trainer): тема «Окружность» + режим «читать условие с чертежа»

Окружность (новая геом-тема g-circle, 9 кл, π ≈ 3,14 → ответ — конечная
десятичная дробь, ученик вводит её): 4 генератора — длина окружности по радиусу
(2πr), по диаметру (πd), площадь круга (πr²), длина дуги ((n/360)·2πr, n=45·k,
require r·k чётно → дробь конечная). Новые типы фигур: circle (радиус/диаметр/
заливка) и circle-arc (два радиуса под центральным углом + выделенная дуга).

Режим «читать значения с чертежа»: у всех 19 геом-генераторов добавлено
figurePrompt (краткое условие); переключатель «Текст / На чертеже» (#tr-figmode)
на странице, выбор сохраняется в localStorage. В режиме чертежа числа берутся с
фигуры, текст минимальный. Движок прокидывает figurePrompt; showStatement
выбирает полный текст или промпт; renderFigureToggle показан только для задач с
чертежом; для текстовых/алгебраических задач режим скрыт, проверка ответа от
режима не зависит. На чертеж n-угольника выведено число сторон (n = …).

Верификация: headless-смоук 6968 проверок / 1140 рендеров; ответы окружности
конечные и принимаются движком (1600 экземпляров, округление до 2 знаков ok);
inline-скрипт парсится; адверсариал-ревью — circle clean, toggle без high/medium
(2 low устранены: скрытие тумблера при неудаче генерации + подпись n сторон).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-26 12:49:15 +03:00
parent ff9900bdcc
commit 1dcde8790a
4 changed files with 175 additions and 5 deletions
+1
View File
@@ -370,6 +370,7 @@
title: gen.title,
kind: kind,
figure: gen.figure || null, // спека чертежа (данные) — рисует TrainerFigures по params
figurePrompt: gen.figurePrompt || null, // краткое условие для режима «читать с чертежа»
lhsExpr: lhsExpr,
rhsExpr: rhsExpr,
display: (kind === 'system' && system)
+52
View File
@@ -375,6 +375,7 @@
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);
@@ -418,6 +419,57 @@
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;
}
};
+84 -1
View File
@@ -41,7 +41,8 @@
{ key: 'g-pyth', label: 'Пифагор', subject: 'geometry', grade: 8, order: 13 },
{ key: 'g-area', label: 'Площади', subject: 'geometry', grade: 8, order: 14 },
{ key: 'g-poly', label: 'Многоугольники', subject: 'geometry', grade: 8, order: 15 },
{ key: 'g-sim', label: 'Подобие', subject: 'geometry', grade: 8, order: 16 }
{ key: 'g-sim', label: 'Подобие', subject: 'geometry', grade: 8, order: 16 },
{ key: 'g-circle', label: 'Окружность', subject: 'geometry', grade: 9, order: 17 }
];
var GENERATORS = [
@@ -501,6 +502,7 @@
id: 'ang-triangle', topic: 'g-angles', order: 1, subject: 'geometry', grade: 7, kind: 'compute',
title: 'Третий угол треугольника',
figure: { type: 'triangle-angles', angA: 'a', angB: 'b' },
figurePrompt: 'Найдите неизвестный угол треугольника (в градусах).',
pick: { a: [20, 80], b: [20, 80] }, derive: { val: '180 - a - b' }, require: 'val >= 15 && val <= 150',
lhs: 'x', rhs: '180 - {a} - {b}', display: 'В треугольнике два угла равны {a}° и {b}°. Найдите третий угол (в градусах).',
answerVar: 'x', answer: 'val', integerAnswer: true,
@@ -515,6 +517,7 @@
id: 'ang-adjacent', topic: 'g-angles', order: 2, subject: 'geometry', grade: 7, kind: 'compute',
title: 'Смежный угол',
figure: { type: 'adjacent-angles', ang: 'a' },
figurePrompt: 'Найдите неизвестный угол (в градусах).',
pick: { a: [25, 155] }, derive: { val: '180 - a' },
lhs: 'x', rhs: '180 - {a}', display: 'Один из смежных углов равен {a}°. Найдите другой смежный с ним угол.',
answerVar: 'x', answer: 'val', integerAnswer: true,
@@ -529,6 +532,7 @@
id: 'ang-exterior', topic: 'g-angles', order: 3, subject: 'geometry', grade: 7, kind: 'compute',
title: 'Внешний угол треугольника',
figure: { type: 'triangle-angles', angA: 'a', angB: 'b', ext: true },
figurePrompt: 'Найдите внешний угол треугольника (в градусах).',
pick: { a: [20, 80], b: [20, 80] }, derive: { val: 'a + b' }, require: 'val <= 160',
lhs: 'x', rhs: '{a} + {b}', display: 'Внешний угол треугольника равен сумме двух не смежных с ним внутренних углов. Эти углы равны {a}° и {b}°. Найдите внешний угол.',
answerVar: 'x', answer: 'val', integerAnswer: true,
@@ -545,6 +549,7 @@
id: 'pyth-hyp', topic: 'g-pyth', order: 1, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Гипотенуза (Пифагор)',
figure: { type: 'right-triangle', a: 'a', b: 'b', c: 'c', unknown: 'c' },
figurePrompt: 'Найдите гипотенузу прямоугольного треугольника (отмечена «?»).',
pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n',
derive: { a: 'm*m - n*n', b: '2*m*n', c: 'm*m + n*n' },
lhs: 'x', rhs: 'sqrt({a}^2 + {b}^2)', display: 'Катеты прямоугольного треугольника равны {a} и {b}. Найдите гипотенузу.',
@@ -560,6 +565,7 @@
id: 'pyth-leg', topic: 'g-pyth', order: 2, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Катет (Пифагор)',
figure: { type: 'right-triangle', a: 'a', b: 'b', c: 'c', unknown: 'b' },
figurePrompt: 'Найдите неизвестный катет (отмечен «?»).',
pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n',
derive: { a: 'm*m - n*n', b: '2*m*n', c: 'm*m + n*n' },
lhs: 'x', rhs: 'sqrt({c}^2 - {a}^2)', display: 'Гипотенуза прямоугольного треугольника {c}, один катет {a}. Найдите второй катет.',
@@ -577,6 +583,7 @@
id: 'area-rect', topic: 'g-area', order: 1, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Площадь прямоугольника',
figure: { type: 'rectangle', w: 'a', h: 'b' },
figurePrompt: 'Найдите площадь прямоугольника.',
pick: { a: [2, 16], b: [2, 16] }, derive: { val: 'a*b' },
lhs: 'x', rhs: '{a}*{b}', display: 'Стороны прямоугольника {a} и {b}. Найдите его площадь.',
answerVar: 'x', answer: 'val', integerAnswer: true,
@@ -591,6 +598,7 @@
id: 'area-triangle', topic: 'g-area', order: 2, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Площадь треугольника',
figure: { type: 'triangle-base-height', base: 'a', height: 'h' },
figurePrompt: 'Найдите площадь треугольника.',
pick: { a: [2, 16], h: [2, 16] }, require: 'mod(a*h, 2) == 0',
derive: { val: 'a*h/2' },
lhs: 'x', rhs: '{a}*{h}/2', display: 'Основание треугольника {a}, высота к нему {h}. Найдите площадь.',
@@ -606,6 +614,7 @@
id: 'area-square', topic: 'g-area', order: 3, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Площадь квадрата',
figure: { type: 'square', a: 'a' },
figurePrompt: 'Найдите площадь квадрата.',
pick: { a: [2, 20] }, derive: { val: 'a*a' },
lhs: 'x', rhs: '{a}^2', display: 'Сторона квадрата {a}. Найдите его площадь.',
answerVar: 'x', answer: 'val', integerAnswer: true,
@@ -733,6 +742,7 @@
id: 'area-trapezoid', topic: 'g-area', order: 4, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Площадь трапеции',
figure: { type: 'trapezoid', bottom: 'a', top: 'b', height: 'h' },
figurePrompt: 'Найдите площадь трапеции.',
pick: { a: [2, 14], b: [2, 14], h: [2, 12] }, require: 'mod((a + b)*h, 2) == 0',
derive: { val: '(a + b)*h/2' },
lhs: 'x', rhs: '({a} + {b})*{h}/2', display: 'Основания трапеции {a} и {b}, высота {h}. Найдите площадь.',
@@ -748,6 +758,7 @@
id: 'area-parallelogram', topic: 'g-area', order: 5, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Площадь параллелограмма',
figure: { type: 'parallelogram', base: 'a', height: 'h' },
figurePrompt: 'Найдите площадь параллелограмма.',
pick: { a: [2, 16], h: [2, 14] }, derive: { val: 'a*h' },
lhs: 'x', rhs: '{a}*{h}', display: 'Сторона параллелограмма {a}, высота к ней {h}. Найдите площадь.',
answerVar: 'x', answer: 'val', integerAnswer: true,
@@ -762,6 +773,7 @@
id: 'area-rhombus', topic: 'g-area', order: 6, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Площадь ромба',
figure: { type: 'rhombus', d1: 'd1', d2: 'd2' },
figurePrompt: 'Найдите площадь ромба.',
pick: { d1: [2, 16], d2: [2, 16] }, require: 'mod(d1*d2, 2) == 0',
derive: { val: 'd1*d2/2' },
lhs: 'x', rhs: '{d1}*{d2}/2', display: 'Диагонали ромба {d1} и {d2}. Найдите площадь.',
@@ -779,6 +791,7 @@
id: 'poly-angles-sum', topic: 'g-poly', order: 1, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Сумма углов многоугольника',
figure: { type: 'regular-polygon', n: 'n' },
figurePrompt: 'Найдите сумму углов многоугольника на чертеже (в градусах).',
pick: { n: [3, 12] }, derive: { val: '180*(n - 2)' },
lhs: 'x', rhs: '180*({n} - 2)', display: 'Найдите сумму углов выпуклого {n}-угольника (в градусах).',
answerVar: 'x', answer: 'val', integerAnswer: true,
@@ -793,6 +806,7 @@
id: 'poly-regular-angle', topic: 'g-poly', order: 2, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Угол правильного многоугольника',
figure: { type: 'regular-polygon', n: 'n', markAngle: true },
figurePrompt: 'Найдите угол правильного многоугольника на чертеже (в градусах).',
pick: { n: [3, 20] }, require: 'mod(180*(n - 2), n) == 0',
derive: { val: '180*(n - 2)/n' },
lhs: 'x', rhs: '180*({n} - 2)/{n}', display: 'Найдите величину угла правильного {n}-угольника (в градусах).',
@@ -810,6 +824,7 @@
id: 'sim-side', topic: 'g-sim', order: 1, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Сторона по коэффициенту',
figure: { type: 'two-similar', side: 'a', k: 'k', mode: 'side' },
figurePrompt: 'Найдите сходственную сторону подобного треугольника (отмечена «?»).',
pick: { a: [2, 15], k: [2, 5] }, derive: { val: 'a*k' },
lhs: 'x', rhs: '{a}*{k}', display: 'Треугольники подобны с коэффициентом {k}. Сторона первого равна {a}. Найдите сходственную сторону второго.',
answerVar: 'x', answer: 'val', integerAnswer: true,
@@ -824,6 +839,7 @@
id: 'sim-perimeter', topic: 'g-sim', order: 2, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Периметр подобной фигуры',
figure: { type: 'two-similar', perim: 'p', k: 'k', mode: 'perimeter' },
figurePrompt: 'Найдите периметр подобной фигуры (отмечена «?»).',
pick: { p: [5, 30], k: [2, 5] }, derive: { val: 'p*k' },
lhs: 'x', rhs: '{p}*{k}', display: 'Фигуры подобны с коэффициентом {k}. Периметр первой равен {p}. Найдите периметр второй.',
answerVar: 'x', answer: 'val', integerAnswer: true,
@@ -991,6 +1007,71 @@
{ note: 'Перемножаем модули; знак минус — если множители разных знаков, плюс — если одинаковых:', tex: 'x = {a}*{b}' },
{ note: 'Получаем:', tex: 'x = {ans}' }
]
},
/* ═══ Тема: Окружность (геометрия, 9 класс) ═══
Считаем с π ≈ 3,14 (как в учебниках) → ответ — КОНЕЧНАЯ десятичная дробь,
ученик вводит её (SimExpr принимает «31.4»). Чертёж показывает радиус/диаметр/угол. */
/* длина окружности по радиусу: C = 2πr */
{
id: 'circ-length', topic: 'g-circle', order: 1, subject: 'geometry', grade: 9, kind: 'compute',
title: 'Длина окружности',
figure: { type: 'circle', r: 'r', show: 'radius' },
figurePrompt: 'Найдите длину окружности (π ≈ 3,14).',
pick: { r: [1, 20] }, derive: { val: '2*3.14*r' },
lhs: 'x', rhs: '2*3.14*{r}', display: 'Радиус окружности равен {r}. Найдите длину окружности (π ≈ 3,14).',
answerVar: 'x', answer: 'val',
solution: [
{ note: 'Длина окружности вычисляется по формуле C = 2πr. Подставляем r = {r} и π ≈ 3,14:', tex: 'x = 2*3.14*{r}' },
{ note: 'Считаем:', tex: 'x = {ans}' }
]
},
/* длина окружности по диаметру: C = πd */
{
id: 'circ-diam', topic: 'g-circle', order: 2, subject: 'geometry', grade: 9, kind: 'compute',
title: 'Длина по диаметру',
figure: { type: 'circle', d: 'd', show: 'diameter' },
figurePrompt: 'Найдите длину окружности (π ≈ 3,14).',
pick: { d: [2, 30] }, derive: { val: '3.14*d' },
lhs: 'x', rhs: '3.14*{d}', display: 'Диаметр окружности равен {d}. Найдите длину окружности (π ≈ 3,14).',
answerVar: 'x', answer: 'val',
solution: [
{ note: 'Длину окружности можно найти через диаметр: C = πd. Подставляем d = {d} и π ≈ 3,14:', tex: 'x = 3.14*{d}' },
{ note: 'Считаем:', tex: 'x = {ans}' }
]
},
/* площадь круга: S = πr² */
{
id: 'circ-area', topic: 'g-circle', order: 3, subject: 'geometry', grade: 9, kind: 'compute',
title: 'Площадь круга',
figure: { type: 'circle', r: 'r', show: 'area' },
figurePrompt: 'Найдите площадь круга (π ≈ 3,14).',
pick: { r: [1, 15] }, derive: { val: '3.14*r^2' },
lhs: 'x', rhs: '3.14*{r}^2', display: 'Радиус круга равен {r}. Найдите его площадь (π ≈ 3,14).',
answerVar: 'x', answer: 'val',
solution: [
{ note: 'Площадь круга вычисляется по формуле S = πr². Подставляем r = {r} и π ≈ 3,14:', tex: 'x = 3.14*{r}^2' },
{ note: 'Считаем:', tex: 'x = {ans}' }
]
},
/* длина дуги: L = (n/360)·2πr; n = 45·k, требуем конечную дробь (r·k чётно) */
{
id: 'circ-arc', topic: 'g-circle', order: 4, subject: 'geometry', grade: 9, kind: 'compute',
title: 'Длина дуги',
figure: { type: 'circle-arc', r: 'r', angle: 'n' },
figurePrompt: 'Найдите длину дуги (π ≈ 3,14).',
pick: { r: [3, 12], k: [1, 7] }, require: 'mod(r*k, 2) == 0',
derive: { n: '45*k', val: '45*k/360*2*3.14*r' },
lhs: 'x', rhs: '{n}/360*2*3.14*{r}', display: 'Дуга окружности радиуса {r} опирается на центральный угол {n}°. Найдите длину дуги (π ≈ 3,14).',
answerVar: 'x', answer: 'val',
solution: [
{ note: 'Длина дуги — это часть длины окружности: L = (n/360)·2πr. Здесь n = {n}°, r = {r}, π ≈ 3,14:', tex: 'x = {n}/360*2*3.14*{r}' },
{ note: 'Считаем:', tex: 'x = {ans}' }
]
}
];
@@ -1031,6 +1112,8 @@
'area-parallelogram': 2, 'area-trapezoid': 3, 'area-rhombus': 2,
// Геометрия — Многоугольники / Подобие
'poly-angles-sum': 1, 'poly-regular-angle': 2, 'sim-side': 1, 'sim-perimeter': 2,
// Геометрия — Окружность
'circ-length': 1, 'circ-diam': 1, 'circ-area': 2, 'circ-arc': 3,
// НОД/НОК / Дроби / Десятичные / Отрицательные
'gcd-pair': 1, 'lcm-pair': 2,
'frac-of-number': 1, 'frac-add-same': 2,
+38 -4
View File
@@ -230,6 +230,13 @@
.tr-diff-btn:hover { border-color: var(--g1); color: var(--accent-ink); }
.tr-diff-btn.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 8px 16px -6px rgba(99,102,241,.5); }
/* ── переключатель «условие текстом / на чертеже» (геометрия) ── */
.tr-figmode { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: center; margin-bottom: 16px; }
.tr-fm-label { font-size: .68rem; font-weight: 800; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .07em; margin-right: 4px; }
.tr-fm-btn { font: inherit; font-size: .8rem; font-weight: 800; cursor: pointer; padding: 6px 14px; border-radius: 99px; border: 1px solid var(--line); background: #fff; color: var(--ink-soft); transition: .14s var(--ease); }
.tr-fm-btn:hover { border-color: var(--g1); color: var(--accent-ink); }
.tr-fm-btn.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 8px 16px -6px rgba(99,102,241,.5); }
/* строка ответа */
.tr-inrow { display: flex; gap: 10px; align-items: stretch; max-width: 460px; margin: 0 auto; }
#tr-eqx { font-family: 'Cambria Math', serif; font-size: 1.55rem; font-weight: 600; color: var(--accent-ink); align-self: center; padding-left: 4px; }
@@ -448,6 +455,7 @@
<div class="tr-figure" id="tr-figure" style="display:none"></div>
</div>
<div class="tr-work">
<div class="tr-figmode" id="tr-figmode" style="display:none"></div>
<div class="tr-difficulty" id="tr-difficulty"></div>
<div id="tr-answerbox">
@@ -600,6 +608,22 @@
if (svg) { box.innerHTML = svg; box.style.display = ''; }
else { box.innerHTML = ''; box.style.display = 'none'; }
}
// Условие: полный текст ИЛИ краткий промпт (числа читаются с чертежа).
function showStatement(problem) {
var eq = $('tr-eq'); if (!eq || !problem) return;
var useFig = figureMode && problem.figure && problem.figurePrompt;
if (useFig) { eq.classList.add('tr-eq-text'); setMath(eq, null, problem.figurePrompt, true); }
else { eq.classList.toggle('tr-eq-text', !problem.latex); setMath(eq, problem.latex, problem.display, true); }
}
// Переключатель «Текст / На чертеже» — только для задач с чертежом и кратким условием.
function renderFigureToggle() {
var box = $('tr-figmode'); if (!box) return;
if (!(cur && cur.figure && cur.figurePrompt)) { box.style.display = 'none'; box.innerHTML = ''; return; }
box.style.display = '';
box.innerHTML = '<span class="tr-fm-label">Условие</span>' +
'<button type="button" class="tr-fm-btn' + (!figureMode ? ' on' : '') + '" data-fm="0">Текст</button>' +
'<button type="button" class="tr-fm-btn' + (figureMode ? ' on' : '') + '" data-fm="1">На чертеже</button>';
}
var topics = (TG.topics ? TG.topics() : [{ key: null, label: 'Задачи' }]).concat([{ key: 'word', label: 'Текстовые задачи', word: true }]);
var isTeacher = !!(ip && ip.isTeacher);
@@ -608,6 +632,8 @@
var diffMode = 'auto'; // уровень сложности: 'auto' | 1 | 2 | 3 (= структурный вариант)
var pinned = null; // закреплённый навык (id) при явном клике по чипу
var customGens = []; // пользовательские генераторы (P13), тема «Авторские»
var figureMode = false; // «читать условие с чертежа» (числа на фигуре, текст краткий)
try { figureMode = localStorage.getItem('tr-figure-mode') === '1'; } catch (e) {}
function skillKey(g) { return g.skill || g.id; }
function skillsOf(topicKey) {
if (topicKey === 'custom') return customGens;
@@ -641,6 +667,7 @@
function serveWordProblem() {
var eq = $('tr-eq'); eq.classList.add('tr-eq-text');
renderFigure(null); // текстовые задачи — без чертежа
var fmb = $('tr-figmode'); if (fmb) { fmb.style.display = 'none'; fmb.innerHTML = ''; }
$('tr-solution').style.display = 'none'; $('tr-solution').innerHTML = '';
var fb = $('tr-feedback'); fb.className = 'tr-feedback'; fb.textContent = '';
if (!wordPool.length) {
@@ -937,14 +964,13 @@
// strict:false + несколько попыток на случай редкой неудачи с ограничениями
cur = null;
for (var i = 0; i < 6 && !cur; i++) cur = TE.instantiate(curGen, { seed: randSeed(), strict: false });
if (!cur) { $('tr-eq').textContent = 'Не удалось сгенерировать задачу'; return; }
if (!cur) { $('tr-eq').textContent = 'Не удалось сгенерировать задачу'; renderFigure(null); renderFigureToggle(); return; }
renderSkills(); // подсветить активный навык (мог смениться вместе с уровнем)
$('tr-skill').textContent = curGen.title;
var eq = $('tr-eq');
eq.classList.toggle('tr-eq-text', !cur.latex); // текстовый prompt (проценты/упрощение) — другим шрифтом
setMath(eq, cur.latex, cur.display, true);
showStatement(cur); // условие: полный текст ИЛИ краткий промпт (числа на чертеже)
renderFigure(cur); // чертёж для геометрии (иначе скрыт)
renderFigureToggle(); // переключатель «Текст / На чертеже» (если уместен)
applyInputMode();
var inp = $('tr-input');
inp.value = ''; inp.disabled = false;
@@ -1202,6 +1228,14 @@
$('tr-teacher').addEventListener('click', function (e) { if (e.target === $('tr-teacher')) $('tr-teacher').style.display = 'none'; });
$('tr-analytics-btn').addEventListener('click', openAnalytics);
$('tr-builder-btn').addEventListener('click', function () { location.href = '/trainer-builder'; });
$('tr-figmode').addEventListener('click', function (e) {
var b = e.target.closest('.tr-fm-btn'); if (!b) return;
var v = b.getAttribute('data-fm') === '1';
if (v === figureMode) return;
figureMode = v;
try { localStorage.setItem('tr-figure-mode', v ? '1' : '0'); } catch (_) {}
showStatement(cur); renderFigureToggle();
});
$('tr-difficulty').addEventListener('click', function (e) {
var b = e.target.closest('.tr-diff-btn'); if (!b) return;
var d = b.getAttribute('data-d');