feat: Phase 4 планиметрии — симметрия (reflect) + правильный n-угольник (ngon)
- GeoEngine: _dependsOn/recompute для constr='reflect' и 'ngon_vertex' - reflect: производная точка-отражение (P'=2·foot-P), зависит от axis+srcPt - ngon: правильный n-угольник по центру и вершине; вершины v1..vn-1 = derived points (constr='ngon_vertex', хранят srcCenter/srcVertex/k/n); при движении центра/вершины все вершины автоматически пересчитываются - GeoSim: _ngonSides=6, setNgonSides(n), инструменты 'reflect'/'ngon' в _handleToolClick - lab.html: кнопки Симметрия и n-угольник, +/- контроллер сторон, хинты Phase 2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user