From fa034fee7c61261e2a3a3e2069e289c999cc92ae Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Mon, 29 Jun 2026 16:26:42 +0300 Subject: [PATCH] =?UTF-8?q?feat(trainer):=20=D1=82=D0=B5=D0=BC=D0=B0=20?= =?UTF-8?q?=C2=AB=D0=A3=D0=B3=D0=BB=D1=8B=C2=BB=20=E2=80=94=20+11=20=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=BE=D0=B2=20?= =?UTF-8?q?+=203=20=D1=84=D0=B8=D0=B3=D1=83=D1=80=D1=8B=20(=D0=BC=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC=D1=83=D0=BC=20=D1=80=D0=B0=D0=B7=D0=BD?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=D0=B8=D1=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализован раздел 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) --- frontend/js/trainer/figures.js | 60 ++++++++++ frontend/js/trainer/generators.js | 178 ++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) diff --git a/frontend/js/trainer/figures.js b/frontend/js/trainer/figures.js index 91b40b4..e5412fd 100644 --- a/frontend/js/trainer/figures.js +++ b/frontend/js/trainer/figures.js @@ -357,6 +357,14 @@ 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]); + 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) var Fs = f.px(P(skew, 0)); 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((Tang.x + Pt.x) / 2 + 4, (Tang.y + Pt.y) / 2 - 8), '?', { fill: UNK, size: 14, weight: 800, anchor: 'start' }); 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; } }; diff --git a/frontend/js/trainer/generators.js b/frontend/js/trainer/generators.js index d8654f2..0e5551f 100644 --- a/frontend/js/trainer/generators.js +++ b/frontend/js/trainer/generators.js @@ -2861,6 +2861,181 @@ { note: 'Длина касательной = √(OP² − r²).', tex: 'x = sqrt({op}^2 - {r}^2)' }, { 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 — Геометрия (углы/Пифагор/площади/многоугольники/подобие/окружность) 'ang-parallel-transversal': 2, 'ang-alternate': 1, 'ang-cointerior': 2, 'ang-parallel-solve': 3, '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, 'area-rect-inverse': 2, 'area-l-shape': 3, 'area-sector': 3, 'poly-diagonals': 2, 'poly-find-n': 3, 'poly-exterior-sum': 2,