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;
|
||||
}
|
||||
|
||||
/** Ортоцентр треугольника 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) {
|
||||
let area = 0;
|
||||
@@ -195,8 +202,11 @@ class GeoEngine {
|
||||
return obj.centerId === id || obj.edgeId === id;
|
||||
case 'point':
|
||||
if (!obj.derived) return false;
|
||||
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 === 'midpoint') return obj.srcA === id || obj.srcB === 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.srcPt === id || obj.srcLine === id) return true;
|
||||
// Если srcLine — обычная прямая, зависим и от её точек
|
||||
@@ -280,6 +290,25 @@ class GeoEngine {
|
||||
obj.x = pP.x + (pB.x - pA.x);
|
||||
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) {
|
||||
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);
|
||||
ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();
|
||||
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);
|
||||
}
|
||||
|
||||
/* Метки равных сторон (штрихи поперёк отрезка) */
|
||||
_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) {
|
||||
const m1 = this._mpt(obj.p1Id), m2 = this._mpt(obj.p2Id);
|
||||
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);
|
||||
ctx.closePath(); ctx.stroke();
|
||||
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) {
|
||||
@@ -1037,7 +1097,18 @@ class GeoSim {
|
||||
ctx.strokeStyle = col;
|
||||
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.beginPath();
|
||||
@@ -1057,7 +1128,7 @@ class GeoSim {
|
||||
|
||||
// Подпись угла вдоль биссектрисы
|
||||
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;
|
||||
ctx.font = '10px Manrope,sans-serif';
|
||||
ctx.fillStyle = col;
|
||||
@@ -1071,21 +1142,26 @@ class GeoSim {
|
||||
}
|
||||
}
|
||||
|
||||
// Прямые углы для foot-конструкций (основание высоты всегда 90°)
|
||||
// Прямые углы для foot и altitude_foot конструкций
|
||||
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 P = this.eng.get(obj.srcPt);
|
||||
const sl = this.eng.get(obj.srcLine);
|
||||
if (!P || !sl) continue;
|
||||
let L1m, L2m;
|
||||
if (sl.type === 'derived_line') {
|
||||
L1m = { x: sl.ptX, y: sl.ptY };
|
||||
L2m = { x: sl.ptX + sl.dirX, y: sl.ptY + sl.dirY };
|
||||
let P, L1m, L2m;
|
||||
if (obj.constr === 'altitude_foot') {
|
||||
P = this.eng.get(obj.ptA);
|
||||
L1m = this._mpt(obj.ptB); L2m = this._mpt(obj.ptC);
|
||||
} 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 L2 = this.vp.toCanvas(L2m.x, L2m.y);
|
||||
const Ppx = this.vp.toCanvas(P.x, P.y);
|
||||
@@ -1838,6 +1914,188 @@ class GeoSim {
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
+47
-1
@@ -3896,6 +3896,26 @@
|
||||
</button>
|
||||
</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="geo-tool-grid">
|
||||
<button id="geo-btn-ngon" class="geo-tool-btn" onclick="geoSetTool('ngon',this)" title="Правильный n-угольник — клик центр, клик вершина">
|
||||
@@ -3913,6 +3933,19 @@
|
||||
</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 -->
|
||||
<div class="gp-section-title" style="margin-top:6px">Параметры</div>
|
||||
<label class="geo-toggle-row" onclick="geoToggle('showGrid',this)">
|
||||
@@ -5411,6 +5444,12 @@
|
||||
ngon: 'Клик — центр правильного многоугольника; второй клик — вершина',
|
||||
tangent: 'Кликни на окружность — построим касательные',
|
||||
translate: 'Кликни начало вектора A',
|
||||
tick: 'Кликни на отрезок или сторону — добавить штрих (1–3; ещё раз — убрать)',
|
||||
arcmark: 'Кликни на вершину полигона — добавить дугу (1–3; ещё раз — убрать)',
|
||||
altitude: 'Кликни на сторону треугольника (или прямую)',
|
||||
median: 'Кликни вершину A треугольника',
|
||||
centroid: 'Кликни первую вершину треугольника',
|
||||
orthocenter: 'Кликни первую вершину треугольника',
|
||||
};
|
||||
|
||||
function geoSetTool(name, btnEl) {
|
||||
@@ -5430,6 +5469,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 — построим ортоцентр',
|
||||
};
|
||||
|
||||
function _geoShowHint(name, phase) {
|
||||
@@ -5514,7 +5560,7 @@
|
||||
if (!geomSim) {
|
||||
geomSim = new GeoSim(canvas);
|
||||
geomSim.onUpdate = _geoUpdateStats;
|
||||
geomSim.onHintChange = (tool, phase) => _geoShowHint(tool, phase > 1);
|
||||
geomSim.onHintChange = (tool, phase) => _geoShowHint(tool, phase);
|
||||
geomSim.onDeleteRequest = _geoShowDeleteConfirm;
|
||||
|
||||
// keyboard shortcuts
|
||||
|
||||
Reference in New Issue
Block a user