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:
Maxim Dolgolyov
2026-04-14 11:08:16 +03:00
parent 76428db513
commit 2191e53529
2 changed files with 320 additions and 16 deletions
+273 -15
View File
@@ -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
View File
@@ -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