fix(geom7): корневой баг G.angle (метки ∠1=∠2 садились в одну точку) + 2 новых SVG в §5

Корневая причина проблемы с наложенными метками углов в §6:

В G.angle формула центра метки была:
  midA = (a1 + a2) / 2 + (|delta| > π ? π : 0)

При a1≈-153° и a2≈+153° (как у ∠2 в §6) среднее даёт 0° —
ровно туда же, куда ставится метка ∠1 (a1≈+25°, a2≈-25°,
тоже среднее = 0°). Результат: обе метки в одной точке.

Правильная формула — идти от a1 на половину delta в направлении
sweep:
  midA = a1 + delta / 2

Это автоматически разносит метки противоположных секторов
в противоположные стороны. ∠1 уходит вправо, ∠2 — влево.

Также добавил 2 новых SVG в §5:
1. Карточка 5.1 «Что такое угол» — теперь содержит три варианта
   обозначения одного и того же угла: ∠BAC (полное), ∠A (короткое),
   α (греческая буква). Каждый — отдельный SVG с подсветкой угла
   жёлтым сектором, общая подпись внизу.

2. Карточка 5.4 «Биссектриса» — наглядный SVG: ∠BAC = 70°,
   биссектриса AD (пунктирная красная) делит его на две равные
   половинки по 35°. Полупрозрачная заливка зелёным/фиолетовым
   для каждой половины, дуги с одинаковыми штрихами как маркер
   равных углов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-29 08:18:40 +03:00
