fix: баги штрихов/дуг планиметрии + переработка инструментов треугольника; фаза 10.1 (теорема Фалеса)

- arcmark: рисуется всегда (не только при showAngles=true)
- altitude/median: 1 клик на вершину треугольника (авто-определение)
- centroid/orthocenter: 1 клик внутри/на треугольник
- thales: 3 клика O, A, B → A'B' параллельно AB, коэф. k

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-04-14 11:24:43 +03:00
parent bdb81ba9c8
commit 84dac03e53
2 changed files with 178 additions and 138 deletions
+162 -127
View File
@@ -580,7 +580,7 @@ class GeoSim {
for (const obj of this.eng.byType('circle')) this._drawCircle(ctx, obj);
// Измерения
if (this.showLengths) this._drawLengths(ctx);
if (this.showAngles) this._drawAngleMeasures(ctx);
this._drawAngleMeasures(ctx); // всегда — для arcmark и прямых углов; showAngles управляет авто-подписями
// Точки поверх всего (включая производные)
for (const obj of this.eng.points()) this._drawPoint(ctx, obj);
// Предпросмотр строящегося объекта
@@ -989,6 +989,36 @@ class GeoSim {
return null;
}
/* Найти вершину полигона под курсором: {poly, idx} или null */
_findVertexOfPoly(px, py, nSides = 0) {
const SNAP_PX = 16;
for (const poly of this.eng.byType('polygon')) {
if (nSides > 0 && poly.pointIds.length !== nSides) continue;
for (let i = 0; i < poly.pointIds.length; i++) {
const p = this._p(poly.pointIds[i]);
if (p && Math.hypot(p.x - px, p.y - py) < SNAP_PX) return { poly, idx: i };
}
}
return null;
}
/* Найти треугольник под курсором (вершина, сторона или внутренность) */
_findTriangleNear(px, py) {
for (const poly of this.eng.byType('polygon')) {
if (poly.pointIds.length !== 3) continue;
const pts = poly.pointIds.map(id => this._p(id)).filter(Boolean);
if (pts.length !== 3) continue;
// Вершины
for (const p of pts) { if (Math.hypot(p.x - px, p.y - py) < 20) return poly; }
// Внутренность (cross-product тест)
const sign = (P, A, B) => (B.x-A.x)*(P.y-A.y) - (B.y-A.y)*(P.x-A.x);
const P = {x:px, y:py};
const s0 = sign(P,pts[0],pts[1]), s1 = sign(P,pts[1],pts[2]), s2 = sign(P,pts[2],pts[0]);
if ((s0>=0&&s1>=0&&s2>=0) || (s0<=0&&s1<=0&&s2<=0)) return poly;
}
return null;
}
/* Найти или создать виртуальный отрезок для стороны полигона */
_ensurePolySide(polyId, p1Id, p2Id) {
for (const obj of this.eng.byType('segment')) {
@@ -1143,45 +1173,46 @@ class GeoSim {
ctx.strokeStyle = col;
ctx.lineWidth = 1.5;
const explicitMark = poly.angleMarks?.[i] || 0; // 0=auto, 1-3=явный
const explicitMark = poly.angleMarks?.[i] || 0; // 0=авто, 1-3=явный
if (explicitMark > 0 && bisLen > 1e-6) {
// Явные метки: 1-3 концентрические дуги
// Явные метки: всегда рисуем 1-3 концентрические дуги
const midAngle = Math.atan2(bisY, bisX);
const halfSpread = Math.acos(Math.max(-1, Math.min(1, uAx*uCx + uAy*uCy))) / 2;
ctx.globalAlpha = 0.7;
ctx.globalAlpha = 0.85;
ctx.lineWidth = 2;
for (let k = 0; k < explicitMark; k++) {
ctx.beginPath();
ctx.arc(Bpx.x, Bpx.y, ARC_R + k * 7, midAngle - halfSpread, midAngle + halfSpread, false);
ctx.arc(Bpx.x, Bpx.y, ARC_R + k * 8, midAngle - halfSpread, midAngle + halfSpread, false);
ctx.stroke();
}
} else if (Math.abs(angle - 90) < 2) {
// Прямой угол — маленький квадрат
ctx.globalAlpha = 0.8;
ctx.beginPath();
ctx.moveTo(Bpx.x + uAx*SQ_SZ, Bpx.y + uAy*SQ_SZ);
ctx.lineTo(Bpx.x + uAx*SQ_SZ + uCx*SQ_SZ, Bpx.y + uAy*SQ_SZ + uCy*SQ_SZ);
ctx.lineTo(Bpx.x + uCx*SQ_SZ, Bpx.y + uCy*SQ_SZ);
ctx.stroke();
} else if (bisLen > 1e-6) {
// Дуга угла — от midAngle-halfSpread до midAngle+halfSpread через биссектрису
const midAngle = Math.atan2(bisY, bisX);
const halfSpread = Math.acos(Math.max(-1, Math.min(1, uAx*uCx + uAy*uCy))) / 2;
ctx.globalAlpha = 0.7;
ctx.beginPath();
ctx.arc(Bpx.x, Bpx.y, ARC_R, midAngle - halfSpread, midAngle + halfSpread, false);
ctx.stroke();
}
// Подпись угла вдоль биссектрисы
if (bisLen > 1e-6) {
const ldist = (explicitMark > 0 ? ARC_R + (explicitMark-1)*7 : Math.abs(angle - 90) < 2 ? SQ_SZ : ARC_R) + LBL_D;
const bx = bisX / bisLen, by = bisY / bisLen;
ctx.font = '10px Manrope,sans-serif';
ctx.fillStyle = col;
ctx.globalAlpha = 0.9;
ctx.textAlign = 'center';
ctx.shadowColor = 'rgba(0,0,0,0.9)'; ctx.shadowBlur = 3;
ctx.fillText(angle.toFixed(1) + '°', Bpx.x + bx*ldist, Bpx.y + by*ldist + 3);
} else if (this.showAngles) {
// Авто-режим: только если включено
if (Math.abs(angle - 90) < 2) {
ctx.globalAlpha = 0.8;
ctx.beginPath();
ctx.moveTo(Bpx.x + uAx*SQ_SZ, Bpx.y + uAy*SQ_SZ);
ctx.lineTo(Bpx.x + uAx*SQ_SZ + uCx*SQ_SZ, Bpx.y + uAy*SQ_SZ + uCy*SQ_SZ);
ctx.lineTo(Bpx.x + uCx*SQ_SZ, Bpx.y + uCy*SQ_SZ);
ctx.stroke();
} else if (bisLen > 1e-6) {
const midAngle = Math.atan2(bisY, bisX);
const halfSpread = Math.acos(Math.max(-1, Math.min(1, uAx*uCx + uAy*uCy))) / 2;
ctx.globalAlpha = 0.7;
ctx.beginPath();
ctx.arc(Bpx.x, Bpx.y, ARC_R, midAngle - halfSpread, midAngle + halfSpread, false);
ctx.stroke();
}
// Подпись угла
if (bisLen > 1e-6) {
const ldist = (Math.abs(angle - 90) < 2 ? SQ_SZ : ARC_R) + LBL_D;
const bx = bisX / bisLen, by = bisY / bisLen;
ctx.font = '10px Manrope,sans-serif';
ctx.fillStyle = col;
ctx.globalAlpha = 0.9;
ctx.textAlign = 'center';
ctx.shadowColor = 'rgba(0,0,0,0.9)'; ctx.shadowBlur = 3;
ctx.fillText(angle.toFixed(1) + '°', Bpx.x + bx*ldist, Bpx.y + by*ldist + 3);
}
}
ctx.restore();
@@ -2009,135 +2040,96 @@ class GeoSim {
/* ══ Phase 7: altitude, median, centroid, orthocenter ══ */
case 'altitude': {
// Шаг 1: выбрать прямую (сторону), Шаг 2: выбрать вершину
if (!this._pendingLineRef) {
const hit = this._hitTestLine(px, py);
if (hit) {
this._pendingLineRef = hit;
if (this.onHintChange) this.onHintChange('altitude', 2);
}
} else {
// 1 клик на вершину треугольника → высота из этой вершины на противоположную сторону
const hitAlt = this._findVertexOfPoly(px, py, 3);
if (hitAlt) {
this._pushUndo();
const vertex = this._ensurePoint(snapped);
const sl = this._pendingLineRef;
let L1, L2;
if (sl.type === 'derived_line') {
L1 = { x: sl.ptX, y: sl.ptY }; L2 = { x: sl.ptX+sl.dirX, y: sl.ptY+sl.dirY };
} else {
L1 = this._mpt(sl.p1Id); L2 = this._mpt(sl.p2Id);
}
if (L1 && L2) {
const f = gFoot({ x: vertex.x, y: vertex.y }, L1, L2);
const nFoot = this.eng.byType('point').filter(p=>p.constr==='foot').length;
const foot = this.eng.add({
type: 'point', derived: true, constr: 'foot',
srcLine: sl.id, srcPt: vertex.id,
x: f.x, y: f.y,
label: 'H' + (nFoot ? String.fromCharCode(0x2081 + nFoot) : '\u2081'),
style: { color: '#4ADE80', size: 4 }
});
// Отрезок-высота: вершина → основание
this.eng.add({ type: 'segment', p1Id: vertex.id, p2Id: foot.id,
style: { color: '#4ADE80', width: 1.5 } });
}
this._pendingLineRef = null; this._pending = []; this._preview = null;
const { poly: polyAlt, idx: iA } = hitAlt;
const ids = polyAlt.pointIds;
const iB = (iA+1)%3, iC = (iA+2)%3;
const ptA = this.eng.get(ids[iA]), ptB = this.eng.get(ids[iB]), ptC = this.eng.get(ids[iC]);
const A = {x:ptA.x,y:ptA.y}, B = {x:ptB.x,y:ptB.y}, C = {x:ptC.x,y:ptC.y};
const f = gFoot(A, B, C);
const nF = this.eng.byType('point').filter(p=>p.constr==='altitude_foot').length;
const foot = this.eng.add({ type:'point', derived:true, constr:'altitude_foot',
ptA:ids[iA], ptB:ids[iB], ptC:ids[iC], x:f.x, y:f.y,
label:'H'+(nF ? String.fromCharCode(0x2080+nF+1) : '\u2081'),
style:{color:'#4ADE80', size:4} });
this.eng.add({ type:'segment', p1Id:ids[iA], p2Id:foot.id,
style:{color:'#4ADE80', width:1.5} });
if (this.onUpdate) this.onUpdate(this.getStats());
}
break;
}
case 'median': {
// 3 клика: вершина A, затем B и C (концы стороны) → середина M + отрезок AM
this._pending.push(snapped);
if (this._pending.length === 1) {
if (this.onHintChange) this.onHintChange('median', 2);
} else if (this._pending.length === 2) {
if (this.onHintChange) this.onHintChange('median', 3);
} else if (this._pending.length === 3) {
// 1 клик на вершину треугольника → медиана из этой вершины к середине противоположной стороны
const hitMed = this._findVertexOfPoly(px, py, 3);
if (hitMed) {
this._pushUndo();
const ptA = this._ensurePoint(this._pending[0]);
const ptB = this._ensurePoint(this._pending[1]);
const ptC = this._ensurePoint(this._pending[2]);
const { poly: polyMed, idx: iA } = hitMed;
const ids = polyMed.pointIds;
const iB = (iA+1)%3, iC = (iA+2)%3;
const ptB = this.eng.get(ids[iB]), ptC = this.eng.get(ids[iC]);
const nMid = this.eng.byType('point').filter(p=>p.constr==='midpoint').length;
const mid = this.eng.add({
type: 'point', derived: true, constr: 'midpoint',
srcA: ptB.id, srcB: ptC.id,
x: (ptB.x+ptC.x)/2, y: (ptB.y+ptC.y)/2,
label: 'M' + (nMid+1), style: { color: '#22d55e', size: 4 }
});
this.eng.add({ type: 'segment', p1Id: ptA.id, p2Id: mid.id,
style: { color: '#22d55e', width: 1.5 } });
this._pending = []; this._preview = null;
const mid = this.eng.add({ type:'point', derived:true, constr:'midpoint',
srcA:ids[iB], srcB:ids[iC],
x:(ptB.x+ptC.x)/2, y:(ptB.y+ptC.y)/2,
label:'M'+(nMid+1), style:{color:'#22d55e', size:4} });
this.eng.add({ type:'segment', p1Id:ids[iA], p2Id:mid.id,
style:{color:'#22d55e', width:1.5} });
if (this.onUpdate) this.onUpdate(this.getStats());
}
break;
}
case 'centroid': {
// 3 клика: A, B, C → 3 медианы + точка центроид G
this._pending.push(snapped);
if (this._pending.length === 1) {
if (this.onHintChange) this.onHintChange('centroid', 2);
} else if (this._pending.length === 2) {
if (this.onHintChange) this.onHintChange('centroid', 3);
} else if (this._pending.length === 3) {
// 1 клик на/внутри треугольника → 3 медианы + центроид G
const polyCen = this._findTriangleNear(px, py);
if (polyCen) {
this._pushUndo();
const pA = this._ensurePoint(this._pending[0]);
const pB = this._ensurePoint(this._pending[1]);
const pC = this._ensurePoint(this._pending[2]);
const nG = this.eng.byType('point').filter(p=>p.constr==='centroid').length;
const [idA, idB, idC] = polyCen.pointIds;
const pA = this.eng.get(idA), pB = this.eng.get(idB), pC = this.eng.get(idC);
const nG = this.eng.byType('point').filter(p=>p.constr==='centroid').length;
const col = '#A78BFA';
// 3 середины сторон
const mBC = this.eng.add({ type:'point', derived:true, constr:'midpoint', srcA:pB.id, srcB:pC.id, x:(pB.x+pC.x)/2, y:(pB.y+pC.y)/2, label:'M', style:{color:col,size:3} });
const mAC = this.eng.add({ type:'point', derived:true, constr:'midpoint', srcA:pA.id, srcB:pC.id, x:(pA.x+pC.x)/2, y:(pA.y+pC.y)/2, label:'M', style:{color:col,size:3} });
const mAB = this.eng.add({ type:'point', derived:true, constr:'midpoint', srcA:pA.id, srcB:pB.id, x:(pA.x+pB.x)/2, y:(pA.y+pB.y)/2, label:'M₃', style:{color:col,size:3} });
// 3 медианы
this.eng.add({ type:'segment', p1Id:pA.id, p2Id:mBC.id, style:{color:col, width:1.5} });
this.eng.add({ type:'segment', p1Id:pB.id, p2Id:mAC.id, style:{color:col, width:1.5} });
this.eng.add({ type:'segment', p1Id:pC.id, p2Id:mAB.id, style:{color:col, width:1.5} });
// Центроид
const mBC = this.eng.add({ type:'point', derived:true, constr:'midpoint', srcA:idB, srcB:idC, x:(pB.x+pC.x)/2, y:(pB.y+pC.y)/2, label:'M₁', style:{color:col,size:3} });
const mAC = this.eng.add({ type:'point', derived:true, constr:'midpoint', srcA:idA, srcB:idC, x:(pA.x+pC.x)/2, y:(pA.y+pC.y)/2, label:'M', style:{color:col,size:3} });
const mAB = this.eng.add({ type:'point', derived:true, constr:'midpoint', srcA:idA, srcB:idB, x:(pA.x+pB.x)/2, y:(pA.y+pB.y)/2, label:'M', style:{color:col,size:3} });
this.eng.add({ type:'segment', p1Id:idA, p2Id:mBC.id, style:{color:col, width:1.5} });
this.eng.add({ type:'segment', p1Id:idB, p2Id:mAC.id, style:{color:col, width:1.5} });
this.eng.add({ type:'segment', p1Id:idC, p2Id:mAB.id, style:{color:col, width:1.5} });
this.eng.add({ type:'point', derived:true, constr:'centroid',
ptA:pA.id, ptB:pB.id, ptC:pC.id,
ptA:idA, ptB:idB, ptC:idC,
x:(pA.x+pB.x+pC.x)/3, y:(pA.y+pB.y+pC.y)/3,
label: nG ? 'G'+(nG+1) : 'G', style:{color:col, size:6} });
this._pending = []; this._preview = null;
if (this.onUpdate) this.onUpdate(this.getStats());
}
break;
}
case 'orthocenter': {
// 3 клика: A, B, C → 3 высоты + точка ортоцентра H
this._pending.push(snapped);
if (this._pending.length === 1) {
if (this.onHintChange) this.onHintChange('orthocenter', 2);
} else if (this._pending.length === 2) {
if (this.onHintChange) this.onHintChange('orthocenter', 3);
} else if (this._pending.length === 3) {
// 1 клик на/внутри треугольника → 3 высоты + ортоцентр H
const polyOrt = this._findTriangleNear(px, py);
if (polyOrt) {
this._pushUndo();
const pA = this._ensurePoint(this._pending[0]);
const pB = this._ensurePoint(this._pending[1]);
const pC = this._ensurePoint(this._pending[2]);
const nH = this.eng.byType('point').filter(p=>p.constr==='orthocenter').length;
const [idA, idB, idC] = polyOrt.pointIds;
const pA = this.eng.get(idA), pB = this.eng.get(idB), pC = this.eng.get(idC);
const nH = this.eng.byType('point').filter(p=>p.constr==='orthocenter').length;
const col = '#F97316';
const A = {x:pA.x,y:pA.y}, B = {x:pB.x,y:pB.y}, C = {x:pC.x,y:pC.y};
// Основания высот
const Ha = this.eng.add({ type:'point', derived:true, constr:'altitude_foot', ptA:pA.id, ptB:pB.id, ptC:pC.id, ...gFoot(A,B,C), label:'H_a', style:{color:col,size:3} });
const Hb = this.eng.add({ type:'point', derived:true, constr:'altitude_foot', ptA:pB.id, ptB:pA.id, ptC:pC.id, ...gFoot(B,A,C), label:'H_b', style:{color:col,size:3} });
const Hc = this.eng.add({ type:'point', derived:true, constr:'altitude_foot', ptA:pC.id, ptB:pA.id, ptC:pB.id, ...gFoot(C,A,B), label:'H_c', style:{color:col,size:3} });
// Отрезки-высоты
this.eng.add({ type:'segment', p1Id:pA.id, p2Id:Ha.id, style:{color:col, width:1.5} });
this.eng.add({ type:'segment', p1Id:pB.id, p2Id:Hb.id, style:{color:col, width:1.5} });
this.eng.add({ type:'segment', p1Id:pC.id, p2Id:Hc.id, style:{color:col, width:1.5} });
// Ортоцентр
const Ha = this.eng.add({ type:'point', derived:true, constr:'altitude_foot', ptA:idA, ptB:idB, ptC:idC, ...gFoot(A,B,C), label:'H_a', style:{color:col,size:3} });
const Hb = this.eng.add({ type:'point', derived:true, constr:'altitude_foot', ptA:idB, ptB:idA, ptC:idC, ...gFoot(B,A,C), label:'H_b', style:{color:col,size:3} });
const Hc = this.eng.add({ type:'point', derived:true, constr:'altitude_foot', ptA:idC, ptB:idA, ptC:idB, ...gFoot(C,A,B), label:'H_c', style:{color:col,size:3} });
this.eng.add({ type:'segment', p1Id:idA, p2Id:Ha.id, style:{color:col, width:1.5} });
this.eng.add({ type:'segment', p1Id:idB, p2Id:Hb.id, style:{color:col, width:1.5} });
this.eng.add({ type:'segment', p1Id:idC, p2Id:Hc.id, style:{color:col, width:1.5} });
const orth = gOrthocenter(A, B, C);
if (orth) {
this.eng.add({ type:'point', derived:true, constr:'orthocenter',
ptA:pA.id, ptB:pB.id, ptC:pC.id,
ptA:idA, ptB:idB, ptC:idC,
x:orth.x, y:orth.y, valid:true,
label: nH ? 'H'+(nH+1) : 'H', style:{color:col, size:6} });
}
this._pending = []; this._preview = null;
if (this.onUpdate) this.onUpdate(this.getStats());
}
break;
@@ -2257,6 +2249,49 @@ class GeoSim {
break;
}
/* ══ Phase 10.1: Теорема Фалеса ══ */
case 'thales': {
// 3 клика: O (центр), A (точка на луче 1), B (точка на луче 2)
// Строит A' = O + k*(A-O), B' = O + k*(B-O), AB ∥ A'B'
this._pending.push(snapped);
if (this._pending.length === 1) {
if (this.onHintChange) this.onHintChange('thales', 2);
} else if (this._pending.length === 2) {
if (this.onHintChange) this.onHintChange('thales', 3);
} else if (this._pending.length === 3) {
this._pushUndo();
const pO = this._ensurePoint(this._pending[0]);
const pA = this._ensurePoint(this._pending[1]);
const pB = this._ensurePoint(this._pending[2]);
const k = this._scaleK;
const col = '#06D6E0';
// A' и B' — производные точки (constr:'scale')
const pA2 = this.eng.add({ type:'point', derived:true, constr:'scale',
srcO:pO.id, srcPt:pA.id, k,
x: pO.x + k*(pA.x-pO.x), y: pO.y + k*(pA.y-pO.y),
label:"A'", style:{color:col, size:4} });
const pB2 = this.eng.add({ type:'point', derived:true, constr:'scale',
srcO:pO.id, srcPt:pB.id, k,
x: pO.x + k*(pB.x-pO.x), y: pO.y + k*(pB.y-pO.y),
label:"B'", style:{color:col, size:4} });
// Лучи O→A'→... (через A и A')
this.eng.add({ type:'segment', p1Id:pO.id, p2Id:pA2.id,
style:{color:'#9CA3AF', width:1, dash:[5,4]} });
this.eng.add({ type:'segment', p1Id:pO.id, p2Id:pB2.id,
style:{color:'#9CA3AF', width:1, dash:[5,4]} });
// Отрезок AB (ближняя параллель)
this.eng.add({ type:'segment', p1Id:pA.id, p2Id:pB.id,
style:{color:'#FFE066', width:2} });
// Отрезок A'B' (дальняя параллель)
this.eng.add({ type:'segment', p1Id:pA2.id, p2Id:pB2.id,
style:{color:col, width:2} });
this._pending = []; this._preview = null;
if (this.onUpdate) this.onUpdate(this.getStats());
}
break;
}
/* ══ Phase 10.2: Подобие (масштаб) ══ */
case 'scale': {
+16 -11
View File
@@ -3963,6 +3963,15 @@
</div>
</div>
<!-- Thales theorem -->
<div class="gp-section-title" style="margin-top:4px">Теорема Фалеса</div>
<div class="geo-tool-grid">
<button id="geo-btn-thales" class="geo-tool-btn" onclick="geoSetTool('thales',this)" title="Теорема Фалеса — клик O, затем A, затем B → A&#39;B&#39; ∥ AB">
<svg viewBox="0 0 24 24" fill="none"><circle cx="4" cy="20" r="2" fill="currentColor"/><line x1="4" y1="20" x2="22" y2="4" stroke-width="1.5"/><line x1="4" y1="20" x2="22" y2="12" stroke-width="1.5"/><line x1="10" y1="15" x2="13" y2="12" stroke-width="2" stroke-dasharray="0"/><line x1="17" y1="9" x2="20" y2="7" stroke-width="2" opacity=".6"/></svg>
Фалес
</button>
</div>
<!-- Mark tools -->
<div class="gp-section-title" style="margin-top:4px">Метки</div>
<div class="geo-tool-grid">
@@ -5481,10 +5490,11 @@
tick: 'Кликни на отрезок или сторону — добавить штрих (1–3; ещё раз — убрать)',
arcmark: 'Кликни на вершину полигона — добавить дугу (1–3; ещё раз — убрать)',
parallelmark: 'Кликни на отрезок или сторону — добавить метку параллельности (1–2; ещё раз — убрать)',
altitude: 'Кликни на сторону треугольника (или прямую)',
median: 'Кликни вершину A треугольника',
centroid: 'Кликни первую вершину треугольника',
orthocenter: 'Кликни первую вершину треугольника',
altitude: 'Кликни на вершину треугольника — построим высоту из неё',
median: 'Кликни на вершину треугольника — построим медиану из неё',
centroid: 'Кликни на треугольник или внутри него — построим все 3 медианы и центроид G',
orthocenter: 'Кликни на треугольник или внутри него — построим все 3 высоты и ортоцентр H',
thales: 'Кликни центр подобия O (начало лучей)',
midline: 'Кликни вершину A треугольника',
parallelogram:'Кликни вершину A параллелограмма',
diagonal: 'Кликни внутри четырёхугольника — построим диагонали',
@@ -5508,18 +5518,13 @@
tangent_2: 'Теперь кликни на внешнюю точку — получишь две касательные',
translate_2: 'Теперь кликни конец вектора B',
translate_3: 'Теперь кликни точку P — она будет перенесена',
altitude_2: 'Теперь кликни вершину — опустим из неё высоту',
median_2: 'Теперь кликни вершину B (один конец основания)',
median_3: 'Теперь кликни вершину C (второй конец основания)',
centroid_2: 'Кликни вершину B',
centroid_3: 'Кликни вершину C — построим центроид',
orthocenter_2: 'Кликни вершину B',
orthocenter_3: 'Кликни вершину C — построим ортоцентр',
midline_2: 'Кликни вершину B (конец первой стороны)',
midline_3: 'Кликни вершину C (конец второй стороны) — построим среднюю линию',
parallelogram_2: 'Кликни вершину B (смежная с A)',
parallelogram_3: 'Кликни вершину C — построим параллелограмм ABCD',
scale_2: 'Кликни точку P — построим P\' = O + k·(P O)',
thales_2: 'Кликни точку A (на первом луче)',
thales_3: 'Кликни точку B (на втором луче) — построим A\'B\' ∥ AB',
};
function _geoShowHint(name, phase) {