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:
+162
-127
@@ -580,7 +580,7 @@ class GeoSim {
|
|||||||
for (const obj of this.eng.byType('circle')) this._drawCircle(ctx, obj);
|
for (const obj of this.eng.byType('circle')) this._drawCircle(ctx, obj);
|
||||||
// Измерения
|
// Измерения
|
||||||
if (this.showLengths) this._drawLengths(ctx);
|
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);
|
for (const obj of this.eng.points()) this._drawPoint(ctx, obj);
|
||||||
// Предпросмотр строящегося объекта
|
// Предпросмотр строящегося объекта
|
||||||
@@ -989,6 +989,36 @@ class GeoSim {
|
|||||||
return null;
|
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) {
|
_ensurePolySide(polyId, p1Id, p2Id) {
|
||||||
for (const obj of this.eng.byType('segment')) {
|
for (const obj of this.eng.byType('segment')) {
|
||||||
@@ -1143,45 +1173,46 @@ class GeoSim {
|
|||||||
ctx.strokeStyle = col;
|
ctx.strokeStyle = col;
|
||||||
ctx.lineWidth = 1.5;
|
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) {
|
if (explicitMark > 0 && bisLen > 1e-6) {
|
||||||
// Явные метки: 1-3 концентрические дуги
|
// Явные метки: всегда рисуем 1-3 концентрические дуги
|
||||||
const midAngle = Math.atan2(bisY, bisX);
|
const midAngle = Math.atan2(bisY, bisX);
|
||||||
const halfSpread = Math.acos(Math.max(-1, Math.min(1, uAx*uCx + uAy*uCy))) / 2;
|
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++) {
|
for (let k = 0; k < explicitMark; k++) {
|
||||||
ctx.beginPath();
|
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();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
} else if (Math.abs(angle - 90) < 2) {
|
} else if (this.showAngles) {
|
||||||
// Прямой угол — маленький квадрат
|
// Авто-режим: только если включено
|
||||||
ctx.globalAlpha = 0.8;
|
if (Math.abs(angle - 90) < 2) {
|
||||||
ctx.beginPath();
|
ctx.globalAlpha = 0.8;
|
||||||
ctx.moveTo(Bpx.x + uAx*SQ_SZ, Bpx.y + uAy*SQ_SZ);
|
ctx.beginPath();
|
||||||
ctx.lineTo(Bpx.x + uAx*SQ_SZ + uCx*SQ_SZ, Bpx.y + uAy*SQ_SZ + uCy*SQ_SZ);
|
ctx.moveTo(Bpx.x + uAx*SQ_SZ, Bpx.y + uAy*SQ_SZ);
|
||||||
ctx.lineTo(Bpx.x + uCx*SQ_SZ, Bpx.y + uCy*SQ_SZ);
|
ctx.lineTo(Bpx.x + uAx*SQ_SZ + uCx*SQ_SZ, Bpx.y + uAy*SQ_SZ + uCy*SQ_SZ);
|
||||||
ctx.stroke();
|
ctx.lineTo(Bpx.x + uCx*SQ_SZ, Bpx.y + uCy*SQ_SZ);
|
||||||
} else if (bisLen > 1e-6) {
|
ctx.stroke();
|
||||||
// Дуга угла — от midAngle-halfSpread до midAngle+halfSpread через биссектрису
|
} else if (bisLen > 1e-6) {
|
||||||
const midAngle = Math.atan2(bisY, bisX);
|
const midAngle = Math.atan2(bisY, bisX);
|
||||||
const halfSpread = Math.acos(Math.max(-1, Math.min(1, uAx*uCx + uAy*uCy))) / 2;
|
const halfSpread = Math.acos(Math.max(-1, Math.min(1, uAx*uCx + uAy*uCy))) / 2;
|
||||||
ctx.globalAlpha = 0.7;
|
ctx.globalAlpha = 0.7;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(Bpx.x, Bpx.y, ARC_R, midAngle - halfSpread, midAngle + halfSpread, false);
|
ctx.arc(Bpx.x, Bpx.y, ARC_R, midAngle - halfSpread, midAngle + halfSpread, false);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
// Подпись угла
|
||||||
// Подпись угла вдоль биссектрисы
|
if (bisLen > 1e-6) {
|
||||||
if (bisLen > 1e-6) {
|
const ldist = (Math.abs(angle - 90) < 2 ? SQ_SZ : ARC_R) + LBL_D;
|
||||||
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;
|
||||||
const bx = bisX / bisLen, by = bisY / bisLen;
|
ctx.font = '10px Manrope,sans-serif';
|
||||||
ctx.font = '10px Manrope,sans-serif';
|
ctx.fillStyle = col;
|
||||||
ctx.fillStyle = col;
|
ctx.globalAlpha = 0.9;
|
||||||
ctx.globalAlpha = 0.9;
|
ctx.textAlign = 'center';
|
||||||
ctx.textAlign = 'center';
|
ctx.shadowColor = 'rgba(0,0,0,0.9)'; ctx.shadowBlur = 3;
|
||||||
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.fillText(angle.toFixed(1) + '°', Bpx.x + bx*ldist, Bpx.y + by*ldist + 3);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -2009,135 +2040,96 @@ class GeoSim {
|
|||||||
/* ══ Phase 7: altitude, median, centroid, orthocenter ══ */
|
/* ══ Phase 7: altitude, median, centroid, orthocenter ══ */
|
||||||
|
|
||||||
case 'altitude': {
|
case 'altitude': {
|
||||||
// Шаг 1: выбрать прямую (сторону), Шаг 2: выбрать вершину
|
// 1 клик на вершину треугольника → высота из этой вершины на противоположную сторону
|
||||||
if (!this._pendingLineRef) {
|
const hitAlt = this._findVertexOfPoly(px, py, 3);
|
||||||
const hit = this._hitTestLine(px, py);
|
if (hitAlt) {
|
||||||
if (hit) {
|
|
||||||
this._pendingLineRef = hit;
|
|
||||||
if (this.onHintChange) this.onHintChange('altitude', 2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._pushUndo();
|
this._pushUndo();
|
||||||
const vertex = this._ensurePoint(snapped);
|
const { poly: polyAlt, idx: iA } = hitAlt;
|
||||||
const sl = this._pendingLineRef;
|
const ids = polyAlt.pointIds;
|
||||||
let L1, L2;
|
const iB = (iA+1)%3, iC = (iA+2)%3;
|
||||||
if (sl.type === 'derived_line') {
|
const ptA = this.eng.get(ids[iA]), ptB = this.eng.get(ids[iB]), ptC = this.eng.get(ids[iC]);
|
||||||
L1 = { x: sl.ptX, y: sl.ptY }; L2 = { x: sl.ptX+sl.dirX, y: sl.ptY+sl.dirY };
|
const A = {x:ptA.x,y:ptA.y}, B = {x:ptB.x,y:ptB.y}, C = {x:ptC.x,y:ptC.y};
|
||||||
} else {
|
const f = gFoot(A, B, C);
|
||||||
L1 = this._mpt(sl.p1Id); L2 = this._mpt(sl.p2Id);
|
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',
|
||||||
if (L1 && L2) {
|
ptA:ids[iA], ptB:ids[iB], ptC:ids[iC], x:f.x, y:f.y,
|
||||||
const f = gFoot({ x: vertex.x, y: vertex.y }, L1, L2);
|
label:'H'+(nF ? String.fromCharCode(0x2080+nF+1) : '\u2081'),
|
||||||
const nFoot = this.eng.byType('point').filter(p=>p.constr==='foot').length;
|
style:{color:'#4ADE80', size:4} });
|
||||||
const foot = this.eng.add({
|
this.eng.add({ type:'segment', p1Id:ids[iA], p2Id:foot.id,
|
||||||
type: 'point', derived: true, constr: 'foot',
|
style:{color:'#4ADE80', width:1.5} });
|
||||||
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;
|
|
||||||
if (this.onUpdate) this.onUpdate(this.getStats());
|
if (this.onUpdate) this.onUpdate(this.getStats());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'median': {
|
case 'median': {
|
||||||
// 3 клика: вершина A, затем B и C (концы стороны) → середина M + отрезок AM
|
// 1 клик на вершину треугольника → медиана из этой вершины к середине противоположной стороны
|
||||||
this._pending.push(snapped);
|
const hitMed = this._findVertexOfPoly(px, py, 3);
|
||||||
if (this._pending.length === 1) {
|
if (hitMed) {
|
||||||
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) {
|
|
||||||
this._pushUndo();
|
this._pushUndo();
|
||||||
const ptA = this._ensurePoint(this._pending[0]);
|
const { poly: polyMed, idx: iA } = hitMed;
|
||||||
const ptB = this._ensurePoint(this._pending[1]);
|
const ids = polyMed.pointIds;
|
||||||
const ptC = this._ensurePoint(this._pending[2]);
|
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 nMid = this.eng.byType('point').filter(p=>p.constr==='midpoint').length;
|
||||||
const mid = this.eng.add({
|
const mid = this.eng.add({ type:'point', derived:true, constr:'midpoint',
|
||||||
type: 'point', derived: true, constr: 'midpoint',
|
srcA:ids[iB], srcB:ids[iC],
|
||||||
srcA: ptB.id, srcB: ptC.id,
|
x:(ptB.x+ptC.x)/2, y:(ptB.y+ptC.y)/2,
|
||||||
x: (ptB.x+ptC.x)/2, y: (ptB.y+ptC.y)/2,
|
label:'M'+(nMid+1), style:{color:'#22d55e', size:4} });
|
||||||
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} });
|
||||||
this.eng.add({ type: 'segment', p1Id: ptA.id, p2Id: mid.id,
|
|
||||||
style: { color: '#22d55e', width: 1.5 } });
|
|
||||||
this._pending = []; this._preview = null;
|
|
||||||
if (this.onUpdate) this.onUpdate(this.getStats());
|
if (this.onUpdate) this.onUpdate(this.getStats());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'centroid': {
|
case 'centroid': {
|
||||||
// 3 клика: A, B, C → 3 медианы + точка центроид G
|
// 1 клик на/внутри треугольника → 3 медианы + центроид G
|
||||||
this._pending.push(snapped);
|
const polyCen = this._findTriangleNear(px, py);
|
||||||
if (this._pending.length === 1) {
|
if (polyCen) {
|
||||||
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) {
|
|
||||||
this._pushUndo();
|
this._pushUndo();
|
||||||
const pA = this._ensurePoint(this._pending[0]);
|
const [idA, idB, idC] = polyCen.pointIds;
|
||||||
const pB = this._ensurePoint(this._pending[1]);
|
const pA = this.eng.get(idA), pB = this.eng.get(idB), pC = this.eng.get(idC);
|
||||||
const pC = this._ensurePoint(this._pending[2]);
|
const nG = this.eng.byType('point').filter(p=>p.constr==='centroid').length;
|
||||||
const nG = this.eng.byType('point').filter(p=>p.constr==='centroid').length;
|
|
||||||
const col = '#A78BFA';
|
const col = '#A78BFA';
|
||||||
// 3 середины сторон
|
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 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:idA, srcB:idC, x:(pA.x+pC.x)/2, y:(pA.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:idA, srcB:idB, x:(pA.x+pB.x)/2, y:(pA.y+pB.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} });
|
this.eng.add({ type:'segment', p1Id:idA, p2Id:mBC.id, style:{color:col, width:1.5} });
|
||||||
// 3 медианы
|
this.eng.add({ type:'segment', p1Id:idB, p2Id:mAC.id, style:{color:col, width:1.5} });
|
||||||
this.eng.add({ type:'segment', p1Id:pA.id, p2Id:mBC.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:'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} });
|
|
||||||
// Центроид
|
|
||||||
this.eng.add({ type:'point', derived:true, constr:'centroid',
|
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,
|
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} });
|
label: nG ? 'G'+(nG+1) : 'G', style:{color:col, size:6} });
|
||||||
this._pending = []; this._preview = null;
|
|
||||||
if (this.onUpdate) this.onUpdate(this.getStats());
|
if (this.onUpdate) this.onUpdate(this.getStats());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'orthocenter': {
|
case 'orthocenter': {
|
||||||
// 3 клика: A, B, C → 3 высоты + точка ортоцентра H
|
// 1 клик на/внутри треугольника → 3 высоты + ортоцентр H
|
||||||
this._pending.push(snapped);
|
const polyOrt = this._findTriangleNear(px, py);
|
||||||
if (this._pending.length === 1) {
|
if (polyOrt) {
|
||||||
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) {
|
|
||||||
this._pushUndo();
|
this._pushUndo();
|
||||||
const pA = this._ensurePoint(this._pending[0]);
|
const [idA, idB, idC] = polyOrt.pointIds;
|
||||||
const pB = this._ensurePoint(this._pending[1]);
|
const pA = this.eng.get(idA), pB = this.eng.get(idB), pC = this.eng.get(idC);
|
||||||
const pC = this._ensurePoint(this._pending[2]);
|
const nH = this.eng.byType('point').filter(p=>p.constr==='orthocenter').length;
|
||||||
const nH = this.eng.byType('point').filter(p=>p.constr==='orthocenter').length;
|
|
||||||
const col = '#F97316';
|
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 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:idA, ptB:idB, ptC:idC, ...gFoot(A,B,C), label:'H_a', style:{color:col,size:3} });
|
||||||
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:idB, ptB:idA, ptC:idC, ...gFoot(B,A,C), label:'H_b', 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:idC, ptB:idA, ptC:idB, ...gFoot(C,A,B), label:'H_c', 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: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:pA.id, p2Id:Ha.id, style:{color:col, width:1.5} });
|
this.eng.add({ type:'segment', p1Id:idC, p2Id:Hc.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 orth = gOrthocenter(A, B, C);
|
const orth = gOrthocenter(A, B, C);
|
||||||
if (orth) {
|
if (orth) {
|
||||||
this.eng.add({ type:'point', derived:true, constr:'orthocenter',
|
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,
|
x:orth.x, y:orth.y, valid:true,
|
||||||
label: nH ? 'H'+(nH+1) : 'H', style:{color:col, size:6} });
|
label: nH ? 'H'+(nH+1) : 'H', style:{color:col, size:6} });
|
||||||
}
|
}
|
||||||
this._pending = []; this._preview = null;
|
|
||||||
if (this.onUpdate) this.onUpdate(this.getStats());
|
if (this.onUpdate) this.onUpdate(this.getStats());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2257,6 +2249,49 @@ class GeoSim {
|
|||||||
break;
|
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: Подобие (масштаб) ══ */
|
/* ══ Phase 10.2: Подобие (масштаб) ══ */
|
||||||
|
|
||||||
case 'scale': {
|
case 'scale': {
|
||||||
|
|||||||
+16
-11
@@ -3963,6 +3963,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</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'B' ∥ 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 -->
|
<!-- Mark tools -->
|
||||||
<div class="gp-section-title" style="margin-top:4px">Метки</div>
|
<div class="gp-section-title" style="margin-top:4px">Метки</div>
|
||||||
<div class="geo-tool-grid">
|
<div class="geo-tool-grid">
|
||||||
@@ -5481,10 +5490,11 @@
|
|||||||
tick: 'Кликни на отрезок или сторону — добавить штрих (1–3; ещё раз — убрать)',
|
tick: 'Кликни на отрезок или сторону — добавить штрих (1–3; ещё раз — убрать)',
|
||||||
arcmark: 'Кликни на вершину полигона — добавить дугу (1–3; ещё раз — убрать)',
|
arcmark: 'Кликни на вершину полигона — добавить дугу (1–3; ещё раз — убрать)',
|
||||||
parallelmark: 'Кликни на отрезок или сторону — добавить метку параллельности (1–2; ещё раз — убрать)',
|
parallelmark: 'Кликни на отрезок или сторону — добавить метку параллельности (1–2; ещё раз — убрать)',
|
||||||
altitude: 'Кликни на сторону треугольника (или прямую)',
|
altitude: 'Кликни на вершину треугольника — построим высоту из неё',
|
||||||
median: 'Кликни вершину A треугольника',
|
median: 'Кликни на вершину треугольника — построим медиану из неё',
|
||||||
centroid: 'Кликни первую вершину треугольника',
|
centroid: 'Кликни на треугольник или внутри него — построим все 3 медианы и центроид G',
|
||||||
orthocenter: 'Кликни первую вершину треугольника',
|
orthocenter: 'Кликни на треугольник или внутри него — построим все 3 высоты и ортоцентр H',
|
||||||
|
thales: 'Кликни центр подобия O (начало лучей)',
|
||||||
midline: 'Кликни вершину A треугольника',
|
midline: 'Кликни вершину A треугольника',
|
||||||
parallelogram:'Кликни вершину A параллелограмма',
|
parallelogram:'Кликни вершину A параллелограмма',
|
||||||
diagonal: 'Кликни внутри четырёхугольника — построим диагонали',
|
diagonal: 'Кликни внутри четырёхугольника — построим диагонали',
|
||||||
@@ -5508,18 +5518,13 @@
|
|||||||
tangent_2: 'Теперь кликни на внешнюю точку — получишь две касательные',
|
tangent_2: 'Теперь кликни на внешнюю точку — получишь две касательные',
|
||||||
translate_2: 'Теперь кликни конец вектора B',
|
translate_2: 'Теперь кликни конец вектора B',
|
||||||
translate_3: 'Теперь кликни точку P — она будет перенесена',
|
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_2: 'Кликни вершину B (конец первой стороны)',
|
||||||
midline_3: 'Кликни вершину C (конец второй стороны) — построим среднюю линию',
|
midline_3: 'Кликни вершину C (конец второй стороны) — построим среднюю линию',
|
||||||
parallelogram_2: 'Кликни вершину B (смежная с A)',
|
parallelogram_2: 'Кликни вершину B (смежная с A)',
|
||||||
parallelogram_3: 'Кликни вершину C — построим параллелограмм ABCD',
|
parallelogram_3: 'Кликни вершину C — построим параллелограмм ABCD',
|
||||||
scale_2: 'Кликни точку P — построим P\' = O + k·(P − O)',
|
scale_2: 'Кликни точку P — построим P\' = O + k·(P − O)',
|
||||||
|
thales_2: 'Кликни точку A (на первом луче)',
|
||||||
|
thales_3: 'Кликни точку B (на втором луче) — построим A\'B\' ∥ AB',
|
||||||
};
|
};
|
||||||
|
|
||||||
function _geoShowHint(name, phase) {
|
function _geoShowHint(name, phase) {
|
||||||
|
|||||||
Reference in New Issue
Block a user