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:
Maxim Dolgolyov
2026-04-14 10:22:49 +03:00
parent 2e7ec81e59
commit e2e351d9c2
2 changed files with 154 additions and 3 deletions
+57
View File
@@ -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 @@
</button>
</div>
<div class="gp-section-title" style="margin-top:4px">Преобразования</div>
<div class="geo-tool-grid">
<button id="geo-btn-reflect" class="geo-tool-btn geo-tool-wide" onclick="geoSetTool('reflect',this)" title="Симметрия точки относительно прямой — клик на ось, затем на точку">
<svg viewBox="0 0 24 24" fill="none"><line x1="12" y1="2" x2="12" y2="22" stroke-width="1.5" stroke-dasharray="3,2"/><circle cx="6" cy="12" r="3" stroke-width="1.5"/><circle cx="18" cy="12" r="2.5" stroke-width="1.5" opacity=".5"/><line x1="9" y1="12" x2="15" y2="12" stroke-width="1" opacity=".6"/></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-угольник — клик центр, клик вершина">
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 20.2,8.5 17.3,18 6.7,18 3.8,8.5" stroke-width="1.5" fill="none"/></svg>
n-угольник
</button>
<div class="geo-ngon-ctrl" id="geo-ngon-ctrl">
<button class="geo-ngon-btn" onclick="geoNgonN(-1)">
<svg viewBox="0 0 16 16" fill="none"><line x1="3" y1="8" x2="13" y2="8" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<span id="geo-ngon-n">6</span>
<button class="geo-ngon-btn" onclick="geoNgonN(+1)">
<svg viewBox="0 0 16 16" fill="none"><line x1="8" y1="3" x2="8" y2="13" stroke-width="2" stroke-linecap="round"/><line x1="3" y1="8" x2="13" y2="8" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
</div>
<!-- Display options -->
<div class="gp-section-title" style="margin-top:6px">Параметры</div>
<label class="geo-toggle-row" onclick="geoToggle('showGrid',this)">
@@ -5328,6 +5372,8 @@
foot: 'Сначала кликни на прямую/отрезок',
circumcircle: 'Кликни 3 точки треугольника — получи описанную окружность',
incircle: 'Кликни 3 точки треугольника — получи вписанную окружность',
reflect: 'Сначала кликни на ось симметрии (прямую/отрезок)',
ngon: 'Клик — центр правильного многоугольника; второй клик — вершина',
};
function geoSetTool(name, btnEl) {
@@ -5347,11 +5393,22 @@
perpendicular: 'Теперь кликни на точку — через неё проведём перпендикуляр',
intersect: 'Теперь кликни на вторую прямую',
foot: 'Теперь кликни на точку — найдём основание перпендикуляра',
reflect: 'Теперь кликни на точку — получишь её симметричное отражение',
};
hint.textContent = phase2hints[name] || _GEO_HINTS[name] || '';
} else {
hint.textContent = _GEO_HINTS[name] || '';
}
// Показываем/скрываем n-selector только для ngon
const ngonCtrl = document.getElementById('geo-ngon-ctrl');
// ngon-ctrl всегда виден — расположен рядом с кнопкой в grid
}
function geoNgonN(delta) {
if (!geomSim) return;
geomSim.setNgonSides(geomSim._ngonSides + delta);
const el = document.getElementById('geo-ngon-n');
if (el) el.textContent = geomSim._ngonSides;
}
function geoToggle(prop, rowEl) {