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
+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;
}
};