feat: Фазы 7–8 планиметрии — элементы треугольника + метки
Фаза 7: - altitude: высота (клик на сторону → клик вершина → foot + отрезок + прямой угол) - median: медиана (3 клика A,B,C → midpoint + отрезок) - centroid: 3 клика → 3 медианы + точка G (centroid constr) - orthocenter: 3 клика → 3 высоты + точка H (orthocenter constr + altitude_foot constr) - gOrthocenter() math function - Прямые углы для altitude_foot в _drawAngleMeasures - Исправлен баг onHintChange: передавался boolean вместо numeric phase Фаза 8: - tick tool: метки равных сторон на отрезках и сторонах полигонов (1–3 штриха) - arcmark tool: метки равных углов на вершинах полигонов (1–3 дуги) - _drawTickMark(), sideMarks[], angleMarks[] на полигонах - Новая секция «Метки» в панели инструментов Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+273
-15
@@ -92,6 +92,13 @@ function gAngleDeg(A, B, C) {
|
|||||||
return Math.acos(Math.max(-1, Math.min(1, cos))) * 180 / Math.PI;
|
return Math.acos(Math.max(-1, Math.min(1, cos))) * 180 / Math.PI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ортоцентр треугольника ABC */
|
||||||
|
function gOrthocenter(A, B, C) {
|
||||||
|
const Ha = gFoot(A, B, C); // основание высоты из A на BC
|
||||||
|
const Hb = gFoot(B, A, C); // основание высоты из B на AC
|
||||||
|
return gIntersectLines(A, Ha, B, Hb);
|
||||||
|
}
|
||||||
|
|
||||||
/** Площадь многоугольника (формула Гаусса) */
|
/** Площадь многоугольника (формула Гаусса) */
|
||||||
function gPolygonArea(pts) {
|
function gPolygonArea(pts) {
|
||||||
let area = 0;
|
let area = 0;
|
||||||
@@ -195,8 +202,11 @@ class GeoEngine {
|
|||||||
return obj.centerId === id || obj.edgeId === id;
|
return obj.centerId === id || obj.edgeId === id;
|
||||||
case 'point':
|
case 'point':
|
||||||
if (!obj.derived) return false;
|
if (!obj.derived) return false;
|
||||||
if (obj.constr === 'midpoint') return obj.srcA === id || obj.srcB === id;
|
if (obj.constr === 'midpoint') return obj.srcA === id || obj.srcB === id;
|
||||||
if (obj.constr === 'intersect') return obj.src1 === id || obj.src2 === id;
|
if (obj.constr === 'intersect') return obj.src1 === id || obj.src2 === id;
|
||||||
|
if (obj.constr === 'centroid' || obj.constr === 'orthocenter')
|
||||||
|
return obj.ptA === id || obj.ptB === id || obj.ptC === id;
|
||||||
|
if (obj.constr === 'altitude_foot') return obj.ptA === id || obj.ptB === id || obj.ptC === id;
|
||||||
if (obj.constr === 'foot' || obj.constr === 'reflect') {
|
if (obj.constr === 'foot' || obj.constr === 'reflect') {
|
||||||
if (obj.srcPt === id || obj.srcLine === id) return true;
|
if (obj.srcPt === id || obj.srcLine === id) return true;
|
||||||
// Если srcLine — обычная прямая, зависим и от её точек
|
// Если srcLine — обычная прямая, зависим и от её точек
|
||||||
@@ -280,6 +290,25 @@ class GeoEngine {
|
|||||||
obj.x = pP.x + (pB.x - pA.x);
|
obj.x = pP.x + (pB.x - pA.x);
|
||||||
obj.y = pP.y + (pB.y - pA.y);
|
obj.y = pP.y + (pB.y - pA.y);
|
||||||
}
|
}
|
||||||
|
} else if (obj.constr === 'centroid') {
|
||||||
|
const pA = _g(obj.ptA), pB = _g(obj.ptB), pC = _g(obj.ptC);
|
||||||
|
if (pA && pB && pC) {
|
||||||
|
obj.x = (pA.x + pB.x + pC.x) / 3;
|
||||||
|
obj.y = (pA.y + pB.y + pC.y) / 3;
|
||||||
|
}
|
||||||
|
} else if (obj.constr === 'orthocenter') {
|
||||||
|
const pA = _g(obj.ptA), pB = _g(obj.ptB), pC = _g(obj.ptC);
|
||||||
|
if (pA && pB && pC) {
|
||||||
|
const h = gOrthocenter(pA, pB, pC);
|
||||||
|
if (h) { obj.x = h.x; obj.y = h.y; obj.valid = true; }
|
||||||
|
else { obj.valid = false; }
|
||||||
|
}
|
||||||
|
} else if (obj.constr === 'altitude_foot') {
|
||||||
|
const pA = _g(obj.ptA), pB = _g(obj.ptB), pC = _g(obj.ptC);
|
||||||
|
if (pA && pB && pC) {
|
||||||
|
const f = gFoot(pA, pB, pC);
|
||||||
|
obj.x = f.x; obj.y = f.y;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'circle' && obj.derived) {
|
} else if (obj.type === 'circle' && obj.derived) {
|
||||||
const pA = _g(obj.ptA), pB = _g(obj.ptB), pC = _g(obj.ptC);
|
const pA = _g(obj.ptA), pB = _g(obj.ptB), pC = _g(obj.ptC);
|
||||||
@@ -733,9 +762,33 @@ class GeoSim {
|
|||||||
if (obj.style?.dash) ctx.setLineDash(obj.style.dash);
|
if (obj.style?.dash) ctx.setLineDash(obj.style.dash);
|
||||||
ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
if (obj.tickMark) this._drawTickMark(ctx, p1, p2, obj.tickMark, col);
|
||||||
if (this.showLabels && obj.label) this._drawObjLabel(ctx, obj.label, {x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2}, col);
|
if (this.showLabels && obj.label) this._drawObjLabel(ctx, obj.label, {x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2}, col);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Метки равных сторон (штрихи поперёк отрезка) */
|
||||||
|
_drawTickMark(ctx, p1, p2, count, col) {
|
||||||
|
const dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||||
|
const len = Math.hypot(dx, dy);
|
||||||
|
if (len < 1e-9 || !count) return;
|
||||||
|
const ux = dx / len, uy = dy / len; // вдоль отрезка
|
||||||
|
const nx = -uy, ny = ux; // перпендикуляр
|
||||||
|
const TICK = 7, SPACING = 5;
|
||||||
|
const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2;
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = col; ctx.lineWidth = 2; ctx.globalAlpha = 0.9;
|
||||||
|
ctx.shadowColor = col; ctx.shadowBlur = 3;
|
||||||
|
for (let k = 0; k < count; k++) {
|
||||||
|
const off = (k - (count - 1) / 2) * SPACING;
|
||||||
|
const cx = mx + ux * off, cy = my + uy * off;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cx + nx * TICK, cy + ny * TICK);
|
||||||
|
ctx.lineTo(cx - nx * TICK, cy - ny * TICK);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
_drawLine(ctx, obj) {
|
_drawLine(ctx, obj) {
|
||||||
const m1 = this._mpt(obj.p1Id), m2 = this._mpt(obj.p2Id);
|
const m1 = this._mpt(obj.p1Id), m2 = this._mpt(obj.p2Id);
|
||||||
if (!m1 || !m2) return;
|
if (!m1 || !m2) return;
|
||||||
@@ -924,6 +977,13 @@ class GeoSim {
|
|||||||
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
|
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
|
||||||
ctx.closePath(); ctx.stroke();
|
ctx.closePath(); ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
// Метки равных сторон
|
||||||
|
if (obj.sideMarks) {
|
||||||
|
for (let i = 0; i < pts.length; i++) {
|
||||||
|
const mark = obj.sideMarks[i] || 0;
|
||||||
|
if (mark > 0) this._drawTickMark(ctx, pts[i], pts[(i+1)%pts.length], mark, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_drawCircle(ctx, obj) {
|
_drawCircle(ctx, obj) {
|
||||||
@@ -1037,7 +1097,18 @@ class GeoSim {
|
|||||||
ctx.strokeStyle = col;
|
ctx.strokeStyle = col;
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
|
|
||||||
if (Math.abs(angle - 90) < 2) {
|
const explicitMark = poly.angleMarks?.[i] || 0; // 0=auto, 1-3=явный
|
||||||
|
if (explicitMark > 0 && bisLen > 1e-6) {
|
||||||
|
// Явные метки: 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;
|
||||||
|
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.stroke();
|
||||||
|
}
|
||||||
|
} else if (Math.abs(angle - 90) < 2) {
|
||||||
// Прямой угол — маленький квадрат
|
// Прямой угол — маленький квадрат
|
||||||
ctx.globalAlpha = 0.8;
|
ctx.globalAlpha = 0.8;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -1057,7 +1128,7 @@ class GeoSim {
|
|||||||
|
|
||||||
// Подпись угла вдоль биссектрисы
|
// Подпись угла вдоль биссектрисы
|
||||||
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;
|
||||||
@@ -1071,21 +1142,26 @@ class GeoSim {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Прямые углы для foot-конструкций (основание высоты всегда 90°)
|
// Прямые углы для foot и altitude_foot конструкций
|
||||||
for (const obj of this.eng.points()) {
|
for (const obj of this.eng.points()) {
|
||||||
if (!obj.derived || obj.constr !== 'foot') continue;
|
if (!obj.derived || (obj.constr !== 'foot' && obj.constr !== 'altitude_foot')) continue;
|
||||||
const F = this.vp.toCanvas(obj.x, obj.y);
|
const F = this.vp.toCanvas(obj.x, obj.y);
|
||||||
const P = this.eng.get(obj.srcPt);
|
let P, L1m, L2m;
|
||||||
const sl = this.eng.get(obj.srcLine);
|
if (obj.constr === 'altitude_foot') {
|
||||||
if (!P || !sl) continue;
|
P = this.eng.get(obj.ptA);
|
||||||
let L1m, L2m;
|
L1m = this._mpt(obj.ptB); L2m = this._mpt(obj.ptC);
|
||||||
if (sl.type === 'derived_line') {
|
|
||||||
L1m = { x: sl.ptX, y: sl.ptY };
|
|
||||||
L2m = { x: sl.ptX + sl.dirX, y: sl.ptY + sl.dirY };
|
|
||||||
} else {
|
} else {
|
||||||
L1m = this._mpt(sl.p1Id); L2m = this._mpt(sl.p2Id);
|
P = this.eng.get(obj.srcPt);
|
||||||
|
const sl = this.eng.get(obj.srcLine);
|
||||||
|
if (!sl) continue;
|
||||||
|
if (sl.type === 'derived_line') {
|
||||||
|
L1m = { x: sl.ptX, y: sl.ptY };
|
||||||
|
L2m = { x: sl.ptX + sl.dirX, y: sl.ptY + sl.dirY };
|
||||||
|
} else {
|
||||||
|
L1m = this._mpt(sl.p1Id); L2m = this._mpt(sl.p2Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!L1m || !L2m) continue;
|
if (!P || !L1m || !L2m) continue;
|
||||||
const L1 = this.vp.toCanvas(L1m.x, L1m.y);
|
const L1 = this.vp.toCanvas(L1m.x, L1m.y);
|
||||||
const L2 = this.vp.toCanvas(L2m.x, L2m.y);
|
const L2 = this.vp.toCanvas(L2m.x, L2m.y);
|
||||||
const Ppx = this.vp.toCanvas(P.x, P.y);
|
const Ppx = this.vp.toCanvas(P.x, P.y);
|
||||||
@@ -1838,6 +1914,188 @@ class GeoSim {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ══ Phase 8: tick marks, arc marks ══ */
|
||||||
|
|
||||||
|
case 'tick': {
|
||||||
|
// Клик на отрезок или сторону полигона → циклически меняет метку (0→1→2→3→0)
|
||||||
|
const line = this._hitTestLine(px, py);
|
||||||
|
if (line) {
|
||||||
|
this._pushUndo();
|
||||||
|
if (line.virtual && line.polyId) {
|
||||||
|
const poly = this.eng.get(line.polyId);
|
||||||
|
if (poly) {
|
||||||
|
if (!poly.sideMarks) poly.sideMarks = new Array(poly.pointIds.length).fill(0);
|
||||||
|
const si = poly.pointIds.indexOf(line.p1Id);
|
||||||
|
if (si >= 0) poly.sideMarks[si] = ((poly.sideMarks[si] || 0) + 1) % 4;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const seg = this.eng.get(line.id);
|
||||||
|
if (seg) seg.tickMark = ((seg.tickMark || 0) + 1) % 4;
|
||||||
|
}
|
||||||
|
if (this.onUpdate) this.onUpdate(this.getStats());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'arcmark': {
|
||||||
|
// Клик на вершину полигона → циклически меняет метку дуги (0→1→2→3→0)
|
||||||
|
const SNAP_PX = 14;
|
||||||
|
let foundPoly = null, foundIdx = -1;
|
||||||
|
outer8:
|
||||||
|
for (const poly of this.eng.byType('polygon')) {
|
||||||
|
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) {
|
||||||
|
foundPoly = poly; foundIdx = i; break outer8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundPoly && foundIdx >= 0) {
|
||||||
|
this._pushUndo();
|
||||||
|
if (!foundPoly.angleMarks) foundPoly.angleMarks = new Array(foundPoly.pointIds.length).fill(0);
|
||||||
|
foundPoly.angleMarks[foundIdx] = (foundPoly.angleMarks[foundIdx] + 1) % 4;
|
||||||
|
if (this.onUpdate) this.onUpdate(this.getStats());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══ 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 {
|
||||||
|
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;
|
||||||
|
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) {
|
||||||
|
this._pushUndo();
|
||||||
|
const ptA = this._ensurePoint(this._pending[0]);
|
||||||
|
const ptB = this._ensurePoint(this._pending[1]);
|
||||||
|
const ptC = this._ensurePoint(this._pending[2]);
|
||||||
|
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;
|
||||||
|
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) {
|
||||||
|
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 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} });
|
||||||
|
// Центроид
|
||||||
|
this.eng.add({ type:'point', derived:true, constr:'centroid',
|
||||||
|
ptA:pA.id, ptB:pB.id, ptC:pC.id,
|
||||||
|
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) {
|
||||||
|
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 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 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,
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-1
@@ -3896,6 +3896,26 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gp-section-title" style="margin-top:4px">Элементы треугольника</div>
|
||||||
|
<div class="geo-tool-grid">
|
||||||
|
<button id="geo-btn-altitude" class="geo-tool-btn" onclick="geoSetTool('altitude',this)" title="Высота — клик на сторону, затем на вершину">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none"><line x1="3" y1="20" x2="21" y2="20" stroke-width="1.5"/><line x1="10" y1="4" x2="10" y2="20" stroke-width="1.5" stroke-dasharray="4,3"/><rect x="10" y="14" width="4" height="4" stroke-width="1.2"/><polygon points="10,4 3,20 21,20" stroke-width="1.5" fill="none"/></svg>
|
||||||
|
Высота
|
||||||
|
</button>
|
||||||
|
<button id="geo-btn-median" class="geo-tool-btn" onclick="geoSetTool('median',this)" title="Медиана — клик вершина A, B, C">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 3,20 21,20" stroke-width="1.5" fill="none"/><line x1="12" y1="3" x2="12" y2="20" stroke-width="1.5"/><circle cx="12" cy="20" r="2.5" fill="currentColor"/></svg>
|
||||||
|
Медиана
|
||||||
|
</button>
|
||||||
|
<button id="geo-btn-centroid" class="geo-tool-btn" onclick="geoSetTool('centroid',this)" title="Центроид — 3 точки треугольника, строит 3 медианы">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 3,20 21,20" stroke-width="1.5" fill="none"/><line x1="12" y1="3" x2="12" y2="20" stroke-width="1.2" opacity=".6"/><line x1="3" y1="20" x2="16.5" y2="11.5" stroke-width="1.2" opacity=".6"/><line x1="21" y1="20" x2="7.5" y2="11.5" stroke-width="1.2" opacity=".6"/><circle cx="12" cy="14.3" r="2.5" fill="currentColor"/></svg>
|
||||||
|
Центроид
|
||||||
|
</button>
|
||||||
|
<button id="geo-btn-orthocenter" class="geo-tool-btn" onclick="geoSetTool('orthocenter',this)" title="Ортоцентр — 3 точки треугольника, строит 3 высоты">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 3,20 21,20" stroke-width="1.5" fill="none"/><line x1="12" y1="3" x2="12" y2="20" stroke-width="1.2" stroke-dasharray="3,2" opacity=".6"/><line x1="3" y1="20" x2="16" y2="12" stroke-width="1.2" stroke-dasharray="3,2" opacity=".6"/><line x1="21" y1="20" x2="8" y2="12" stroke-width="1.2" stroke-dasharray="3,2" opacity=".6"/><circle cx="12" cy="14" r="2.5" fill="currentColor"/></svg>
|
||||||
|
Ортоцентр
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
||||||
<button id="geo-btn-ngon" class="geo-tool-btn" onclick="geoSetTool('ngon',this)" title="Правильный n-угольник — клик центр, клик вершина">
|
<button id="geo-btn-ngon" class="geo-tool-btn" onclick="geoSetTool('ngon',this)" title="Правильный n-угольник — клик центр, клик вершина">
|
||||||
@@ -3913,6 +3933,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mark tools -->
|
||||||
|
<div class="gp-section-title" style="margin-top:4px">Метки</div>
|
||||||
|
<div class="geo-tool-grid">
|
||||||
|
<button id="geo-btn-tick" class="geo-tool-btn" onclick="geoSetTool('tick',this)" title="Метки равных сторон — клик на отрезок или сторону (1–3 штриха)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none"><line x1="3" y1="20" x2="21" y2="4" stroke-width="1.5"/><line x1="11" y1="7" x2="8" y2="11" stroke-width="2" stroke-linecap="round"/><line x1="13" y1="9" x2="10" y2="13" stroke-width="2" stroke-linecap="round"/></svg>
|
||||||
|
Штрихи
|
||||||
|
</button>
|
||||||
|
<button id="geo-btn-arcmark" class="geo-tool-btn" onclick="geoSetTool('arcmark',this)" title="Метки равных углов — клик на вершину полигона (1–3 дуги)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none"><path d="M4 20 L20 20 L20 4" stroke-width="1.5" fill="none"/><path d="M8 20 A12 12 0 0 1 20 8" stroke-width="1.5" fill="none"/><path d="M11 20 A9 9 0 0 1 20 11" stroke-width="1.5" fill="none"/></svg>
|
||||||
|
Дуги
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Display options -->
|
<!-- Display options -->
|
||||||
<div class="gp-section-title" style="margin-top:6px">Параметры</div>
|
<div class="gp-section-title" style="margin-top:6px">Параметры</div>
|
||||||
<label class="geo-toggle-row" onclick="geoToggle('showGrid',this)">
|
<label class="geo-toggle-row" onclick="geoToggle('showGrid',this)">
|
||||||
@@ -5411,6 +5444,12 @@
|
|||||||
ngon: 'Клик — центр правильного многоугольника; второй клик — вершина',
|
ngon: 'Клик — центр правильного многоугольника; второй клик — вершина',
|
||||||
tangent: 'Кликни на окружность — построим касательные',
|
tangent: 'Кликни на окружность — построим касательные',
|
||||||
translate: 'Кликни начало вектора A',
|
translate: 'Кликни начало вектора A',
|
||||||
|
tick: 'Кликни на отрезок или сторону — добавить штрих (1–3; ещё раз — убрать)',
|
||||||
|
arcmark: 'Кликни на вершину полигона — добавить дугу (1–3; ещё раз — убрать)',
|
||||||
|
altitude: 'Кликни на сторону треугольника (или прямую)',
|
||||||
|
median: 'Кликни вершину A треугольника',
|
||||||
|
centroid: 'Кликни первую вершину треугольника',
|
||||||
|
orthocenter: 'Кликни первую вершину треугольника',
|
||||||
};
|
};
|
||||||
|
|
||||||
function geoSetTool(name, btnEl) {
|
function geoSetTool(name, btnEl) {
|
||||||
@@ -5430,6 +5469,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 — построим ортоцентр',
|
||||||
};
|
};
|
||||||
|
|
||||||
function _geoShowHint(name, phase) {
|
function _geoShowHint(name, phase) {
|
||||||
@@ -5514,7 +5560,7 @@
|
|||||||
if (!geomSim) {
|
if (!geomSim) {
|
||||||
geomSim = new GeoSim(canvas);
|
geomSim = new GeoSim(canvas);
|
||||||
geomSim.onUpdate = _geoUpdateStats;
|
geomSim.onUpdate = _geoUpdateStats;
|
||||||
geomSim.onHintChange = (tool, phase) => _geoShowHint(tool, phase > 1);
|
geomSim.onHintChange = (tool, phase) => _geoShowHint(tool, phase);
|
||||||
geomSim.onDeleteRequest = _geoShowDeleteConfirm;
|
geomSim.onDeleteRequest = _geoShowDeleteConfirm;
|
||||||
|
|
||||||
// keyboard shortcuts
|
// keyboard shortcuts
|
||||||
|
|||||||
Reference in New Issue
Block a user