feat(trainer): тема «Углы» — +11 генераторов + 3 фигуры (максимум разнообразия)

Реализован раздел B плана ROADMAP_V4_VARIETY. Все «корень-вперёд», целые ответы.

Генераторы (g-angles): ang-alt-exterior (накрест внешние), ang-coint-exterior
(односторонние внешние), ang-parallel-twostep (двухшаговая), ang-alt-solve
(накрест заданы выражениями — найти x), ang-bisector (биссектриса, a/2),
ang-complementary (дополнительные, 90−a), ang-right-acute (острый угол прям.
треугольника, 90−a), ang-parallelogram (соседний угол, 180−a), ang-polygon-missing
(недостающий угол n-угольника), ang-triangle-ratio (углы в отношении p:q:r —
наибольший), ang-clock (угол между стрелками часов).

figures.js: +3 типа (angle-bisector, complementary, clock) + расширен
parallelogram (markAngle — дуги углов вместо размеров). ang-right-acute
переиспользует triangle-angles (угол 90°), ang-polygon-missing — regular-polygon.

Итого 189 генераторов; тема «Углы» — 20 генераторов. Смоук v41 91934 проверки
(рендер фигур, приём/отказ ответа, шаги→LaTeX); figures-смоук 18565 / 3060
рендеров на 51 геом-генераторе.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-29 16:26:42 +03:00
parent 8610197f5f
commit fa034fee7c
2 changed files with 238 additions and 0 deletions
+60
View File
@@ -357,6 +357,14 @@
var f = fit([A, B, C, D]); 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 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]); var body = pgon([As, Bs, Cs, Ds]);
if (spec.markAngle != null) { // задача про углы: дуги вместо размеров
var ma = num(p, spec.markAngle);
var arcA = angleArc(As, Bs, Ds, 18);
body += arcA.path + txt(arcA.labelPos, fmt(ma) + '°', { fill: '#fff', size: 12 });
var arcB = angleArc(Bs, Cs, As, 18);
body += arcB.path + txt(arcB.labelPos, '?', { fill: UNK, size: 14, weight: 800 });
return body;
}
// высота от D перпендикулярно основанию (на проекцию D=skew) // высота от D перпендикулярно основанию (на проекцию D=skew)
var Fs = f.px(P(skew, 0)); var Fs = f.px(P(skew, 0));
body += ln(Ds, Fs, { dash: true, stroke: DASH, w: 2 }); body += ln(Ds, Fs, { dash: true, stroke: DASH, w: 2 });
@@ -680,6 +688,58 @@
body += txt(P((O.x + Pt.x) / 2, (O.y + Pt.y) / 2 + 13), fmt(op), { fill: '#fff', size: 11 }); 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' }); 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; return body;
},
/* Биссектриса делит угол full° пополам: два луча + пунктирная биссектриса, половина «?». */
'angle-bisector': function (spec, p) {
var full = num(p, spec.full);
if (!(full > 0) || full >= 180) return null;
var O = P(-1, 0), hRad = deg2rad(full / 2);
var R1 = P(O.x + Math.cos(hRad) * 2.2, O.y + Math.sin(hRad) * 2.2);
var R2 = P(O.x + Math.cos(-hRad) * 2.2, O.y + Math.sin(-hRad) * 2.2);
var Bz = P(O.x + 2.2, O.y);
var f = fit([O, R1, R2, Bz]);
var Os = f.px(O), r1 = f.px(R1), r2 = f.px(R2), bz = f.px(Bz);
var body = ln(Os, r1, { w: 2.4 }) + ln(Os, r2, { w: 2.4 }) + ln(Os, bz, { dash: true, stroke: ARC, w: 2 }) + dot(Os);
var arcFull = angleArc(Os, r1, r2, 28);
body += arcFull.path + txt(arcFull.labelPos, fmt(full) + '°', { fill: '#fff', size: 12 });
var arcHalf = angleArc(Os, r1, bz, 15);
body += arcHalf.path + txt(arcHalf.labelPos, '?', { fill: UNK, size: 14, weight: 800 });
return body;
},
/* Дополнительные углы: прямой угол (горизонт.+верт. лучи), средний луч делит на a° и «?». */
'complementary': function (spec, p) {
var a = num(p, spec.given);
if (!(a > 0) || a >= 90) return null;
var O = P(0, 0), Hr = P(1.4, 0), Vr = P(0, 1.4), Mr = P(Math.cos(deg2rad(a)), Math.sin(deg2rad(a)));
var f = fit([O, Hr, Vr]);
var Os = f.px(O), hr = f.px(Hr), vr = f.px(Vr), mr = f.px(Mr);
var body = ln(Os, hr, { w: 2.4 }) + ln(Os, vr, { w: 2.4 }) + ln(Os, mr, { w: 2.2 }) + dot(Os);
body += rightAngle(Os, hr, vr, 11);
var arcA = angleArc(Os, hr, mr, 18);
body += arcA.path + txt(arcA.labelPos, fmt(a) + '°', { fill: '#fff', size: 12 });
var arcQ = angleArc(Os, mr, vr, 18);
body += arcQ.path + txt(arcQ.labelPos, '?', { fill: UNK, size: 14, weight: 800 });
return body;
},
/* Циферблат: окружность + 12 делений, минутная стрелка вверх (12), часовая к hour. */
'clock': function (spec, p) {
var H = num(p, spec.hour);
if (!(H >= 0)) 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 body = circleSvg(O, rad) + dot(O, 2.6);
for (var i = 0; i < 12; i++) {
var ta = deg2rad(90 - i * 30);
body += ln(P(O.x + Math.cos(ta) * rad, O.y - Math.sin(ta) * rad),
P(O.x + Math.cos(ta) * rad * 0.88, O.y - Math.sin(ta) * rad * 0.88), { w: 1.6, stroke: 'rgba(255,255,255,.6)' });
}
body += ln(O, P(O.x, O.y - rad * 0.82), { w: 2.6 }); // минутная (на 12)
var hA = deg2rad(90 - H * 30);
body += ln(O, P(O.x + Math.cos(hA) * rad * 0.55, O.y - Math.sin(hA) * rad * 0.55), { w: 3.4, stroke: ARC }); // часовая
return body;
} }
}; };
+178
View File
@@ -2861,6 +2861,181 @@
{ note: 'Длина касательной = √(OP² − r²).', tex: 'x = sqrt({op}^2 - {r}^2)' }, { note: 'Длина касательной = √(OP² − r²).', tex: 'x = sqrt({op}^2 - {r}^2)' },
{ note: 'Считаем.', tex: 'x = {ans}' } { note: 'Считаем.', tex: 'x = {ans}' }
] ]
},
/* ═══════════════════════════════════════════════════════════════════════
V4 «Максимум разнообразия» — Углы: ещё пары при параллельных, биссектриса,
дополнительные, прямоугольный/параллелограмм/многоугольник, отношение, часы.
═══════════════════════════════════════════════════════════════════════ */
/* накрест лежащие внешние углы */
{
id: 'ang-alt-exterior', topic: 'g-angles', order: 4.3, subject: 'geometry', grade: 7, kind: 'compute',
title: 'Накрест лежащие внешние',
figure: { type: 'parallel-lines-transversal', given: 'a', rel: 'alternate' },
figurePrompt: 'Найдите накрест лежащий внешний угол (в градусах).',
pick: { a: [30, 150] }, derive: { val: 'a' },
lhs: 'x', rhs: '{a}', display: 'Прямые параллельны, секущая образует угол {a}°. Найдите накрест лежащий внешний с ним угол.',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Накрест лежащие внешние углы при параллельных прямых равны.', tex: 'x = {a}' },
{ note: 'Ответ.', tex: 'x = {ans}' }
]
},
/* односторонние внешние углы */
{
id: 'ang-coint-exterior', topic: 'g-angles', order: 4.5, subject: 'geometry', grade: 7, kind: 'compute',
title: 'Односторонние внешние',
figure: { type: 'parallel-lines-transversal', given: 'a', rel: 'cointerior' },
figurePrompt: 'Найдите односторонний внешний угол (в градусах).',
pick: { a: [30, 150] }, derive: { val: '180 - a' },
lhs: 'x', rhs: '180 - {a}', display: 'Прямые параллельны, секущая образует угол {a}°. Найдите односторонний внешний с ним угол.',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Сумма односторонних внешних углов при параллельных прямых равна 180°.', tex: 'x = 180 - {a}' },
{ note: 'Считаем.', tex: 'x = {ans}' }
]
},
/* двухшаговая: накрест лежащий, затем смежный с ним */
{
id: 'ang-parallel-twostep', topic: 'g-angles', order: 4.7, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Параллельные: два шага',
figure: { type: 'parallel-lines-transversal', given: 'a', rel: 'cointerior' },
figurePrompt: 'Найдите смежный с накрест лежащим угол (в градусах).',
pick: { a: [30, 140] }, derive: { val: '180 - a' },
lhs: 'x', rhs: '180 - {a}', display: 'Прямые параллельны, секущая образует угол {a}°. Найдите угол, смежный с накрест лежащим ему.',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Шаг 1: накрест лежащий угол равен {a}°.', tex: '' },
{ note: 'Шаг 2: смежный с ним угол дополняет до 180°.', tex: 'x = 180 - {a}' },
{ note: 'Считаем.', tex: 'x = {ans}' }
]
},
/* накрест лежащие заданы выражениями — найти x */
{
id: 'ang-alt-solve', topic: 'g-angles', order: 4.8, subject: 'geometry', grade: 7,
title: 'Накрест лежащие: найти x',
figure: { type: 'parallel-lines-transversal', given: 'c', rel: 'alternate' },
figurePrompt: 'Накрест лежащие углы равны. Найдите x.',
pick: { a: [2, 6], b: [1, 20], root: [2, 12] },
derive: { c: 'a*root + b', cmb: 'a*root' }, require: 'c >= 20 && c <= 160',
lhs: '{a}*x + {b}', rhs: '{c}', display: 'Прямые параллельны. Накрест лежащие углы равны: ({a}x + {b})° и {c}°. Найдите x.',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Накрест лежащие углы при параллельных прямых равны.', tex: '{a}x + {b} = {c}' },
{ note: 'Переносим {b} вправо.', tex: '{a}x = {cmb}' },
{ note: 'Делим на {a}.', tex: 'x = {root}' }
]
},
/* биссектриса делит угол пополам */
{
id: 'ang-bisector', topic: 'g-angles', order: 7, subject: 'geometry', grade: 7, kind: 'compute',
title: 'Биссектриса угла',
figure: { type: 'angle-bisector', full: 'a' },
figurePrompt: 'Найдите половину угла (в градусах).',
pick: { half: [15, 80] }, derive: { a: '2*half', val: 'half' },
lhs: 'x', rhs: '{a}/2', display: 'Биссектриса делит угол {a}° пополам. Найдите половину угла.',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Биссектриса делит угол на две равные части.', tex: 'x = {a} / 2' },
{ note: 'Считаем.', tex: 'x = {ans}' }
]
},
/* дополнительные углы (сумма 90°) */
{
id: 'ang-complementary', topic: 'g-angles', order: 8, subject: 'geometry', grade: 7, kind: 'compute',
title: 'Дополнительные углы',
figure: { type: 'complementary', given: 'a' },
figurePrompt: 'Найдите дополнительный угол (в градусах).',
pick: { a: [10, 80] }, derive: { val: '90 - a' },
lhs: 'x', rhs: '90 - {a}', display: 'Два угла дополнительные (в сумме 90°). Один из них равен {a}°. Найдите другой.',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Дополнительные углы в сумме дают 90°.', tex: 'x = 90 - {a}' },
{ note: 'Считаем.', tex: 'x = {ans}' }
]
},
/* второй острый угол прямоугольного треугольника */
{
id: 'ang-right-acute', topic: 'g-angles', order: 9, subject: 'geometry', grade: 7, kind: 'compute',
title: 'Острый угол прям. треугольника',
figure: { type: 'triangle-angles', angA: 'r90', angB: 'a' },
figurePrompt: 'Найдите второй острый угол (в градусах).',
pick: { a: [20, 70] }, derive: { r90: '90', val: '90 - a' },
lhs: 'x', rhs: '90 - {a}', display: 'В прямоугольном треугольнике один острый угол равен {a}°. Найдите другой острый угол.',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Сумма острых углов прямоугольного треугольника равна 90°.', tex: 'x = 90 - {a}' },
{ note: 'Считаем.', tex: 'x = {ans}' }
]
},
/* соседний угол параллелограмма */
{
id: 'ang-parallelogram', topic: 'g-angles', order: 10, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Угол параллелограмма',
figure: { type: 'parallelogram', base: 5, height: 3, markAngle: 'a' },
figurePrompt: 'Найдите соседний угол параллелограмма (в градусах).',
pick: { a: [40, 140] }, derive: { val: '180 - a' },
lhs: 'x', rhs: '180 - {a}', display: 'Один угол параллелограмма равен {a}°. Найдите соседний с ним угол.',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Соседние углы параллелограмма в сумме дают 180°.', tex: 'x = 180 - {a}' },
{ note: 'Считаем.', tex: 'x = {ans}' }
]
},
/* недостающий угол многоугольника */
{
id: 'ang-polygon-missing', topic: 'g-angles', order: 11, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Недостающий угол многоугольника',
figure: { type: 'regular-polygon', n: 'n' },
figurePrompt: 'Найдите оставшийся угол многоугольника (в градусах).',
pick: { n: [4, 8], miss: [60, 150] },
derive: { total: '180*(n - 2)', Sigma: '180*(n - 2) - miss', val: 'miss' }, require: 'Sigma > 0',
lhs: 'x', rhs: '180*({n} - 2) - {Sigma}', display: 'В выпуклом {n}-угольнике сумма всех углов, кроме одного, равна {Sigma}°. Найдите оставшийся угол.',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Сумма всех углов {n}-угольника = 180·({n} 2) = {total}°.', tex: '180*({n} - 2) = {total}' },
{ note: 'Вычитаем сумму известных углов.', tex: 'x = {total} - {Sigma}' },
{ note: 'Считаем.', tex: 'x = {ans}' }
]
},
/* углы треугольника в отношении p:q:r — наибольший */
{
id: 'ang-triangle-ratio', topic: 'g-angles', order: 12, subject: 'geometry', grade: 7, kind: 'compute',
title: 'Углы в отношении',
pick: { p: [1, 4], q: [1, 4], r: [1, 4] }, require: 'mod(180, p + q + r) == 0',
derive: { sum: 'p + q + r', unit: '180/(p + q + r)', mx: 'max(p, max(q, r))', val: '180*max(p, max(q, r))/(p + q + r)' },
lhs: 'x', rhs: '180*max({p}, max({q}, {r}))/({p} + {q} + {r})', display: 'Углы треугольника относятся как {p} : {q} : {r}. Найдите наибольший угол (в градусах).',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Всего частей {p} + {q} + {r} = {sum}. Одна часть = 180 ÷ {sum} = {unit}°.', tex: '' },
{ note: 'Наибольший угол — {mx} частей.', tex: 'x = {unit} * {mx}' },
{ note: 'Считаем.', tex: 'x = {ans}' }
]
},
/* угол между стрелками часов */
{
id: 'ang-clock', topic: 'g-angles', order: 13, subject: 'geometry', grade: 6, kind: 'compute',
title: 'Угол между стрелками часов',
figure: { type: 'clock', hour: 'H' },
figurePrompt: 'Найдите угол между стрелками (в градусах).',
pick: { H: [1, 11] }, derive: { ang0: '30*H', val: '(30*H) > 180 ? 360 - 30*H : 30*H' },
lhs: 'x', rhs: '(30*{H}) > 180 ? 360 - 30*{H} : 30*{H}', display: 'Сколько градусов между часовой и минутной стрелками в {H}:00?',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Циферблат — 360°, каждый час — 30°. В {H}:00 между стрелками {ang0}°.', tex: '' },
{ note: 'Берём меньший угол (не больше 180°).', tex: 'x = {ans}' }
]
} }
]; ];
@@ -2932,6 +3107,9 @@
// V4.1 — Геометрия (углы/Пифагор/площади/многоугольники/подобие/окружность) // V4.1 — Геометрия (углы/Пифагор/площади/многоугольники/подобие/окружность)
'ang-parallel-transversal': 2, 'ang-alternate': 1, 'ang-cointerior': 2, 'ang-parallel-solve': 3, 'ang-parallel-transversal': 2, 'ang-alternate': 1, 'ang-cointerior': 2, 'ang-parallel-solve': 3,
'ang-isosceles-base': 2, 'ang-vertical': 1, 'ang-isosceles-base': 2, 'ang-vertical': 1,
'ang-alt-exterior': 2, 'ang-coint-exterior': 2, 'ang-parallel-twostep': 3, 'ang-alt-solve': 3,
'ang-bisector': 1, 'ang-complementary': 1, 'ang-right-acute': 1, 'ang-parallelogram': 2,
'ang-polygon-missing': 3, 'ang-triangle-ratio': 3, 'ang-clock': 2,
'pyth-perimeter': 3, 'pyth-distance': 3, 'pyth-rect-diagonal': 2, 'pyth-space-diagonal': 3, 'pyth-perimeter': 3, 'pyth-distance': 3, 'pyth-rect-diagonal': 2, 'pyth-space-diagonal': 3,
'area-rect-inverse': 2, 'area-l-shape': 3, 'area-sector': 3, 'area-rect-inverse': 2, 'area-l-shape': 3, 'area-sector': 3,
'poly-diagonals': 2, 'poly-find-n': 3, 'poly-exterior-sum': 2, 'poly-diagonals': 2, 'poly-find-n': 3, 'poly-exterior-sum': 2,