feat: режим аннотации поверх симуляции в онлайн-уроке + fix планиметрии (arcmark, triangle tools)

Онлайн-урок:
- Кнопка «Рисовать» в баре симуляции (только учителю)
- При активации: холст доски показывается поверх iframe (z-index), фон прозрачный
- Учитель рисует прямо поверх симуляции обычными инструментами
- Студенты видят то же самое через SSE (classroom_sim_annotate)
- Выход из режима → кнопка «Вернуться к симуляции»

Планиметрия (bugfix):
- arcmark теперь рисуется всегда (не зависит от showAngles)
- altitude/median: 1 клик на вершину треугольника (авто-находит противоположную сторону)
- centroid/orthocenter: 1 клик внутри/на треугольник

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-04-14 11:31:39 +03:00
parent 84dac03e53
commit b520f4b849
4 changed files with 133 additions and 0 deletions
@@ -949,6 +949,19 @@ function simMode(req, res) {
res.json({ ok: true });
}
/* POST /api/classroom/:id/sim/annotate — teacher toggles draw-over-sim mode */
function simAnnotate(req, res) {
const sessionId = Number(req.params.id);
const session = db.prepare(`SELECT * FROM classroom_sessions WHERE id=? AND status='active'`).get(sessionId);
if (!session) return res.status(404).json({ error: 'Сессия не активна' });
if (session.teacher_id !== req.user.id && req.user.role !== 'admin')
return res.status(403).json({ error: 'Нет доступа' });
const { active } = req.body;
emitToSession(sessionId, { type: 'classroom_sim_annotate', sessionId, active: !!active });
res.json({ ok: true });
}
/* DELETE /api/classroom/:id/sim — teacher closes simulation */
function simClose(req, res) {
const sessionId = Number(req.params.id);
@@ -1522,6 +1535,7 @@ module.exports = {
simClose,
simState,
simMode,
simAnnotate,
clearPage,
previewStroke,
broadcastCursor,
+1
View File
@@ -98,6 +98,7 @@ router.post('/:id/sim', ...teacher, c.simOpen);
router.delete('/:id/sim', ...teacher, c.simClose);
router.post('/:id/sim/state', ...teacher, c.simState);
router.post('/:id/sim/mode', ...teacher, c.simMode);
router.post('/:id/sim/annotate', ...teacher, c.simAnnotate);
// Cursor broadcast (all participants)
router.post('/:id/cursor', ...auth, c.broadcastCursor);
+111
View File
@@ -2007,6 +2007,55 @@
background: #0D0D1A;
}
/* ── Annotate-over-sim mode ───────────────────────────────────────────── */
/* Board floats above the sim panel (sim visible behind transparent canvas) */
.cr-board-area.annotate-active .cr-sim-panel { z-index: 1; }
.cr-board-area.annotate-active .cr-board-wrap {
z-index: 45;
background: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
}
.cr-board-area.annotate-active .cr-board-wrap::before,
.cr-board-area.annotate-active .cr-board-wrap::after { display: none !important; }
/* Floating bar shown while annotating */
.cr-annotate-bar {
display: none;
position: absolute; top: 0; left: 0; right: 0; z-index: 60;
height: 40px; flex-shrink: 0;
background: rgba(10,6,22,0.92);
border-bottom: 1.5px solid rgba(241,91,181,0.4);
backdrop-filter: blur(6px);
align-items: center; gap: 8px; padding: 0 12px;
}
.cr-board-area.annotate-active .cr-annotate-bar { display: flex; }
.cr-annotate-bar-label {
font-size: 0.75rem; font-weight: 700; color: #F15BB5;
display: flex; align-items: center; gap: 5px; flex: 1;
}
.cr-annotate-bar-label svg { width: 14px; height: 14px; stroke: #F15BB5; }
.cr-annotate-exit {
padding: 4px 12px; border-radius: 7px; border: 1px solid rgba(241,91,181,0.4);
background: rgba(241,91,181,0.1); color: #F15BB5;
font-family: 'Manrope',sans-serif; font-size: 0.72rem; font-weight: 700;
cursor: pointer; transition: all .15s;
}
.cr-annotate-exit:hover { background: rgba(241,91,181,0.22); }
/* "Draw" button in sim bar */
.cr-sim-annotate-btn {
display: none;
padding: 3px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.12);
background: transparent; color: rgba(255,255,255,0.5);
font-family: 'Manrope',sans-serif; font-size: 0.7rem; font-weight: 700;
cursor: pointer; transition: all .15s; align-items: center; gap: 4px;
margin-right: 4px;
}
.cr-sim-annotate-btn svg { width: 12px; height: 12px; flex-shrink: 0; }
.cr-sim-annotate-btn:hover { color: rgba(255,255,255,0.8); border-color: rgba(241,91,181,0.4); }
.cr-sim-annotate-btn.active { background: rgba(241,91,181,0.18); border-color: rgba(241,91,181,0.5); color: #F15BB5; }
/* Show draw button only for teachers when sim is open */
.cr-sim-panel.open .cr-sim-annotate-btn.teacher-ctrl { display: flex; }
/* ── Simulation picker modal ────────────────────────────────────────── */
.cr-sim-picker-overlay {
position: fixed; inset: 0; z-index: 200;
@@ -2202,6 +2251,11 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/></svg>
</div>
<span class="cr-sim-bar-title" id="cr-sim-bar-title">Симуляция</span>
<!-- Draw-over button (teacher only) -->
<button class="cr-sim-annotate-btn teacher-ctrl" id="cr-sim-annotate-btn" onclick="crToggleAnnotate()" title="Рисовать поверх симуляции">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
Рисовать
</button>
<!-- Mode toggle (teacher only) -->
<div class="cr-sim-mode" id="cr-sim-mode-toggle" style="display:none">
<button class="cr-sim-mode-btn active" id="cr-sim-mode-demo" onclick="crSetSimMode('demo')" title="Все видят одно и то же">Демо</button>
@@ -2215,6 +2269,14 @@
<!-- Blocks student interaction in demo mode -->
<div class="cr-sim-blocker" id="cr-sim-blocker"></div>
</div>
<!-- Annotate-mode floating bar (shown on top of sim when drawing) -->
<div class="cr-annotate-bar" id="cr-annotate-bar">
<span class="cr-annotate-bar-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<button class="cr-annotate-exit" id="cr-annotate-exit-btn" onclick="crToggleAnnotate()">Вернуться к симуляции</button>
</div>
</div><!-- /.cr-board-area -->
<!-- student nav: page nav + follow toggle -->
<div class="cr-student-nav" id="cr-student-nav" style="display:none">
@@ -3600,6 +3662,8 @@
}
} else if (data.type === 'classroom_sim_mode') {
if (_sessionId == data.sessionId) onSimModeChange(data.mode);
} else if (data.type === 'classroom_sim_annotate') {
if (_sessionId == data.sessionId) _crApplyAnnotate(data.active);
} else if (data.type === '_sse_reconnect') {
// SSE reconnected after a drop — re-sync all real-time state to fill the gap
if (_sessionId) resyncAfterReconnect();
@@ -6867,6 +6931,9 @@
const modeToggle = document.getElementById('cr-sim-mode-toggle');
const blocker = document.getElementById('cr-sim-blocker');
// Exit annotate mode when sim closes
if (_annotateActive) _crApplyAnnotate(false);
panel.classList.remove('open');
if (modeToggle) modeToggle.style.display = 'none';
if (blocker) blocker.classList.remove('active');
@@ -6882,6 +6949,50 @@
}
}
let _annotateActive = false;
let _annotateTool = 'pencil'; // saved tool before entering annotate mode
async function crToggleAnnotate() {
if (!_simActive) return;
const isTeacher = _me && (_me.role === 'teacher' || _me.role === 'admin');
const newVal = !_annotateActive;
_crApplyAnnotate(newVal);
// Only teacher broadcasts to students
if (isTeacher && _sessionId) {
try {
await LS.post(`/api/classroom/${_sessionId}/sim/annotate`, { active: newVal });
} catch (e) { /* non-critical */ }
}
}
function _crApplyAnnotate(active) {
_annotateActive = active;
const boardArea = document.getElementById('cr-board-area');
const simBtn = document.getElementById('cr-sim-annotate-btn');
const isTeacher = _me && (_me.role === 'teacher' || _me.role === 'admin');
boardArea?.classList.toggle('annotate-active', active);
if (simBtn) simBtn.classList.toggle('active', active);
if (!_wb) return;
_wb.setAnnotateMode(active);
if (active) {
// Remember current tool, switch to pencil for drawing
_annotateTool = _wb._currentTool || 'pencil';
if (isTeacher) {
_wb.setTool('pencil');
document.querySelectorAll('.cr-tool-btn').forEach(b => b.classList.remove('active'));
document.getElementById('cr-tool-pencil')?.classList.add('active');
}
} else {
// Restore previous tool
if (isTeacher) {
_wb.setTool(_annotateTool);
document.getElementById(`cr-tool-${_annotateTool}`)?.classList.add('active');
}
}
}
async function crSetSimMode(mode) {
if (!_sessionId) return;
try {
+7
View File
@@ -140,6 +140,7 @@ class Whiteboard {
// Board theme
this._boardTheme = opts.boardTheme || 'chalkboard';
this._bgNoiseCache = new Map(); // canvas element per theme
this._annotateMode = false; // true = transparent bg (draw over simulation)
// Compass state machine
this._compassState = 'idle'; // 'idle'|'setting-radius'|'waiting-arc'|'drawing-arc'
@@ -2148,10 +2149,16 @@ class Whiteboard {
return this._bgNoiseCache.get(theme);
}
setAnnotateMode(v) {
this._annotateMode = !!v;
this.render();
}
_renderBg(ctx) {
const W = this._cssW || 300;
const H = this._cssH || 150;
ctx.clearRect(0, 0, W, H);
if (this._annotateMode) return; // transparent bg — simulation shows through
const theme = this._boardTheme || 'chalkboard';
// Base background