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:
@@ -949,6 +949,19 @@ function simMode(req, res) {
|
|||||||
res.json({ ok: true });
|
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 */
|
/* DELETE /api/classroom/:id/sim — teacher closes simulation */
|
||||||
function simClose(req, res) {
|
function simClose(req, res) {
|
||||||
const sessionId = Number(req.params.id);
|
const sessionId = Number(req.params.id);
|
||||||
@@ -1522,6 +1535,7 @@ module.exports = {
|
|||||||
simClose,
|
simClose,
|
||||||
simState,
|
simState,
|
||||||
simMode,
|
simMode,
|
||||||
|
simAnnotate,
|
||||||
clearPage,
|
clearPage,
|
||||||
previewStroke,
|
previewStroke,
|
||||||
broadcastCursor,
|
broadcastCursor,
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ router.post('/:id/sim', ...teacher, c.simOpen);
|
|||||||
router.delete('/:id/sim', ...teacher, c.simClose);
|
router.delete('/:id/sim', ...teacher, c.simClose);
|
||||||
router.post('/:id/sim/state', ...teacher, c.simState);
|
router.post('/:id/sim/state', ...teacher, c.simState);
|
||||||
router.post('/:id/sim/mode', ...teacher, c.simMode);
|
router.post('/:id/sim/mode', ...teacher, c.simMode);
|
||||||
|
router.post('/:id/sim/annotate', ...teacher, c.simAnnotate);
|
||||||
|
|
||||||
// Cursor broadcast (all participants)
|
// Cursor broadcast (all participants)
|
||||||
router.post('/:id/cursor', ...auth, c.broadcastCursor);
|
router.post('/:id/cursor', ...auth, c.broadcastCursor);
|
||||||
|
|||||||
@@ -2007,6 +2007,55 @@
|
|||||||
background: #0D0D1A;
|
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 ────────────────────────────────────────── */
|
/* ── Simulation picker modal ────────────────────────────────────────── */
|
||||||
.cr-sim-picker-overlay {
|
.cr-sim-picker-overlay {
|
||||||
position: fixed; inset: 0; z-index: 200;
|
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>
|
<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>
|
</div>
|
||||||
<span class="cr-sim-bar-title" id="cr-sim-bar-title">Симуляция</span>
|
<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) -->
|
<!-- Mode toggle (teacher only) -->
|
||||||
<div class="cr-sim-mode" id="cr-sim-mode-toggle" style="display:none">
|
<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>
|
<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 -->
|
<!-- Blocks student interaction in demo mode -->
|
||||||
<div class="cr-sim-blocker" id="cr-sim-blocker"></div>
|
<div class="cr-sim-blocker" id="cr-sim-blocker"></div>
|
||||||
</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 -->
|
</div><!-- /.cr-board-area -->
|
||||||
<!-- student nav: page nav + follow toggle -->
|
<!-- student nav: page nav + follow toggle -->
|
||||||
<div class="cr-student-nav" id="cr-student-nav" style="display:none">
|
<div class="cr-student-nav" id="cr-student-nav" style="display:none">
|
||||||
@@ -3600,6 +3662,8 @@
|
|||||||
}
|
}
|
||||||
} else if (data.type === 'classroom_sim_mode') {
|
} else if (data.type === 'classroom_sim_mode') {
|
||||||
if (_sessionId == data.sessionId) onSimModeChange(data.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') {
|
} else if (data.type === '_sse_reconnect') {
|
||||||
// SSE reconnected after a drop — re-sync all real-time state to fill the gap
|
// SSE reconnected after a drop — re-sync all real-time state to fill the gap
|
||||||
if (_sessionId) resyncAfterReconnect();
|
if (_sessionId) resyncAfterReconnect();
|
||||||
@@ -6867,6 +6931,9 @@
|
|||||||
const modeToggle = document.getElementById('cr-sim-mode-toggle');
|
const modeToggle = document.getElementById('cr-sim-mode-toggle');
|
||||||
const blocker = document.getElementById('cr-sim-blocker');
|
const blocker = document.getElementById('cr-sim-blocker');
|
||||||
|
|
||||||
|
// Exit annotate mode when sim closes
|
||||||
|
if (_annotateActive) _crApplyAnnotate(false);
|
||||||
|
|
||||||
panel.classList.remove('open');
|
panel.classList.remove('open');
|
||||||
if (modeToggle) modeToggle.style.display = 'none';
|
if (modeToggle) modeToggle.style.display = 'none';
|
||||||
if (blocker) blocker.classList.remove('active');
|
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) {
|
async function crSetSimMode(mode) {
|
||||||
if (!_sessionId) return;
|
if (!_sessionId) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ class Whiteboard {
|
|||||||
// Board theme
|
// Board theme
|
||||||
this._boardTheme = opts.boardTheme || 'chalkboard';
|
this._boardTheme = opts.boardTheme || 'chalkboard';
|
||||||
this._bgNoiseCache = new Map(); // canvas element per theme
|
this._bgNoiseCache = new Map(); // canvas element per theme
|
||||||
|
this._annotateMode = false; // true = transparent bg (draw over simulation)
|
||||||
|
|
||||||
// Compass state machine
|
// Compass state machine
|
||||||
this._compassState = 'idle'; // 'idle'|'setting-radius'|'waiting-arc'|'drawing-arc'
|
this._compassState = 'idle'; // 'idle'|'setting-radius'|'waiting-arc'|'drawing-arc'
|
||||||
@@ -2148,10 +2149,16 @@ class Whiteboard {
|
|||||||
return this._bgNoiseCache.get(theme);
|
return this._bgNoiseCache.get(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAnnotateMode(v) {
|
||||||
|
this._annotateMode = !!v;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
_renderBg(ctx) {
|
_renderBg(ctx) {
|
||||||
const W = this._cssW || 300;
|
const W = this._cssW || 300;
|
||||||
const H = this._cssH || 150;
|
const H = this._cssH || 150;
|
||||||
ctx.clearRect(0, 0, W, H);
|
ctx.clearRect(0, 0, W, H);
|
||||||
|
if (this._annotateMode) return; // transparent bg — simulation shows through
|
||||||
const theme = this._boardTheme || 'chalkboard';
|
const theme = this._boardTheme || 'chalkboard';
|
||||||
|
|
||||||
// Base background
|
// Base background
|
||||||
|
|||||||
Reference in New Issue
Block a user