diff --git a/frontend/js/labs/geometry.js b/frontend/js/labs/geometry.js index 57e09b6..45bfcf9 100644 --- a/frontend/js/labs/geometry.js +++ b/frontend/js/labs/geometry.js @@ -170,12 +170,13 @@ class GeoEngine { 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 === 'foot') { + if (obj.constr === 'foot' || obj.constr === 'reflect') { if (obj.srcPt === id || obj.srcLine === id) return true; // Если srcLine — обычная прямая, зависим и от её точек const sl = this._objects.get(obj.srcLine); return !!(sl && (sl.p1Id === id || sl.p2Id === id)); } + if (obj.constr === 'ngon_vertex') return obj.srcCenter === id || obj.srcVertex === id; return false; case 'derived_line': switch (obj.constr) { @@ -208,7 +209,7 @@ class GeoEngine { if (pt) { obj.x = pt.x; obj.y = pt.y; obj.valid = true; } else obj.valid = false; } - } else if (obj.constr === 'foot') { + } else if (obj.constr === 'foot' || obj.constr === 'reflect') { const srcPt = _g(obj.srcPt); const sl = _g(obj.srcLine); if (!srcPt || !sl) return; @@ -221,8 +222,22 @@ class GeoEngine { } if (L1 && L2) { const f = gFoot({ x: srcPt.x, y: srcPt.y }, L1, L2); - obj.x = f.x; obj.y = f.y; + if (obj.constr === 'foot') { + obj.x = f.x; obj.y = f.y; + } else { + // reflect: P' = 2*foot - P + obj.x = 2*f.x - srcPt.x; + obj.y = 2*f.y - srcPt.y; + } } + } else if (obj.constr === 'ngon_vertex') { + const center = _g(obj.srcCenter), v0 = _g(obj.srcVertex); + if (!center || !v0) return; + const angle = 2 * Math.PI * obj.k / obj.n; + const dx = v0.x - center.x, dy = v0.y - center.y; + const cos_a = Math.cos(angle), sin_a = Math.sin(angle); + obj.x = center.x + dx*cos_a - dy*sin_a; + obj.y = center.y + dx*sin_a + dy*cos_a; } } else if (obj.type === 'circle' && obj.derived) { const pA = _g(obj.ptA), pB = _g(obj.ptB), pC = _g(obj.ptC); @@ -370,9 +385,14 @@ class GeoSim { this.onHintChange = null; // cb(tool, phase) — уведомить UI о смене подсказки this._labelCounter = 0; + this._ngonSides = 6; // для инструмента правильного многоугольника this._bindEvents(); } + setNgonSides(n) { + this._ngonSides = Math.max(3, Math.min(20, n)); + } + /* ── Инициализация ─────────────────────────────────────────── */ fit() { const c = this.canvas; @@ -1499,6 +1519,80 @@ class GeoSim { } break; } + + /* ══ Phase 4: reflect, ngon ══ */ + + case 'reflect': { + if (!this._pendingLineRef) { + // Первый клик: выбрать ось симметрии (прямую/отрезок) + const hit = this._hitTestLine(px, py); + if (hit) { + this._pendingLineRef = hit; + if (this.onHintChange) this.onHintChange('reflect', 2); + } + } else { + // Второй клик: точка, которую отражаем + this._pushUndo(); + const srcPt = 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: srcPt.x, y: srcPt.y }, L1, L2); + const lbl = this._nextLabel(); + this.eng.add({ + type: 'point', derived: true, constr: 'reflect', + srcLine: sl.id, srcPt: srcPt.id, + x: 2*f.x - srcPt.x, + y: 2*f.y - srcPt.y, + label: lbl, style: { color: '#F472B6', size: 4 } + }); + } + this._pendingLineRef = null; this._pending = []; this._preview = null; + if (this.onUpdate) this.onUpdate(this.getStats()); + } + break; + } + + case 'ngon': { + this._pending.push(snapped); + if (this._pending.length === 2) { + this._pushUndo(); + const n = this._ngonSides; + const center = this._ensurePoint(this._pending[0]); + const v0 = this._ensurePoint(this._pending[1]); + const col = '#D4B96B'; + const ptIds = [v0.id]; + + for (let k = 1; k < n; k++) { + const angle = 2 * Math.PI * k / n; + const dx = v0.x - center.x, dy = v0.y - center.y; + const cos_a = Math.cos(angle), sin_a = Math.sin(angle); + const vk = this.eng.add({ + type: 'point', derived: true, constr: 'ngon_vertex', + srcCenter: center.id, srcVertex: v0.id, k, n, + x: center.x + dx*cos_a - dy*sin_a, + y: center.y + dx*sin_a + dy*cos_a, + label: '', style: { color: col, size: 4 } + }); + ptIds.push(vk.id); + } + + this.eng.add({ + type: 'polygon', pointIds: ptIds, + style: { color: col, fillColor: 'rgba(212,185,107,0.08)' } + }); + + this._pending = []; this._preview = null; + if (this.onUpdate) this.onUpdate(this.getStats()); + } + break; + } } this.render(); } diff --git a/frontend/lab.html b/frontend/lab.html index 18feb62..5503a80 100644 --- a/frontend/lab.html +++ b/frontend/lab.html @@ -672,6 +672,25 @@ grid-column: span 2; } + .geo-ngon-ctrl { + display: flex; align-items: center; justify-content: center; gap: 6px; + border: 1.5px solid var(--border); border-radius: 10px; + padding: 4px 6px; + } + .geo-ngon-btn { + width: 22px; height: 22px; border-radius: 6px; + border: 1px solid var(--border-h); + background: transparent; color: var(--text-2); + cursor: pointer; display: flex; align-items: center; justify-content: center; + transition: background .12s; + } + .geo-ngon-btn svg { width: 12px; height: 12px; stroke: currentColor; } + .geo-ngon-btn:hover { background: rgba(155,93,229,.1); } + #geo-ngon-n { + font-size: 0.78rem; font-weight: 700; color: var(--text); + min-width: 18px; text-align: center; + } + .geo-toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 5px 4px; border-radius: 8px; cursor: pointer; @@ -3840,6 +3859,31 @@ +