parent a61b1e3c20
commit 31b40b0e99
2 changed files with 82 additions and 4 deletions
+2 -2
View File
@@ -155,8 +155,8 @@ G.angle = function(V, A, B, opts){
const color = opts.color || '#dc2626';
let s = '<path d="M '+x1+' '+y1+' A '+r+' '+r+' 0 '+large+' '+sweep+' '+x2+' '+y2+'" fill="'+(opts.fill||'none')+'" stroke="'+color+'" stroke-width="'+(opts.width||2)+'"/>';
if(opts.label){
/* Центр подписи — середина биссектрисы */
const midA = (a1 + a2) / 2 + (Math.abs(delta) > Math.PI ? Math.PI : 0);
/* Центр подписи — середина ДУГИ, в направлении sweep */
const midA = a1 + delta / 2;
const lr = r + (opts.labelOffset || 12);
const lx = V.x + lr * Math.cos(midA);
const ly = V.y + lr * Math.sin(midA);
+80 -2
View File
@@ -1018,11 +1018,52 @@ function buildP5(){
+ '<div>'+angleViz(180,'#7c3aed','Развёрн.')+'</div>'
+ '</div>';
/* SVG: обозначение одного угла 3 разными способами */
let svgNotation='';
if(G){
function notationVariant(label, mode){
const b=G.svgBox(180,160,{id:'p5-not-'+mode,cell:20});
const V={x:40,y:130}, A={x:160,y:130};
const B={x:V.x+115*Math.cos(Math.PI/3.5), y:V.y-115*Math.sin(Math.PI/3.5)};
let s=b.open;
/* Заливка-сектор */
const a1=0, a2=-Math.PI/3.5, wR=32;
const x1=V.x+wR, y1=V.y;
const x2=V.x+wR*Math.cos(a2), y2=V.y+wR*Math.sin(a2);
s += '<path d="M '+V.x+' '+V.y+' L '+x1+' '+y1+' A '+wR+' '+wR+' 0 0 0 '+x2+' '+y2+' Z" fill="#f59e0b" opacity="0.20"/>';
s += G.segment(V,A,{color:'#0891b2',width:2.6});
s += G.segment(V,B,{color:'#0891b2',width:2.6});
s += G.angle(V,A,B,{color:'#dc2626',r:22,width:2,label:label,fontSize:14,labelOffset:14});
s += G.point(V.x,V.y,'',{r:4,color:'#0891b2'});
/* Подписи вершин по краям */
if(mode==='full'){
s += '<text x="'+(V.x-12)+'" y="'+(V.y+18)+'" font-size="15" font-weight="800" font-family="Unbounded" fill="#1e293b">A</text>';
s += '<text x="'+(B.x+6)+'" y="'+(B.y-4)+'" font-size="15" font-weight="800" font-family="Unbounded" fill="#1e293b">B</text>';
s += '<text x="'+(A.x+6)+'" y="'+(A.y+6)+'" font-size="15" font-weight="800" font-family="Unbounded" fill="#1e293b">C</text>';
s += '<text x="90" y="152" text-anchor="middle" font-size="13" fill="#7c3aed" font-weight="800" font-family="Unbounded">∠BAC</text>';
} else if(mode==='short'){
s += '<text x="'+(V.x-12)+'" y="'+(V.y+18)+'" font-size="15" font-weight="800" font-family="Unbounded" fill="#1e293b">A</text>';
s += '<text x="90" y="152" text-anchor="middle" font-size="13" fill="#7c3aed" font-weight="800" font-family="Unbounded">∠A</text>';
} else {
s += '<text x="90" y="152" text-anchor="middle" font-size="13" fill="#7c3aed" font-weight="800" font-family="Unbounded">α (альфа)</text>';
}
s += b.close;
return s;
}
svgNotation = '<div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center">'
+ '<div>'+notationVariant('∠BAC','full')+'</div>'
+ '<div>'+notationVariant('∠A','short')+'</div>'
+ '<div>'+notationVariant('α','greek')+'</div>'
+ '</div>';
}
html += makeCard('theory', 'Что такое угол', '5.1', `
<p>Если из точки провести <b>два луча</b>, получится <b>угол</b>.</p>
<p>Эти лучи называются <b>сторонами</b> угла, а их общая точка — <b>вершиной</b>.</p>
<p>Обозначение тремя буквами: $\\angle BAC$ — вершина $A$ записывается <b>посередине</b>. Если по рисунку понятно, какой угол — обозначают одной буквой: $\\angle A$.</p>
<p>Часто углы обозначают малыми греческими буквами: $\\alpha, \\beta, \\gamma, \\varphi$.</p>`);
<p>Часто углы обозначают малыми греческими буквами: $\\alpha, \\beta, \\gamma, \\varphi$.</p>
<p style="font-size:.9rem;color:var(--muted);text-align:center;margin-top:10px"><b>Один и тот же угол можно обозначить тремя способами:</b></p>
`+svgNotation+``);
html += makeCard('rule', 'Измерение углов. Градус', '5.2', `
<p>Углы измеряют в <b>градусах</b>. Развёрнутый угол (стороны на одной прямой) равен <b>$180°$</b>.</p>
@@ -1042,9 +1083,46 @@ function buildP5(){
</ul>
<div class="svg-host">`+svgAngles+`</div>`);
/* SVG: биссектриса угла — наглядно две равные половинки */
let svgBisector='';
if(G){
const b=G.svgBox(280,180,{id:'p5-bis',cell:20});
const V={x:60,y:140};
const A={x:240,y:140};
const fullAngle=Math.PI/3; /* 60° сверху от горизонтали */
const halfAngle=fullAngle/2;
const B={x:V.x+170*Math.cos(fullAngle), y:V.y-170*Math.sin(fullAngle)};
const D={x:V.x+150*Math.cos(halfAngle), y:V.y-150*Math.sin(halfAngle)};
/* Две залитые половинки разными цветами */
function wedgePath(a1,a2,r,fill){
const x1=V.x+r*Math.cos(a1), y1=V.y-r*Math.sin(a1);
const x2=V.x+r*Math.cos(a2), y2=V.y-r*Math.sin(a2);
return '<path d="M '+V.x+' '+V.y+' L '+x1+' '+y1+' A '+r+' '+r+' 0 0 0 '+x2+' '+y2+' Z" fill="'+fill+'" opacity="0.30"/>';
}
svgBisector = b.open
+ wedgePath(0, halfAngle, 42, '#10b981') /* нижняя половинка ∠DAC */
+ wedgePath(halfAngle, fullAngle, 42, '#7c3aed') /* верхняя половинка ∠BAD */
+ G.segment(V,A,{color:'#0891b2',width:2.6})
+ G.segment(V,B,{color:'#0891b2',width:2.6})
+ G.segment(V,D,{color:'#dc2626',width:2.5,dash:'5 3'})
/* Дуги равных половинок (одинаковое число штрихов = равные углы) */
+ '<path d="M '+(V.x+28)+' '+V.y+' A 28 28 0 0 0 '+(V.x+28*Math.cos(halfAngle))+' '+(V.y-28*Math.sin(halfAngle))+'" fill="none" stroke="#dc2626" stroke-width="2"/>'
+ '<path d="M '+(V.x+28*Math.cos(halfAngle))+' '+(V.y-28*Math.sin(halfAngle))+' A 28 28 0 0 0 '+(V.x+28*Math.cos(fullAngle))+' '+(V.y-28*Math.sin(fullAngle))+'" fill="none" stroke="#dc2626" stroke-width="2"/>'
+ '<text x="'+(V.x+40)+'" y="'+(V.y-6)+'" font-size="11" fill="#047857" font-weight="700" font-family="JetBrains Mono">35°</text>'
+ '<text x="'+(V.x+30)+'" y="'+(V.y-32)+'" font-size="11" fill="#6d28d9" font-weight="700" font-family="JetBrains Mono">35°</text>'
+ G.point(V.x,V.y,'',{r:4,color:'#0891b2'})
+ '<text x="'+(V.x-14)+'" y="'+(V.y+18)+'" font-size="14" font-weight="800" font-family="Unbounded" fill="#1e293b">A</text>'
+ '<text x="'+(B.x+6)+'" y="'+(B.y-4)+'" font-size="14" font-weight="800" font-family="Unbounded" fill="#1e293b">B</text>'
+ '<text x="'+(A.x+6)+'" y="'+(A.y+6)+'" font-size="14" font-weight="800" font-family="Unbounded" fill="#1e293b">C</text>'
+ '<text x="'+(D.x+6)+'" y="'+(D.y+4)+'" font-size="14" font-weight="800" font-family="Unbounded" fill="#dc2626">D</text>'
+ '<text x="140" y="170" text-anchor="middle" font-size="11" fill="#475569" font-weight="700" font-family="Unbounded">AD — биссектриса ∠BAC = 70°</text>'
+ b.close;
}
html += makeCard('rule', 'Биссектриса угла', '5.4', `
<p><b>Биссектрисой</b> угла называется луч, исходящий из его вершины и делящий угол на два <b>равных</b> угла.</p>
<p>Если $\\angle BAC = 70°$, а $AD$ — биссектриса, то $\\angle BAD = \\angle DAC = 35°$.</p>`);
<p>Если $\\angle BAC = 70°$, а $AD$ — биссектриса, то $\\angle BAD = \\angle DAC = 35°$.</p>
<div class="svg-host">`+svgBisector+`</div>`);
html += '<div class="wg" id="p5-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Какой угол?</div></div>'