'use strict'; const { user, isTeacher, isAdmin } = LS.initPage(); window._simQuizAllowed = true; // default; overridden after permission fetch for students LS.showBoardIfAllowed(); /* ════════════════════════════════ SIM CATALOGUE (defined after P_* consts below) ════════════════════════════════ */ let _catFilter = 'all'; var _disabledSimIds = new Set(); let _simModuleDisabled = false; function filterSims(cat, btn) { _catFilter = cat; document.querySelectorAll('.lab-filter').forEach(b => b.classList.remove('active')); btn.classList.add('active'); renderSims(); } function renderSims() { // Контент-движок: мёрж код-реестра поверх legacy SIMS. // Порядок берём из SIMS; для мигрированных id используем манифест реестра; // registry-only записи добавляем в конец. const _reg = (window.LabRegistry ? window.LabRegistry.all() : []); const _regById = {}; _reg.forEach(m => { _regById[m.id] = m; }); const _seen = {}; const _merged = []; SIMS.forEach(s => { _merged.push(s.id && _regById[s.id] ? _regById[s.id] : s); if (s.id) _seen[s.id] = 1; }); _reg.forEach(m => { if (!_seen[m.id]) _merged.push(m); }); const base = _catFilter === 'all' ? _merged : _merged.filter(s => s.cat === _catFilter); const list = base.filter(s => !s.id || !_disabledSimIds.has(s.id)); document.getElementById('sim-grid').innerHTML = list.map(s => `
${window.LabRegistry ? window.LabRegistry.resolvePreview(s) : s.preview}
${s.cat === 'math' ? '∑ Математика' : s.cat === 'chem' ? ' Химия' : s.cat === 'bio' ? ' Биология' : s.cat === 'game' ? ' Игры' : LS.icon('zap',14) + ' Физика'}
${s.title}
${s.desc}
${!s.id ? '
Скоро
' : ''}
`).join(''); if (window.lucide) lucide.createIcons(); } /* ════════════════════════════════ CARD PREVIEW SVGs — вынесены в /js/lab-previews.js (единый источник). Берём их по имени из window.__LabP; SIMS ниже ссылается на эти алиасы. ════════════════════════════════ */ var __LP = window.__LabP || {}; var P_GRAPH = __LP.P_GRAPH, P_TRANSFORM = __LP.P_TRANSFORM, P_TRIANGLE = __LP.P_TRIANGLE, P_CIRCLES = __LP.P_CIRCLES, P_QUADRATIC = __LP.P_QUADRATIC, P_3D = __LP.P_3D, P_PROB = __LP.P_PROB, P_NORMAL = __LP.P_NORMAL, P_TRIGCIRCLE = __LP.P_TRIGCIRCLE, P_PROJECTILE = __LP.P_PROJECTILE, P_PENDULUM = __LP.P_PENDULUM, P_COLLISION = __LP.P_COLLISION, P_CIRCUIT = __LP.P_CIRCUIT, P_MAGNETIC = __LP.P_MAGNETIC, P_FIELD = __LP.P_FIELD, P_LENS = __LP.P_LENS, P_REFRACTION = __LP.P_REFRACTION, P_MIRROR = __LP.P_MIRROR, P_ISOPROCESS = __LP.P_ISOPROCESS, P_GAS = __LP.P_GAS, P_NEWTON = __LP.P_NEWTON, P_SANDBOX = __LP.P_SANDBOX, P_HYDRO = __LP.P_HYDRO, P_KINETICS = __LP.P_KINETICS, P_EQUILIBRIUM = __LP.P_EQUILIBRIUM, P_ELECTROLYSIS = __LP.P_ELECTROLYSIS, P_BOHR = __LP.P_BOHR, P_ORBITALS = __LP.P_ORBITALS, P_PH = __LP.P_PH, P_CHEMSANDBOX = __LP.P_CHEMSANDBOX, P_STOICHIOMETRY = __LP.P_STOICHIOMETRY, P_PERIODIC = __LP.P_PERIODIC, P_CRYSTAL = __LP.P_CRYSTAL, P_CELLDIVISION = __LP.P_CELLDIVISION, P_PHOTOSYNTHESIS = __LP.P_PHOTOSYNTHESIS, P_ANGRYBIRDS = __LP.P_ANGRYBIRDS, P_WAVES = __LP.P_WAVES, P_RADIOACTIVE = __LP.P_RADIOACTIVE, P_HEATENGINE = __LP.P_HEATENGINE, P_GEOMETRY = __LP.P_GEOMETRY, P_RACE = __LP.P_RACE, P_LOGIC = __LP.P_LOGIC, P_QUALANALYSIS = __LP.P_QUALANALYSIS, P_ORGANIC = __LP.P_ORGANIC, P_SOLUTIONS = __LP.P_SOLUTIONS; const SIMS = [ /* ── Математика ── */ { id: 'graph', cat: 'math', title: 'График функции', desc: 'Строй графики функций y = f(x) с параметрами, зумом и курсором координат.', preview: P_GRAPH }, { id: 'graphtransform', cat: 'math', 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: 'Интерактивный треугольник: медианы, высоты, биссектрисы, вписанная и описанная окружности.', preview: P_TRIANGLE }, { id: 'quadratic', cat: 'math', title: 'Корни квадратного уравнения', desc: 'Задай a, b, c ползунками — смотри дискриминант и корни анимированно на числовой оси.', preview: P_QUADRATIC }, { id: 'stereo', cat: 'math', title: 'Стереометрия 3D', desc: 'Вращаемые объёмные фигуры: куб, пирамида, цилиндр, конус с формулами объёма и площади. Сечения, развёртка, вписанные/описанные сферы.', preview: P_3D }, { id: 'probability', cat: 'math', title: 'Теория вероятностей', desc: 'Подброс монеты/кубика N раз — гистограмма частот и закон больших чисел в действии.', preview: P_PROB }, { id: 'trigcircle', cat: 'math', title: 'Тригонометрическая окружность', desc: 'Единичная окружность с sin, cos, tg, ctg. Перетаскивай точку — все функции обновляются мгновенно. График синхронизирован.', preview: P_TRIGCIRCLE }, { id: 'normaldist', cat: 'math', title: 'Нормальное распределение', desc: 'Двигай μ и σ ползунками — колокол Гаусса и площадь под кривой обновляются мгновенно.', preview: P_NORMAL }, /* ── Физика ── */ { id: 'projectile', cat: 'phys', title: 'Бросок тела', desc: 'Задай начальную скорость и угол — симулируй траекторию, дальность и высоту полёта.', preview: P_PROJECTILE }, { id: 'pendulum', cat: 'phys', title: 'Маятник', desc: 'Регулируй длину и угол отклонения — изучай период колебаний и затухание.', preview: P_PENDULUM }, { id: 'collision', cat: 'phys', title: 'Столкновение шаров', desc: 'Упругий и неупругий удар двух тел: законы сохранения импульса и энергии.', preview: P_COLLISION }, { id: 'emfield', cat: 'phys', title: 'Электромагнитные поля', desc: 'Электрическое и магнитное поля в одной симуляции: заряды, токи, силовые линии, эквипотенциали, частица Лоренца.', preview: P_MAGNETIC }, { id: 'circuit', cat: 'phys', title: 'Электрические цепи', desc: 'Конструктор цепей из резисторов и конденсаторов. Законы Ома и Кирхгофа наглядно.', preview: P_CIRCUIT }, { id: 'hydrostatics', cat: 'phys', title: 'Гидростатика', desc: 'Давление жидкости P=ρgh, закон Архимеда, сообщающиеся сосуды, поверхностное натяжение и капиллярность.', preview: P_HYDRO }, { id: 'dynamics', cat: 'phys', title: 'Динамика', desc: 'Законы Ньютона, песочница сил, наклонная плоскость — всё в одном интерактивном модуле.', preview: P_SANDBOX }, { id: 'opticsbench', cat: 'phys', title: 'Оптическая скамья', desc: 'Линза, зеркала и преломление в одной симуляции: формула линзы, зеркальное отражение, закон Снеллиуса, ПВО, дисперсия.', preview: P_LENS }, { id: 'isoprocess', cat: 'phys', title: 'Изопроцессы', desc: 'PV-диаграмма для четырёх изопроцессов идеального газа. Расчёт работы, теплоты и внутренней энергии.', preview: P_ISOPROCESS }, { id: 'waves', cat: 'phys', title: 'Волны и звук', desc: 'Поперечные и продольные волны, суперпозиция, стоячие волны. Частота, амплитуда, фаза, гармоники.', preview: P_WAVES }, { id: 'radioactive', cat: 'phys', title: 'Радиоактивный распад', desc: 'Период полураспада, цепочки распадов, активность. Визуализация ядер + кривая N(t). Радиоуглеродное датирование.', preview: P_RADIOACTIVE }, { id: 'race', cat: 'phys', title: 'Гонка с задачами', desc: 'Кинематика 1D: встреча, догон, кто первый. Реши задачу — проверь анимацией и графиком x(t).', preview: P_RACE }, { id: 'heatengine', cat: 'phys', title: 'Тепловые двигатели', desc: 'Циклы Карно, Отто, Дизеля, Брайтона. PV-диаграмма, поршень, КПД.', preview: P_HEATENGINE }, { id: 'logic', cat: 'phys', title: 'Логические схемы', desc: 'Конструктор цифровых схем: И/ИЛИ/НЕ/XOR, триггеры, сумматоры. Авто-таблица истинности.', preview: P_LOGIC }, /* ── Химия / Молекулярная физика ── */ { id: 'molphys', cat: 'chem', title: 'Молекулярная физика', desc: 'Идеальный газ, броуновское движение, агрегатные состояния и диффузия — всё в одном модуле.', preview: P_GAS }, { id: 'chemistry', cat: 'chem', title: 'Химические реакции', desc: 'Кинетика реакций, металл + кислота в колбе, ОВР с переносом электронов, ионный обмен — всё в одном модуле.', preview: P_KINETICS }, { id: 'equilibrium', cat: 'chem', title: 'Химическое равновесие', desc: 'Прямая и обратная реакция, принцип Ле Шателье: изменяй T, P, концентрацию и наблюдай сдвиг.', preview: P_EQUILIBRIUM }, { id: 'electrolysis', cat: 'chem', title: 'Электролиз', desc: 'Катод и анод в растворе электролита: движение ионов, выделение газа, закон Фарадея.', preview: P_ELECTROLYSIS }, /* ── Скоро: Атомная структура ── */ { id: 'bohratom', cat: 'chem', title: 'Атом Бора', desc: 'Электроны на орбитах, квантование энергии, эмиссия и поглощение фотонов при переходах.', preview: P_BOHR }, { id: 'orbitals', cat: 'chem', title: 'Молекулярные орбитали', desc: 'H₂, H₂O — ковалентная связь, перекрывание орбиталей, 3D-визуализация электронных облаков.', preview: P_ORBITALS }, /* ── Скоро: Визуальная химия ── */ { id: 'titration', cat: 'chem', title: 'pH и кривая титрования', desc: 'Добавляй кислоту или щёлочь — наблюдай изменение pH, цвет раствора и кривую нейтрализации.', preview: P_PH }, { id: 'chemsandbox', cat: 'chem', title: 'Химическая песочница', desc: 'Смешивай реагенты, наблюдай реакции: осадки, газы, изменение цвета. Свободное экспериментирование.', preview: P_CHEMSANDBOX }, { id: 'stoichiometry', cat: 'chem', title: 'Стехиометрия', desc: 'Расчёты по уравнениям: масса, моль, объём. Лимитирующий реагент, выход. 10 реакций.', preview: P_STOICHIOMETRY }, { id: 'crystal', cat: 'chem', title: 'Кристаллическая решётка', desc: 'NaCl, алмаз, металл — интерактивная 3D-решётка, типы связей, вращение структуры.', preview: P_CRYSTAL }, { id: 'qualanalysis', cat: 'chem', title: 'Качественный анализ', desc: 'Определяй катионы и анионы качественными реакциями: осадки, газы, пламя. Два режима: guided и свободный эксперимент.', preview: P_QUALANALYSIS }, { id: 'periodic', cat: 'chem', title: 'Периодическая таблица', desc: '118 элементов: подсветка по типу/блоку, карточка элемента, боровские оболочки, графики свойств.', preview: P_PERIODIC }, { id: 'organic', cat: 'chem', title: 'Органическая химия', desc: 'Конструктор молекул с проверкой валентности, гомологические ряды с таблицей свойств, качественные реакции (бромная вода, KMnO₄, зеркало Толленса, Cu(OH)₂, FeCl₃, Na).', preview: P_ORGANIC }, { id: 'solutions', cat: 'chem', title: 'Растворы', desc: 'Калькулятор раствора: ω, ν, C_M, плотность. Разбавление и смешивание с визуализацией. Кривые растворимости S(T) для 8 веществ + задача на перекристаллизацию.', preview: P_SOLUTIONS }, /* ── Биология ── */ { id: 'celldivision', cat: 'bio', title: 'Деление клетки', desc: 'Митоз и мейоз: анимированные фазы, хромосомы, веретено деления, ядерная оболочка.', preview: P_CELLDIVISION }, { id: 'photosynthesis', cat: 'bio', title: 'Фотосинтез и дыхание', desc: 'Световые реакции в тилакоидах, цикл Кальвина, митохондриальное дыхание — молекулярная анимация.', preview: P_PHOTOSYNTHESIS }, /* ── Игры ── */ { id: 'angrybirds', cat: 'game', title: 'Angry Birds Physics', desc: 'Запускай птиц из рогатки, разрушай блоки, побеждай свиней. Реальная физика: гравитация, ветер, импульс. 6 уровней.', preview: P_ANGRYBIRDS }, ]; var _theoryOpen = false; function toggleTheory() { _theoryOpen = !_theoryOpen; document.getElementById('theory-panel').classList.toggle('open', _theoryOpen); const btn = document.getElementById('theory-toggle'); btn.style.background = _theoryOpen ? 'rgba(155,93,229,0.15)' : ''; btn.style.borderColor = _theoryOpen ? 'var(--violet)' : ''; btn.style.color = _theoryOpen ? 'var(--violet)' : ''; } function loadTheory(simId) { // Контент-движок: теория мигрированных симуляций берётся из манифеста реестра. const _rm = window.LabRegistry ? window.LabRegistry.get(simId) : null; const t = (_rm && _rm.theory) ? _rm.theory : THEORY[simId]; const el = document.getElementById('theory-content'); // Фаза 1: панель учебных заданий (если есть для этой симуляции) const tasksHtml = (window.LabTasks && LabTasks.has(simId)) ? LabTasks.mountHtml(simId) : ''; const tbtn = document.getElementById('theory-toggle'); if (tbtn) tbtn.classList.toggle('lt-has', !!tasksHtml); if (!t) { el.innerHTML = '
Теория для этой симуляции пока не добавлена
' + tasksHtml; if (tasksHtml) LabTasks.afterMount(simId); return; } let html = `
${LS.icon('book-open',16)} ${t.title}
`; for (const s of t.sections) { html += '
'; if (s.head) html += `
${s.head}
`; if (s.formula) html += `
`; if (s.text) html += `
${s.text}
`; if (s.vars) html += `
${s.vars.map(([v,d]) => `
${v} — ${d}
`).join('')}
`; html += '
'; } el.innerHTML = html + tasksHtml; // render KaTeX formulas (теория) el.querySelectorAll('.tp-formula[data-formula]').forEach(div => { try { katex.render(div.dataset.formula, div, { displayMode: true, throwOnError: false }); } catch(e) { div.textContent = div.dataset.formula; } }); if (tasksHtml) LabTasks.afterMount(simId); } /* ── Контент-движок, Фаза 5: чип «Связано с программой» ────────────────── Подтягивает курикулумные связи симуляции (GET /api/lab/sims/:id/related) и рендерит чипы-ссылки рядом с заголовком симуляции. Самодостаточно: создаёт контейнер #sim-related динамически (без правок lab.html/CSS — меньше риск конфликта с параллельными сессиями). Тихо прячется, если связей нет/ошибка. */ var _LAB_LINK_ICON = ''; function _labRelEsc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } function _ensureRelatedHost() { var host = document.getElementById('sim-related'); if (host) return host; host = document.createElement('div'); host.id = 'sim-related'; host.style.cssText = 'display:none;align-items:center;gap:6px;flex-wrap:wrap;margin-left:14px;min-width:0'; var title = document.getElementById('sim-topbar-title'); if (title && title.parentNode) title.parentNode.insertBefore(host, title.nextSibling); return host; } function _loadRelated(simId) { var host = _ensureRelatedHost(); host.style.display = 'none'; host.innerHTML = ''; if (!window.LS || !LS.api) return; LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related') .then(function (data) { var links = (data && data.links) || {}; var all = [].concat(links.textbook || [], links.topic || [], links.kmap || [], links.question || []); if (!all.length) return; var chipBase = 'display:inline-flex;align-items:center;gap:4px;font-size:.72rem;padding:3px 9px;border-radius:999px;'; var html = '' + _LAB_LINK_ICON + ' Связано с программой'; all.forEach(function (l) { var label = _labRelEsc(l.label || (l.kind + ':' + l.ref_id)); if (l.href) { html += '' + label + ''; } else { html += '' + label + ''; } }); host.innerHTML = html; host.style.display = 'flex'; if (window.lucide) lucide.createIcons(); }) .catch(function () { /* нет связей или ошибка — чип просто не показываем */ }); } window._loadRelated = _loadRelated; /* ── embed mode + auto-open from ?sim= ── */ const _qp = new URLSearchParams(location.search); var _embedMode = _qp.get('embed') === '1'; var _autoSim = _qp.get('sim'); /* ── Sim state relay (embed mode only) ──────────────────────────────── */ // Map simId → { getState, applyState } registered by openSim handlers const _simStateRegistry = {}; function _registerSimState(simId, getState, applyState) { _simStateRegistry[simId] = { getState, applyState }; } let _lastEmittedState = null; let _stateEmitInterval = null; function _startStateEmit(simId) { if (_stateEmitInterval) clearInterval(_stateEmitInterval); _lastEmittedState = null; _stateEmitInterval = setInterval(() => { const reg = _simStateRegistry[simId]; if (!reg) return; try { const state = reg.getState(); const json = JSON.stringify(state); if (json === _lastEmittedState) return; _lastEmittedState = json; window.parent.postMessage({ type: 'sim_state', simId, state }, '*'); } catch {} }, 400); } function _stopStateEmit() { if (_stateEmitInterval) { clearInterval(_stateEmitInterval); _stateEmitInterval = null; } _lastEmittedState = null; } // Receive apply_sim_state from parent (students) window.addEventListener('message', e => { if (!_embedMode) return; const d = e.data; if (!d || d.type !== 'apply_sim_state') return; const reg = _simStateRegistry[_autoSim]; if (!reg) return; try { reg.applyState(d.state); _lastEmittedState = JSON.stringify(d.state); // suppress echo } catch {} }); if (_embedMode) { document.querySelector('.sidebar').style.display = 'none'; document.querySelector('.sb-content').style.marginLeft = '0'; document.querySelector('.app-layout').classList.add('embed-mode'); document.getElementById('lab-home').style.display = 'none'; document.getElementById('theory-toggle').style.display = 'none'; if (_autoSim) { document.getElementById('lab-sim').classList.add('open'); document.querySelector('.sim-topbar').style.display = 'none'; // defer until all external scripts are loaded window.addEventListener('load', () => openSim(_autoSim)); } } else { /* init — fetch sim settings + permissions in parallel, then render */ const _permFetch = (!isTeacher && !isAdmin) ? LS.api('/api/permissions/me').catch(() => null) : Promise.resolve(null); Promise.all([ LS.api('/api/settings/sims').catch(() => ({})), _permFetch, ]).then(([cfg, permData]) => { _simModuleDisabled = cfg.module_disabled || false; _disabledSimIds = new Set(cfg.disabled_ids || []); // check simulations.access for students if (!isTeacher && !isAdmin && permData) { const p = permData.permissions?.find(p => p.key === 'simulations.access'); if (p && p.effective === false) { document.getElementById('sim-grid').innerHTML = `
Доступ к симуляциям закрыт
Администратор ограничил доступ к лаборатории
`; return; } // store quiz permission for later use const qp = permData.permissions?.find(p => p.key === 'simulations.quiz'); window._simQuizAllowed = !qp || qp.effective !== false; } else { window._simQuizAllowed = true; } if (_simModuleDisabled) { document.getElementById('sim-grid').innerHTML = `
Модуль симуляций отключён
Администратор временно отключил лабораторию
`; } else { renderSims(); if (_autoSim) openSim(_autoSim); // hash-router: activate sim from URL fragment after catalogue renders else _activateFromHash(); } }); lucide.createIcons(); LS.notif.init(); } /* ─── Hash router for sim deep-links ───────────────────────────────────── URL pattern: /lab#sim/ matches SIMS[i].id (e.g. 'projectile', 'graph', 'chemsandbox'). F5 restores sim. Browser back/forward switches between sims. Click on sim-card updates URL via wrapped openSim. ──────────────────────────────────────────────────────────────────────── */ // Build valid-id set from SIMS catalogue (filters out "coming soon" entries) const _SIM_HASH_MAP = {}; SIMS.forEach(function(s) { if (s.id) { _SIM_HASH_MAP[s.id] = s.id; } }); // backward-compat aliases: old URLs redirect to unified emfield sim _SIM_HASH_MAP['magnetic'] = 'magnetic'; _SIM_HASH_MAP['coulomb'] = 'coulomb'; // backward-compat aliases: old optics sims redirect to opticsbench _SIM_HASH_MAP['thinlens'] = 'opticsbench'; _SIM_HASH_MAP['mirrors'] = 'opticsbench'; _SIM_HASH_MAP['refraction'] = 'opticsbench'; var _routerNavigating = false; function _activateFromHash() { var m = (location.hash || '').match(/^#sim\/([\w-]+)/); if (!m) return false; var simName = m[1]; if (!_SIM_HASH_MAP[simName]) { // eslint-disable-next-line no-console window.console && window.console.warn('lab-router: unknown sim', simName); return false; } openSim(simName); return true; } // Intercept openSim to push URL hash on user-initiated navigation var _origOpenSim = openSim; openSim = function(id) { _origOpenSim(id); if (!_routerNavigating && !_embedMode) { var baseId = id.includes(':') ? id.split(':')[0] : id; if (_SIM_HASH_MAP[baseId]) { _routerNavigating = true; location.hash = '#sim/' + baseId; // use setTimeout so hashchange fires after flag is set setTimeout(function() { _routerNavigating = false; }, 0); } } }; /* ─── Sim Fade Transition + View Transitions API ───────────────────────── Wraps openSim with a fade-out (150ms) → swap → fade-in (200ms) sequence. If document.startViewTransition is available it is used for GPU-composited cross-fade; otherwise the manual .sim-fading CSS class is toggled. The hash-router wrap above runs synchronously during the transition so URL updates are not delayed. ──────────────────────────────────────────────────────────────────────── */ var _hashRouterOpenSim = openSim; // reference after hash-router wrap openSim = function(id) { var labSim = document.getElementById('lab-sim'); if (!labSim) { _hashRouterOpenSim(id); return; } function _doSwitch() { labSim.classList.add('sim-fading'); setTimeout(function() { _hashRouterOpenSim(id); labSim.classList.remove('sim-fading'); }, 150); } if (typeof document.startViewTransition === 'function' && !_embedMode) { document.startViewTransition(function() { _hashRouterOpenSim(id); }); } else { _doSwitch(); } }; // Intercept closeSim to clear hash when returning to home grid var _origCloseSim = closeSim; closeSim = function() { _origCloseSim(); if (!_embedMode) { _routerNavigating = true; history.pushState(null, '', location.pathname + location.search); setTimeout(function() { _routerNavigating = false; }, 0); } }; // Browser back/forward navigation window.addEventListener('hashchange', function() { if (_routerNavigating) return; var hasHash = _activateFromHash(); if (!hasHash && document.getElementById('lab-sim').classList.contains('open')) { _origCloseSim(); } });