feat: планиметрия — интерактивная геометрическая симуляция
- Новый файл frontend/js/labs/geometry.js (~1200 строк): GeoEngine (граф объектов с каскадным удалением), GeoViewport (система координат math↔canvas, зум/пан), GeoSim (полный движок: точки, отрезки, прямые, лучи, окружности, треугольники, многоугольники, привязка к сетке и точкам, undo/redo, экспорт PNG, classroom sync) - frontend/lab.html: карточка, ctrl, sim-geometry секция, функции geoSetTool/geoToggle/_openGeometry, скрипт-тег - frontend/admin.html: geometry в ADMIN_SIMS - backend/src/db/migrate.js: таблицы geometry_tasks, geometry_submissions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2867,3 +2867,35 @@ db.exec(`
|
||||
PRIMARY KEY (session_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// ── Geometry (Planimetry) ────────────────────────────────────────────────────
|
||||
// Saved geometry constructions (teacher-created tasks/templates)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS geometry_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
teacher_id INTEGER NOT NULL REFERENCES users(id),
|
||||
class_id INTEGER REFERENCES classes(id) ON DELETE SET NULL,
|
||||
title TEXT NOT NULL DEFAULT 'Без названия',
|
||||
description TEXT DEFAULT '',
|
||||
state_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_geo_tasks_teacher ON geometry_tasks(teacher_id)');
|
||||
|
||||
// Student submissions for geometry tasks
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS geometry_submissions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL REFERENCES geometry_tasks(id) ON DELETE CASCADE,
|
||||
student_id INTEGER NOT NULL REFERENCES users(id),
|
||||
state_json TEXT NOT NULL DEFAULT '{}',
|
||||
score REAL DEFAULT NULL,
|
||||
feedback TEXT DEFAULT '',
|
||||
submitted_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(task_id, student_id)
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_geo_subs_task ON geometry_submissions(task_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_geo_subs_student ON geometry_submissions(student_id)');
|
||||
|
||||
@@ -4446,6 +4446,7 @@
|
||||
const ADMIN_SIMS = [
|
||||
{ id: 'graph', cat: 'Математика', title: 'График функции' },
|
||||
{ id: 'graphtransform', cat: 'Математика', title: 'Трансформации графиков' },
|
||||
{ id: 'geometry', cat: 'Математика', title: 'Планиметрия' },
|
||||
{ id: 'triangle', cat: 'Математика', title: 'Геометрия треугольника' },
|
||||
{ id: 'quadratic', cat: 'Математика', title: 'Корни квадратного уравнения' },
|
||||
{ id: 'stereo', cat: 'Математика', title: 'Стереометрия 3D' },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+338
-2
@@ -641,6 +641,94 @@
|
||||
.embed-mode #lab-sim { flex: 1; }
|
||||
.embed-mode .sim-body-wrap { height: 100vh; }
|
||||
.embed-mode .graph-panel { max-height: 100vh; }
|
||||
|
||||
/* ════════════════════════════════
|
||||
GEOMETRY SIM STYLES
|
||||
════════════════════════════════ */
|
||||
.geo-panel {
|
||||
width: 210px; flex-shrink: 0;
|
||||
background: var(--surface);
|
||||
border-right: 1.5px solid var(--border);
|
||||
display: flex; flex-direction: column;
|
||||
overflow-y: auto; padding: 12px 10px; gap: 4px;
|
||||
}
|
||||
.geo-tool-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.geo-tool-btn {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 7px 9px; border-radius: 10px;
|
||||
border: 1.5px solid var(--border);
|
||||
background: transparent; color: var(--text-2);
|
||||
font-family: 'Manrope', sans-serif; font-size: 0.73rem; font-weight: 700;
|
||||
cursor: pointer; transition: all .14s; white-space: nowrap;
|
||||
}
|
||||
.geo-tool-btn svg { width: 13px; height: 13px; stroke: currentColor; stroke-width: 2.2; flex-shrink: 0; }
|
||||
.geo-tool-btn:hover { border-color: rgba(155,93,229,.4); color: var(--violet); background: rgba(155,93,229,.06); }
|
||||
.geo-tool-btn.active { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,.12); }
|
||||
|
||||
.geo-tool-wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.geo-toggle-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 5px 4px; border-radius: 8px; cursor: pointer;
|
||||
transition: background .13s;
|
||||
}
|
||||
.geo-toggle-row:hover { background: rgba(255,255,255,.04); }
|
||||
.geo-toggle-label {
|
||||
font-size: 0.73rem; font-weight: 600; color: var(--text-2);
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.geo-toggle-label svg { width: 12px; height: 12px; stroke: currentColor; stroke-width: 2; opacity: .7; }
|
||||
.geo-toggle {
|
||||
width: 28px; height: 16px; border-radius: 8px;
|
||||
background: rgba(255,255,255,.1); border: 1.5px solid var(--border-h);
|
||||
position: relative; transition: background .15s; flex-shrink: 0;
|
||||
}
|
||||
.geo-toggle::after {
|
||||
content: ''; position: absolute; left: 2px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 9px; height: 9px; border-radius: 50%;
|
||||
background: rgba(255,255,255,.4); transition: all .15s;
|
||||
}
|
||||
.geo-toggle.on { background: var(--violet); border-color: var(--violet); }
|
||||
.geo-toggle.on::after { left: calc(100% - 11px); background: #fff; }
|
||||
|
||||
.geo-info-box {
|
||||
background: rgba(155,93,229,.07); border: 1px solid rgba(155,93,229,.15);
|
||||
border-radius: 10px; padding: 8px 10px; margin-top: 4px;
|
||||
font-size: 0.72rem; line-height: 1.55; color: var(--text-2);
|
||||
}
|
||||
.geo-info-box .geo-info-key { color: var(--violet); font-weight: 700; }
|
||||
|
||||
.geo-stat-row {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
font-size: 0.7rem; padding: 2px 0;
|
||||
border-bottom: 1px dashed rgba(255,255,255,.05);
|
||||
color: var(--text-2);
|
||||
}
|
||||
.geo-stat-row:last-child { border: none; }
|
||||
.geo-stat-row b { color: var(--text); font-weight: 700; }
|
||||
|
||||
.geo-canvas-outer {
|
||||
flex: 1; min-width: 0; position: relative; background: #0a0718;
|
||||
}
|
||||
.geo-canvas-outer canvas {
|
||||
display: block; position: absolute; top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
.geo-hint-bar {
|
||||
position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
|
||||
background: rgba(10,7,24,.82); border: 1px solid rgba(255,255,255,.1);
|
||||
border-radius: 20px; padding: 4px 14px;
|
||||
font-size: 0.7rem; color: rgba(255,255,255,.5);
|
||||
pointer-events: none; white-space: nowrap;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||||
@@ -741,6 +829,26 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- geometry controls -->
|
||||
<div id="ctrl-geometry" class="sim-zoom-btns" style="display:none">
|
||||
<button class="zoom-btn" onclick="geomSim&&geomSim.undo()" title="Отменить (Ctrl+Z)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 7v6h6"/><path d="M3 13A9 9 0 1 0 6 6.3L3 7"/></svg>
|
||||
</button>
|
||||
<button class="zoom-btn" onclick="geomSim&&geomSim.redo()" title="Повторить (Ctrl+Y)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 7v6h-6"/><path d="M21 13A9 9 0 1 1 18 6.3L21 7"/></svg>
|
||||
</button>
|
||||
<button class="zoom-btn" onclick="geomSim&&geomSim.deleteSelected()" title="Удалить выбранное">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||||
</button>
|
||||
<div style="width:1px;height:20px;background:rgba(255,255,255,0.1);margin:0 2px"></div>
|
||||
<button class="zoom-btn" onclick="geomSim&&geomSim.resetView()" title="Сброс вида">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
</button>
|
||||
<button class="zoom-btn" onclick="geomSim&&geomSim.exportPNG()" title="Экспорт PNG">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- trig circle controls -->
|
||||
<div id="ctrl-trigcircle" class="sim-zoom-btns" style="display:none">
|
||||
<button class="zoom-btn" onclick="trigReset()" title="Сбросить на 45°">
|
||||
@@ -3634,6 +3742,130 @@
|
||||
</div><!-- /.sim-body-wrap -->
|
||||
</div><!-- /#sim-hydro -->
|
||||
|
||||
<!-- ══════════════════════════════════════════════
|
||||
ПЛАНИМЕТРИЯ
|
||||
══════════════════════════════════════════════ -->
|
||||
<div id="sim-geometry" class="sim-proj-wrap" style="display:none">
|
||||
<div class="sim-body-wrap">
|
||||
|
||||
<!-- left panel -->
|
||||
<div class="geo-panel">
|
||||
|
||||
<!-- Tool: select + point -->
|
||||
<div class="gp-section-title">Инструмент</div>
|
||||
<div class="geo-tool-grid">
|
||||
<button id="geo-btn-select" class="geo-tool-btn active" onclick="geoSetTool('select',this)" title="Выделить / переместить (Esc)">
|
||||
<svg viewBox="0 0 24 24" fill="none"><path d="M5 3l14 9-7 1-4 7z" stroke-width="2"/></svg>
|
||||
Выбор
|
||||
</button>
|
||||
<button id="geo-btn-point" class="geo-tool-btn" onclick="geoSetTool('point',this)" title="Поставить точку">
|
||||
<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="4" fill="currentColor"/></svg>
|
||||
Точка
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gp-section-title" style="margin-top:4px">Построения</div>
|
||||
<div class="geo-tool-grid">
|
||||
<button id="geo-btn-segment" class="geo-tool-btn" onclick="geoSetTool('segment',this)" title="Отрезок — 2 точки">
|
||||
<svg viewBox="0 0 24 24" fill="none"><line x1="4" y1="20" x2="20" y2="4" stroke-width="2.5"/><circle cx="4" cy="20" r="2.5" fill="currentColor"/><circle cx="20" cy="4" r="2.5" fill="currentColor"/></svg>
|
||||
Отрезок
|
||||
</button>
|
||||
<button id="geo-btn-line" class="geo-tool-btn" onclick="geoSetTool('line',this)" title="Прямая — 2 точки">
|
||||
<svg viewBox="0 0 24 24" fill="none"><line x1="2" y1="22" x2="22" y2="2" stroke-width="2" stroke-dasharray="3,2"/></svg>
|
||||
Прямая
|
||||
</button>
|
||||
<button id="geo-btn-ray" class="geo-tool-btn" onclick="geoSetTool('ray',this)" title="Луч — начало + направление">
|
||||
<svg viewBox="0 0 24 24" fill="none"><line x1="4" y1="20" x2="22" y2="4" stroke-width="2"/><polyline points="17 4 22 4 22 9" stroke-width="2"/><circle cx="4" cy="20" r="2.5" fill="currentColor"/></svg>
|
||||
Луч
|
||||
</button>
|
||||
<button id="geo-btn-circle" class="geo-tool-btn" onclick="geoSetTool('circle',this)" title="Окружность — центр + радиус">
|
||||
<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="8" stroke-width="2"/><circle cx="12" cy="12" r="2" fill="currentColor"/></svg>
|
||||
Круг
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gp-section-title" style="margin-top:4px">Фигуры</div>
|
||||
<div class="geo-tool-grid">
|
||||
<button id="geo-btn-triangle" class="geo-tool-btn" onclick="geoSetTool('triangle',this)" title="Треугольник — 3 точки">
|
||||
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 22,21 2,21" stroke-width="2"/></svg>
|
||||
Треуг.
|
||||
</button>
|
||||
<button id="geo-btn-quad" class="geo-tool-btn" onclick="geoSetTool('quad',this)" title="Четырёхугольник — 4 точки">
|
||||
<svg viewBox="0 0 24 24" fill="none"><polygon points="3,6 21,4 20,19 4,20" stroke-width="2"/></svg>
|
||||
Четырёх.
|
||||
</button>
|
||||
<button id="geo-btn-polygon" class="geo-tool-btn geo-tool-wide" onclick="geoSetTool('polygon',this)" title="Многоугольник — N точек, Enter/двойной клик для завершения">
|
||||
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,2 22,8 19,21 5,21 2,8" stroke-width="2"/></svg>
|
||||
Многоугольник
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Display options -->
|
||||
<div class="gp-section-title" style="margin-top:6px">Параметры</div>
|
||||
<label class="geo-toggle-row" onclick="geoToggle('showGrid',this)">
|
||||
<span class="geo-toggle-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
Сетка
|
||||
</span>
|
||||
<div class="geo-toggle on" id="geo-tog-showGrid"></div>
|
||||
</label>
|
||||
<label class="geo-toggle-row" onclick="geoToggle('showAxes',this)">
|
||||
<span class="geo-toggle-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><line x1="12" y1="20" x2="12" y2="4"/><line x1="4" y1="12" x2="20" y2="12"/></svg>
|
||||
Оси
|
||||
</span>
|
||||
<div class="geo-toggle on" id="geo-tog-showAxes"></div>
|
||||
</label>
|
||||
<label class="geo-toggle-row" onclick="geoToggle('showLabels',this)">
|
||||
<span class="geo-toggle-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||
Метки
|
||||
</span>
|
||||
<div class="geo-toggle on" id="geo-tog-showLabels"></div>
|
||||
</label>
|
||||
<label class="geo-toggle-row" onclick="geoToggle('showLengths',this)">
|
||||
<span class="geo-toggle-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="8" x2="4" y2="16"/><line x1="20" y1="8" x2="20" y2="16"/></svg>
|
||||
Длины
|
||||
</span>
|
||||
<div class="geo-toggle" id="geo-tog-showLengths"></div>
|
||||
</label>
|
||||
<label class="geo-toggle-row" onclick="geoToggle('showAngles',this)">
|
||||
<span class="geo-toggle-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 20 L20 20 L20 4"/><path d="M7 20 A13 13 0 0 1 20 9"/></svg>
|
||||
Углы
|
||||
</span>
|
||||
<div class="geo-toggle" id="geo-tog-showAngles"></div>
|
||||
</label>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="gp-section-title" style="margin-top:6px">Объектов</div>
|
||||
<div style="display:flex;flex-direction:column;gap:0">
|
||||
<div class="geo-stat-row"><span>Точки</span><b id="geo-st-pts">0</b></div>
|
||||
<div class="geo-stat-row"><span>Отрезки</span><b id="geo-st-segs">0</b></div>
|
||||
<div class="geo-stat-row"><span>Окружности</span><b id="geo-st-circs">0</b></div>
|
||||
<div class="geo-stat-row"><span>Многоугольники</span><b id="geo-st-polys">0</b></div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div style="margin-top:auto;padding-top:8px;display:flex;flex-direction:column;gap:4px">
|
||||
<button class="gp-btn" onclick="geomSim&&geomSim.reset()" title="Очистить всё">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
|
||||
Очистить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /.geo-panel -->
|
||||
|
||||
<!-- canvas area -->
|
||||
<div class="geo-canvas-outer">
|
||||
<canvas id="geo-canvas"></canvas>
|
||||
<div class="geo-hint-bar" id="geo-hint">Кликни для добавления точки</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.sim-body-wrap -->
|
||||
</div><!-- /#sim-geometry -->
|
||||
|
||||
<!-- ── Theory panel (overlay right) ── -->
|
||||
<div class="theory-panel" id="theory-panel">
|
||||
<div class="theory-panel-inner" id="theory-content"></div>
|
||||
@@ -4188,6 +4420,21 @@
|
||||
stroke="#F15BB5" stroke-width="2.5" fill="none" opacity="0.9"/>
|
||||
<text x="135" y="132" font-size="8" fill="rgba(255,255,255,0.35)" text-anchor="middle" font-family="Manrope,sans-serif">v = \u03bbf \u00b7 y = A sin(\u03c9t \u2212 kx) \u00b7 \u0441\u0442\u043e\u044f\u0447\u0438\u0435 \u0432\u043e\u043b\u043d\u044b</text>`);
|
||||
|
||||
/* Geometry (planimetry) preview */
|
||||
const P_GEOMETRY = _svg(`${_grid('rgba(255,255,255,0.04)')}
|
||||
<circle cx="135" cy="70" r="50" fill="rgba(155,93,229,0.07)" stroke="#9B5DE5" stroke-width="1.5"/>
|
||||
<polygon points="85,99 185,99 135,20" fill="rgba(6,214,224,0.08)" stroke="#06D6E0" stroke-width="1.8"/>
|
||||
<line x1="85" y1="99" x2="162" y2="57" stroke="rgba(241,91,181,0.45)" stroke-width="1.2" stroke-dasharray="4,3"/>
|
||||
<line x1="185" y1="99" x2="109" y2="57" stroke="rgba(241,91,181,0.45)" stroke-width="1.2" stroke-dasharray="4,3"/>
|
||||
<line x1="135" y1="20" x2="135" y2="99" stroke="rgba(241,91,181,0.45)" stroke-width="1.2" stroke-dasharray="4,3"/>
|
||||
<circle cx="135" cy="64" r="4" fill="#06D6E0" opacity="0.9"/>
|
||||
<circle cx="85" cy="99" r="4" fill="#9B5DE5"/>
|
||||
<circle cx="185" cy="99" r="4" fill="#9B5DE5"/>
|
||||
<circle cx="135" cy="20" r="4" fill="#9B5DE5"/>
|
||||
<text x="78" y="111" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">A</text>
|
||||
<text x="188" y="111" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">B</text>
|
||||
<text x="131" y="16" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">C</text>`);
|
||||
|
||||
const SIMS = [
|
||||
/* ── Математика ── */
|
||||
{ id: 'graph', cat: 'math',
|
||||
@@ -4198,6 +4445,10 @@
|
||||
title: 'Трансформации графиков',
|
||||
desc: 'Наблюдай, как сдвиги, растяжения и отражения меняют вид функции y = a·f(kx+b)+c.',
|
||||
preview: P_TRANSFORM },
|
||||
{ id: 'geometry', cat: 'math',
|
||||
title: 'Планиметрия',
|
||||
desc: 'Интерактивная среда построений: точки, отрезки, прямые, окружности, многоугольники. Полноценный чертёж с привязкой и измерениями.',
|
||||
preview: P_GEOMETRY },
|
||||
{ id: 'triangle', cat: 'math',
|
||||
title: 'Геометрия треугольника',
|
||||
desc: 'Интерактивный треугольник: медианы, высоты, биссектрисы, вписанная и описанная окружности.',
|
||||
@@ -4359,6 +4610,7 @@
|
||||
let bohrSim = null;
|
||||
let elecSim = null;
|
||||
let wavesSim = null;
|
||||
let geomSim = null;
|
||||
|
||||
const ALL_SIM_BODIES = ['sim-graph','sim-proj','sim-coll','sim-tri','sim-trigcircle','sim-mag',
|
||||
'sim-molphys',
|
||||
@@ -4368,11 +4620,12 @@
|
||||
'sim-quadratic','sim-normaldist','sim-graphtransform',
|
||||
'sim-pendulum','sim-equilibrium','sim-thinlens','sim-titration',
|
||||
'sim-refraction','sim-mirrors','sim-isoprocess','sim-probability','sim-bohratom','sim-electrolysis',
|
||||
'sim-waves','sim-hydro'];
|
||||
'sim-waves','sim-hydro','sim-geometry'];
|
||||
const ALL_CTRL_BARS = ['ctrl-graph','ctrl-proj','ctrl-coll','ctrl-tri','ctrl-trigcircle','ctrl-mag',
|
||||
'ctrl-molphys',
|
||||
'ctrl-coulomb','ctrl-circuit','ctrl-chemistry','ctrl-dynamics','ctrl-chemsandbox',
|
||||
'ctrl-celldivision','ctrl-photosynthesis','ctrl-angrybirds','ctrl-waves','ctrl-hydro'];
|
||||
'ctrl-celldivision','ctrl-photosynthesis','ctrl-angrybirds','ctrl-waves','ctrl-hydro',
|
||||
'ctrl-geometry'];
|
||||
|
||||
/* ── sim routing ── */
|
||||
|
||||
@@ -4425,6 +4678,7 @@
|
||||
if (id === 'waves') _openWaves();
|
||||
if (id === 'hydrostatics') _openHydro();
|
||||
if (id.startsWith('hydrostatics:')) _openHydro(id.split(':')[1]);
|
||||
if (id === 'geometry') _openGeometry();
|
||||
}
|
||||
|
||||
function _simShow(elId) {
|
||||
@@ -5012,6 +5266,87 @@
|
||||
document.getElementById('tbar-Rr').textContent = f2(s.R) + ' / ' + f2(s.r);
|
||||
}
|
||||
|
||||
/* ── geometry (planimetry) ── */
|
||||
|
||||
const _GEO_HINTS = {
|
||||
select: 'Клик — выбрать объект, перетащи точку для перемещения',
|
||||
point: 'Клик — поставить точку',
|
||||
segment: 'Кликни 2 точки для отрезка',
|
||||
line: 'Кликни 2 точки для прямой',
|
||||
ray: 'Кликни: начало, затем направление',
|
||||
circle: 'Клик — центр; второй клик — радиус',
|
||||
triangle: 'Кликни 3 точки для треугольника',
|
||||
quad: 'Кликни 4 точки для четырёхугольника',
|
||||
polygon: 'Кликай точки; двойной клик или Enter — завершить',
|
||||
};
|
||||
|
||||
function geoSetTool(name, btnEl) {
|
||||
if (!geomSim) return;
|
||||
geomSim.setTool(name);
|
||||
document.querySelectorAll('.geo-tool-btn').forEach(b => b.classList.remove('active'));
|
||||
if (btnEl) btnEl.classList.add('active');
|
||||
const hint = document.getElementById('geo-hint');
|
||||
if (hint) hint.textContent = _GEO_HINTS[name] || '';
|
||||
}
|
||||
|
||||
function geoToggle(prop, rowEl) {
|
||||
if (!geomSim) return;
|
||||
geomSim[prop] = !geomSim[prop];
|
||||
const tog = rowEl.querySelector('.geo-toggle');
|
||||
if (tog) tog.classList.toggle('on', geomSim[prop]);
|
||||
geomSim.render();
|
||||
}
|
||||
|
||||
function _geoUpdateStats() {
|
||||
if (!geomSim) return;
|
||||
const s = geomSim.getStats();
|
||||
document.getElementById('geo-st-pts').textContent = s.pts;
|
||||
document.getElementById('geo-st-segs').textContent = s.segs;
|
||||
document.getElementById('geo-st-circs').textContent = s.circs;
|
||||
document.getElementById('geo-st-polys').textContent = s.polys;
|
||||
}
|
||||
|
||||
function _openGeometry() {
|
||||
document.getElementById('sim-topbar-title').textContent = 'Планиметрия';
|
||||
_simShow('sim-geometry');
|
||||
_simShow('ctrl-geometry');
|
||||
|
||||
_registerSimState(
|
||||
'geometry',
|
||||
() => geomSim?.exportState(),
|
||||
st => { if (geomSim && st) { geomSim.importState(st); _geoUpdateStats(); } }
|
||||
);
|
||||
if (_embedMode) _startStateEmit('geometry');
|
||||
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
const canvas = document.getElementById('geo-canvas');
|
||||
if (!geomSim) {
|
||||
geomSim = new GeoSim(canvas);
|
||||
geomSim.onUpdate = _geoUpdateStats;
|
||||
|
||||
// keyboard shortcuts
|
||||
canvas.setAttribute('tabindex', '0');
|
||||
canvas.addEventListener('keydown', e => {
|
||||
if (!geomSim) return;
|
||||
if (e.key === 'Escape') { geoSetTool('select', document.getElementById('geo-btn-select')); }
|
||||
if ((e.ctrlKey||e.metaKey) && e.key === 'z') { e.preventDefault(); geomSim.undo(); _geoUpdateStats(); }
|
||||
if ((e.ctrlKey||e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key==='z'))) { e.preventDefault(); geomSim.redo(); _geoUpdateStats(); }
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') { geomSim.deleteSelected(); _geoUpdateStats(); }
|
||||
if (e.key === 'Enter') { geomSim._finishPolygon?.(); _geoUpdateStats(); }
|
||||
});
|
||||
}
|
||||
geomSim.fit();
|
||||
geomSim.render();
|
||||
_geoUpdateStats();
|
||||
|
||||
// sync toggle UI to current state
|
||||
['showGrid','showAxes','showLabels','showLengths','showAngles'].forEach(p => {
|
||||
const el = document.getElementById('geo-tog-' + p);
|
||||
if (el) el.classList.toggle('on', !!geomSim[p]);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/* ── trig circle ── */
|
||||
|
||||
let trigSim = null;
|
||||
@@ -8320,5 +8655,6 @@
|
||||
<script src="/js/labs/bohratom.js"></script>
|
||||
<script src="/js/labs/electrolysis.js"></script>
|
||||
<script src="/js/labs/hydrostatics.js"></script>
|
||||
<script src="/js/labs/geometry.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user