merge: feature/lab-content-engine → master
Контент-движок лаборатории (фазы 0-5): LabRegistry, data-driven регистрация, вынос тел в labs-bodies.html, ленивая загрузка кода, БД-каталог lab_sims + API + админка, курикулумные связи lab_sim_links + двусторонняя навигация. Плюс накопленная работа параллельных сессий (chemistry-8, phys7, biochem, optics). Разрешение конфликтов: frontend/lab.html — версия feature (контент-движок); opticsbench.js / seed_biochem_challenges.js / BIOCHEM_UPGRADE.md / biochem-pathways-plan.md — версия master (более свежая работа парал. сессий). Тесты: 160, 157 pass, 3 fail (pre-existing baseline auth.test.js). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
/*
|
||||
* LabLoader — ленивый загрузчик кода симуляций (контент-движок, Фаза 3).
|
||||
*
|
||||
* Тяжёлый код симуляций (~2.5 МБ) и three.js (~600 КБ) больше НЕ грузятся на старте.
|
||||
* При открытии симуляции LabLoader.ensure(id) подгружает её файлы (по манифесту
|
||||
* window.SIM_DEPS из _sim_deps.js) и, при необходимости, three.js — затем резолвит.
|
||||
*
|
||||
* Гарантия корректности (самовосстановление): если после загрузки указанных файлов
|
||||
* глобальная open-функция (SIM_DEPS[id].open, напр. "_openPendulum") всё ещё не
|
||||
* определена, грузятся ВСЕ ленивые файлы (window.LAB_LAZY_FILES). Поэтому ошибка в
|
||||
* манифесте не может «сломать» симуляцию — в худшем случае грузится больше файлов
|
||||
* (поведение как до Фазы 3). Манифест лишь оптимизирует объём загрузки.
|
||||
*
|
||||
* Все загрузки кешируются (по URL) и дедуплицируются.
|
||||
*/
|
||||
(function () {
|
||||
var BASE = '/js/labs/';
|
||||
var THREE_URL = 'https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.min.js';
|
||||
var _cache = {}; // url -> Promise
|
||||
var _allLoaded = false;
|
||||
|
||||
function loadScript(url) {
|
||||
if (_cache[url]) return _cache[url];
|
||||
_cache[url] = new Promise(function (resolve, reject) {
|
||||
var s = document.createElement('script');
|
||||
s.src = url;
|
||||
s.async = false; // сохранить порядок при добавлении нескольких сразу
|
||||
s.onload = function () { resolve(url); };
|
||||
s.onerror = function () { delete _cache[url]; reject(new Error('LabLoader: не удалось загрузить ' + url)); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return _cache[url];
|
||||
}
|
||||
|
||||
function ensureThree() {
|
||||
if (typeof window.THREE !== 'undefined') return Promise.resolve();
|
||||
return loadScript(THREE_URL);
|
||||
}
|
||||
|
||||
function loadFiles(files) {
|
||||
return Promise.all((files || []).map(function (f) { return loadScript(BASE + f); }));
|
||||
}
|
||||
|
||||
function loadAllLazy() {
|
||||
if (_allLoaded) return Promise.resolve();
|
||||
var list = window.LAB_LAZY_FILES || [];
|
||||
return loadFiles(list).then(function () { _allLoaded = true; });
|
||||
}
|
||||
|
||||
// ensure(id): загрузить всё необходимое для симуляции id, вернуть Promise.
|
||||
function ensure(id) {
|
||||
var dep = (window.SIM_DEPS && window.SIM_DEPS[id]) || null;
|
||||
if (!dep) {
|
||||
// нет манифеста для id — безопасно грузим всё
|
||||
return loadAllLazy();
|
||||
}
|
||||
var p = dep.three ? ensureThree() : Promise.resolve();
|
||||
return p
|
||||
.then(function () { return loadFiles(dep.files); })
|
||||
.then(function () {
|
||||
var openName = dep.open;
|
||||
if (openName && typeof window[openName] !== 'function') {
|
||||
if (window.console) console.warn('[LabLoader] самовосстановление для "' + id + '": ' + openName + ' не найдена после загрузки ' + JSON.stringify(dep.files) + ' — гружу все ленивые файлы');
|
||||
return loadAllLazy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.LabLoader = {
|
||||
ensure: ensure,
|
||||
ensureThree: ensureThree,
|
||||
loadScript: loadScript,
|
||||
loadAllLazy: loadAllLazy
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Контент-движок, Фаза 1 — data-driven регистрация ВСЕХ симуляций в LabRegistry.
|
||||
*
|
||||
* Вместо ручного переписывания 40 манифестов модуль строит их из единых источников:
|
||||
* - метаданные (id/cat/title/desc) и preview — из массива SIMS (lab-glue.js);
|
||||
* - теория — из объекта THEORY (lab-init.js);
|
||||
* - поведение open(ctx) — из карты OPEN ниже (обёртки над глобальными _openXxx).
|
||||
* Это структурно гарантирует паритет с прежним каталогом и диспетчеризацией.
|
||||
*
|
||||
* Подключается ПОСЛЕДНИМ среди labs-скриптов (defer), поэтому SIMS, THEORY и все
|
||||
* _openXxx уже определены. Останов/закрытие симуляций по-прежнему выполняет
|
||||
* «дробовик» _pauseAllSims()/closeSim() (точный паритет) — поэтому stop/destroy
|
||||
* в манифестах не задаются на этом этапе.
|
||||
*
|
||||
* После регистрации if-цепочка в openSim() становится мёртвой и удалена.
|
||||
*
|
||||
* В Фазе 1 заменил пилотный _pilots.js. SIMS/THEORY остаются источниками данных
|
||||
* (SIMS → БД в Фазе 4, THEORY сворачивается в манифесты позже).
|
||||
*/
|
||||
(function () {
|
||||
if (!window.LabRegistry) return;
|
||||
if (typeof SIMS === 'undefined') return;
|
||||
var R = window.LabRegistry;
|
||||
var T = (typeof THEORY !== 'undefined') ? THEORY : {};
|
||||
|
||||
// id -> open(ctx). ctx.arg — параметр deep-link (после двоеточия): stereo:cube и т.п.
|
||||
var OPEN = {
|
||||
graph: function (c) { _openGraph(); },
|
||||
projectile: function (c) { _openProjectile(); },
|
||||
collision: function (c) { _openCollision(); },
|
||||
triangle: function (c) { _openTriangle(); },
|
||||
trigcircle: function (c) { _openTrigCircle(); },
|
||||
emfield: function (c) { _openEMField(c.arg || 'E'); },
|
||||
molphys: function (c) { _openMolPhys(c.arg); },
|
||||
circuit: function (c) { _openCircuit(); },
|
||||
chemistry: function (c) { _openChemistry(c.arg); },
|
||||
dynamics: function (c) { _openDynamics(c.arg); },
|
||||
crystal: function (c) { _openCrystal(); },
|
||||
orbitals: function (c) { _openOrbitals(); },
|
||||
stereo: function (c) { _openStereo(c.arg); },
|
||||
chemsandbox: function (c) { _openChemSandbox(); },
|
||||
celldivision: function (c) { _openCellDivision(); },
|
||||
photosynthesis: function (c) { _openPhotosynthesis(); },
|
||||
angrybirds: function (c) { _openAngryBirds(); },
|
||||
quadratic: function (c) { _openQuadratic(); },
|
||||
normaldist: function (c) { _openNormalDist(); },
|
||||
graphtransform: function (c) { _openGraphTransform(); },
|
||||
pendulum: function (c) { _openPendulum(); },
|
||||
equilibrium: function (c) { _openEquilibrium(); },
|
||||
opticsbench: function (c) { _openOpticsBench(c.arg || 'lens'); },
|
||||
isoprocess: function (c) { _openIsoprocess(); },
|
||||
titration: function (c) { _openTitration(); },
|
||||
probability: function (c) { _openProbability(); },
|
||||
bohratom: function (c) { _openBohrAtom(); },
|
||||
electrolysis: function (c) { _openElectrolysis(); },
|
||||
race: function (c) { _openRace(); },
|
||||
waves: function (c) { _openWaves(); },
|
||||
hydrostatics: function (c) { _openHydro(c.arg); },
|
||||
radioactive: function (c) { _openRadioactive(); },
|
||||
geometry: function (c) { _openGeometry(); },
|
||||
logic: function (c) { _openLogic(); },
|
||||
heatengine: function (c) { _openHeatEngine(); },
|
||||
stoichiometry: function (c) { _openStoich(); },
|
||||
qualanalysis: function (c) { _openQualAnalysis(); },
|
||||
periodic: function (c) { _openPeriodic(); },
|
||||
organic: function (c) { _openOrganic(); },
|
||||
solutions: function (c) { _openSolutions(); }
|
||||
};
|
||||
|
||||
SIMS.forEach(function (s) {
|
||||
if (!s.id) return; // "Скоро" — карточка без id
|
||||
var open = OPEN[s.id];
|
||||
if (!open) { // подстраховка: незамапленный id оставляем legacy-пути
|
||||
if (window.console) console.warn('[LabRegistry] нет open() для', s.id);
|
||||
return;
|
||||
}
|
||||
R.register({
|
||||
id: s.id,
|
||||
cat: s.cat,
|
||||
title: s.title,
|
||||
desc: s.desc,
|
||||
preview: s.preview, // уже готовая SVG-строка (P_* вычислены в SIMS)
|
||||
theory: T[s.id] || null,
|
||||
// Фаза 3: ленивая загрузка кода. LabLoader.ensure(id) подгружает файлы
|
||||
// симуляции (+ three.js при необходимости), затем выполняется raw-open.
|
||||
// Если LabLoader недоступен — открываем синхронно как раньше (фолбэк).
|
||||
open: (function (rawOpen, simId) {
|
||||
return function (c) {
|
||||
if (window.LabLoader && window.LabLoader.ensure) {
|
||||
return window.LabLoader.ensure(simId).then(function () { rawOpen(c); });
|
||||
}
|
||||
rawOpen(c);
|
||||
};
|
||||
})(open, s.id)
|
||||
// stop/destroy: глобальный «дробовик» _pauseAllSims()/closeSim() — паритет
|
||||
});
|
||||
});
|
||||
|
||||
// Алиасы deep-link → канонический id[:arg]. Диспетчер openSim() нормализует их
|
||||
// перед обращением к реестру (карточек у алиасов нет — только прямые ссылки).
|
||||
window.LAB_SIM_ALIASES = {
|
||||
magnetic: 'emfield:B',
|
||||
coulomb: 'emfield:E',
|
||||
thinlens: 'opticsbench:lens',
|
||||
mirrors: 'opticsbench:mirror',
|
||||
refraction: 'opticsbench:refraction'
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
/*
|
||||
* LabRegistry — единый реестр симуляций лаборатории (контент-движок).
|
||||
*
|
||||
* Цель: симуляции описываются декларативным манифестом и сами себя регистрируют,
|
||||
* вместо захардкоженных массивов (SIMS), if-цепочек (openSim) и объектов (THEORY).
|
||||
*
|
||||
* Манифест:
|
||||
* {
|
||||
* id: 'pendulum', // уникальный, без ':arg'
|
||||
* cat: 'phys', // math | phys | chem | bio | game
|
||||
* title: 'Маятник',
|
||||
* desc: 'Колебания, период…',
|
||||
* preview: string | function(), // SVG-разметка карточки (функция вычисляется лениво)
|
||||
* theory: { title, sections[] },// объект для панели теории (как в THEORY)
|
||||
* bodyId: 'sim-pendulum', // (опц.) id тела; mount() — для ленивого создания DOM (Фаза 2)
|
||||
* mount: function(host){}, // (опц.) ленивое монтирование тела
|
||||
* open: function(ctx){}, // ctx = { id, arg } — открыть/инициализировать
|
||||
* stop: function(){}, // (опц.) остановить анимации (не разрушая)
|
||||
* destroy: function(){}, // (опц.) полностью закрыть; по умолчанию == stop
|
||||
* subject, grade, topics // (опц.) курикулумные поля (Фаза 5)
|
||||
* }
|
||||
*
|
||||
* Загружается ПЕРВЫМ среди labs-скриптов, чтобы window.LabRegistry существовал
|
||||
* к моменту исполнения тел остальных модулей.
|
||||
*/
|
||||
(function () {
|
||||
var _list = []; // манифесты в порядке регистрации
|
||||
var _byId = {}; // id -> манифест
|
||||
var _active = null; // текущая открытая симуляция
|
||||
|
||||
function _baseId(id) {
|
||||
return id == null ? id : String(id).split(':')[0];
|
||||
}
|
||||
|
||||
function register(m) {
|
||||
if (!m || !m.id) return null;
|
||||
if (Object.prototype.hasOwnProperty.call(_byId, m.id)) {
|
||||
// перерегистрация: заменить на месте, сохранив позицию
|
||||
for (var i = 0; i < _list.length; i++) {
|
||||
if (_list[i].id === m.id) { _list[i] = m; break; }
|
||||
}
|
||||
} else {
|
||||
_list.push(m);
|
||||
}
|
||||
_byId[m.id] = m;
|
||||
return m;
|
||||
}
|
||||
|
||||
function get(id) {
|
||||
var b = _baseId(id);
|
||||
return Object.prototype.hasOwnProperty.call(_byId, b) ? _byId[b] : null;
|
||||
}
|
||||
|
||||
function has(id) { return !!get(id); }
|
||||
|
||||
function all() { return _list.slice(); }
|
||||
|
||||
function setActive(m) { _active = m || null; }
|
||||
|
||||
function stopActive() {
|
||||
if (_active && typeof _active.stop === 'function') {
|
||||
try { _active.stop(); } catch (e) { /* noop */ }
|
||||
}
|
||||
}
|
||||
|
||||
function destroyActive() {
|
||||
if (_active) {
|
||||
if (typeof _active.destroy === 'function') {
|
||||
try { _active.destroy(); } catch (e) { /* noop */ }
|
||||
} else if (typeof _active.stop === 'function') {
|
||||
try { _active.stop(); } catch (e) { /* noop */ }
|
||||
}
|
||||
}
|
||||
_active = null;
|
||||
}
|
||||
|
||||
function active() { return _active; }
|
||||
|
||||
// Разрешить preview (строка или функция) в готовую разметку.
|
||||
function resolvePreview(m) {
|
||||
if (!m) return '';
|
||||
var p = m.preview;
|
||||
if (typeof p === 'function') {
|
||||
try { return p() || ''; } catch (e) { return ''; }
|
||||
}
|
||||
return p || '';
|
||||
}
|
||||
|
||||
window.LabRegistry = {
|
||||
register: register,
|
||||
get: get,
|
||||
has: has,
|
||||
all: all,
|
||||
setActive: setActive,
|
||||
stopActive: stopActive,
|
||||
destroyActive: destroyActive,
|
||||
active: active,
|
||||
resolvePreview: resolvePreview
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,300 @@
|
||||
'use strict';
|
||||
/* Контент-движок, Фаза 3 — манифест зависимостей симуляций (СГЕНЕРИРОВАН).
|
||||
id -> { open: имя глобальной _openX, files: [ленивые файлы], three: нужен ли three.js }.
|
||||
Файлы загружаются лениво по клику (см. _loader.js). three.js — только для 3D-симуляций.
|
||||
Самовосстановление в _loader: если после загрузки open-функция не определена,
|
||||
грузятся ВСЕ ленивые файлы -> корректность не зависит от точности манифеста.
|
||||
Регенерация: node tools/gen-sim-deps.js (см. CONTEXT). НЕ редактировать вручную. */
|
||||
window.SIM_DEPS = {
|
||||
"graph": {
|
||||
"open": "_openGraph",
|
||||
"files": [],
|
||||
"three": false
|
||||
},
|
||||
"projectile": {
|
||||
"open": "_openProjectile",
|
||||
"files": [
|
||||
"projectile.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"collision": {
|
||||
"open": "_openCollision",
|
||||
"files": [
|
||||
"collision.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"triangle": {
|
||||
"open": "_openTriangle",
|
||||
"files": [
|
||||
"triangle.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"trigcircle": {
|
||||
"open": "_openTrigCircle",
|
||||
"files": [
|
||||
"trigcircle.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"emfield": {
|
||||
"open": "_openEMField",
|
||||
"files": [
|
||||
"emfield.js",
|
||||
"logic.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"molphys": {
|
||||
"open": "_openMolPhys",
|
||||
"files": [
|
||||
"brownian.js",
|
||||
"diffusion.js",
|
||||
"gas.js",
|
||||
"states.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"circuit": {
|
||||
"open": "_openCircuit",
|
||||
"files": [
|
||||
"circuit.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"chemistry": {
|
||||
"open": "_openChemistry",
|
||||
"files": [
|
||||
"circuit.js",
|
||||
"flask.js",
|
||||
"ionexchange.js",
|
||||
"reactions.js",
|
||||
"redox.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"dynamics": {
|
||||
"open": "_openDynamics",
|
||||
"files": [
|
||||
"forcesandbox.js",
|
||||
"newton.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"crystal": {
|
||||
"open": "_openCrystal",
|
||||
"files": [
|
||||
"crystal.js"
|
||||
],
|
||||
"three": true
|
||||
},
|
||||
"orbitals": {
|
||||
"open": "_openOrbitals",
|
||||
"files": [
|
||||
"orbitals.js"
|
||||
],
|
||||
"three": true
|
||||
},
|
||||
"stereo": {
|
||||
"open": "_openStereo",
|
||||
"files": [
|
||||
"stereo.js"
|
||||
],
|
||||
"three": true
|
||||
},
|
||||
"chemsandbox": {
|
||||
"open": "_openChemSandbox",
|
||||
"files": [
|
||||
"chemsandbox.js",
|
||||
"collision.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"celldivision": {
|
||||
"open": "_openCellDivision",
|
||||
"files": [
|
||||
"celldivision.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"photosynthesis": {
|
||||
"open": "_openPhotosynthesis",
|
||||
"files": [
|
||||
"photosynthesis.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"angrybirds": {
|
||||
"open": "_openAngryBirds",
|
||||
"files": [
|
||||
"angrybirds.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"quadratic": {
|
||||
"open": "_openQuadratic",
|
||||
"files": [
|
||||
"quadratic.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"normaldist": {
|
||||
"open": "_openNormalDist",
|
||||
"files": [
|
||||
"normaldist.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"graphtransform": {
|
||||
"open": "_openGraphTransform",
|
||||
"files": [
|
||||
"graphtransform.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"pendulum": {
|
||||
"open": "_openPendulum",
|
||||
"files": [
|
||||
"pendulum.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"equilibrium": {
|
||||
"open": "_openEquilibrium",
|
||||
"files": [
|
||||
"equilibrium.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"opticsbench": {
|
||||
"open": "_openOpticsBench",
|
||||
"files": [
|
||||
"opticsbench.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"isoprocess": {
|
||||
"open": "_openIsoprocess",
|
||||
"files": [
|
||||
"isoprocess.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"titration": {
|
||||
"open": "_openTitration",
|
||||
"files": [
|
||||
"titration.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"probability": {
|
||||
"open": "_openProbability",
|
||||
"files": [
|
||||
"probability.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"bohratom": {
|
||||
"open": "_openBohrAtom",
|
||||
"files": [
|
||||
"bohratom.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"electrolysis": {
|
||||
"open": "_openElectrolysis",
|
||||
"files": [
|
||||
"electrolysis.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"race": {
|
||||
"open": "_openRace",
|
||||
"files": [
|
||||
"race.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"waves": {
|
||||
"open": "_openWaves",
|
||||
"files": [
|
||||
"waves.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"hydrostatics": {
|
||||
"open": "_openHydro",
|
||||
"files": [
|
||||
"hydrostatics.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"radioactive": {
|
||||
"open": "_openRadioactive",
|
||||
"files": [
|
||||
"radioactive.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"geometry": {
|
||||
"open": "_openGeometry",
|
||||
"files": [
|
||||
"geometry.js",
|
||||
"triangle.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"logic": {
|
||||
"open": "_openLogic",
|
||||
"files": [
|
||||
"logic.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"heatengine": {
|
||||
"open": "_openHeatEngine",
|
||||
"files": [
|
||||
"heatengine.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"stoichiometry": {
|
||||
"open": "_openStoich",
|
||||
"files": [
|
||||
"stoichiometry.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"qualanalysis": {
|
||||
"open": "_openQualAnalysis",
|
||||
"files": [
|
||||
"qualanalysis.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"periodic": {
|
||||
"open": "_openPeriodic",
|
||||
"files": [
|
||||
"_periodic_data.js",
|
||||
"periodic.js"
|
||||
],
|
||||
"three": true
|
||||
},
|
||||
"organic": {
|
||||
"open": "_openOrganic",
|
||||
"files": [
|
||||
"organic.js"
|
||||
],
|
||||
"three": false
|
||||
},
|
||||
"solutions": {
|
||||
"open": "_openSolutions",
|
||||
"files": [
|
||||
"solutions.js"
|
||||
],
|
||||
"three": false
|
||||
}
|
||||
};
|
||||
window.LAB_LAZY_FILES = ["angrybirds.js","bohratom.js","brownian.js","celldivision.js","chemsandbox.js","circuit.js","collision.js","crystal.js","diffusion.js","electrolysis.js","emfield.js","equilibrium.js","flask.js","forcesandbox.js","gas.js","geometry.js","graphtransform.js","heatengine.js","hydrostatics.js","ionexchange.js","isoprocess.js","logic.js","newton.js","normaldist.js","opticsbench.js","orbitals.js","organic.js","pendulum.js","periodic.js","photosynthesis.js","probability.js","projectile.js","quadratic.js","qualanalysis.js","race.js","radioactive.js","reactions.js","redox.js","solutions.js","states.js","stereo.js","stoichiometry.js","titration.js","triangle.js","trigcircle.js","waves.js","_periodic_data.js"];
|
||||
@@ -20,11 +20,25 @@
|
||||
}
|
||||
|
||||
function renderSims() {
|
||||
const base = _catFilter === 'all' ? SIMS : SIMS.filter(s => s.cat === _catFilter);
|
||||
// Контент-движок: мёрж код-реестра поверх 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 => `
|
||||
<div class="sim-card ${s.id ? '' : 'soon'}" ${s.id ? `onclick="openSim('${s.id}')"` : ''}>
|
||||
${s.preview}
|
||||
${window.LabRegistry ? window.LabRegistry.resolvePreview(s) : s.preview}
|
||||
<div class="sim-body">
|
||||
<div class="sim-cat ${s.cat}">${s.cat === 'math' ? '∑ Математика' : s.cat === 'chem' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg> Химия' : s.cat === 'bio' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M2 15c6.667-6 13.333 0 20-6"/><path d="M9 22c1.798-2 2.518-4 2.807-6"/><path d="M15 2c-1.798 2-2.518 4-2.807 6"/><path d="m17 6-2.5-2.5M14 8 13 7M7 18l2.5 2.5M3.5 14.5l.5.5M20 9l.5.5M6.5 12.5l1 1M16.5 10.5l1 1M10 16l1.5 1.5"/></svg> Биология' : s.cat === 'game' ? '<svg class="ic" viewBox="0 0 24 24"><line x1="6" y1="12" x2="10" y2="12"/><line x1="8" y1="10" x2="8" y2="14"/><line x1="15" y1="13" x2="15.01" y2="13"/><line x1="18" y1="11" x2="18.01" y2="11"/><rect x="2" y="6" width="20" height="12" rx="2"/></svg> Игры' : LS.icon('zap',14) + ' Физика'}</div>
|
||||
<div class="sim-title">${s.title}</div>
|
||||
@@ -935,7 +949,9 @@
|
||||
}
|
||||
|
||||
function loadTheory(simId) {
|
||||
const t = THEORY[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');
|
||||
if (!t) { el.innerHTML = '<div class="tp-text" style="text-align:center;padding:40px 0;color:var(--text-3)">Теория для этой симуляции пока не добавлена</div>'; return; }
|
||||
let html = `<div class="tp-title">${LS.icon('book-open',16)} ${t.title}</div>`;
|
||||
@@ -955,6 +971,58 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Контент-движок, Фаза 5: чип «Связано с программой» ──────────────────
|
||||
Подтягивает курикулумные связи симуляции (GET /api/lab/sims/:id/related) и
|
||||
рендерит чипы-ссылки рядом с заголовком симуляции. Самодостаточно: создаёт
|
||||
контейнер #sim-related динамически (без правок lab.html/CSS — меньше риск
|
||||
конфликта с параллельными сессиями). Тихо прячется, если связей нет/ошибка. */
|
||||
var _LAB_LINK_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
|
||||
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 = '<span style="font-size:.68rem;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:.05em">'
|
||||
+ _LAB_LINK_ICON + ' Связано с программой</span>';
|
||||
all.forEach(function (l) {
|
||||
var label = _labRelEsc(l.label || (l.kind + ':' + l.ref_id));
|
||||
if (l.href) {
|
||||
html += '<a href="' + _labRelEsc(l.href) + '" title="Открыть в учебнике" style="' + chipBase
|
||||
+ 'background:rgba(155,93,229,.14);color:var(--violet);text-decoration:none;border:1px solid rgba(155,93,229,.32)">' + label + '</a>';
|
||||
} else {
|
||||
html += '<span style="' + chipBase
|
||||
+ 'background:rgba(255,255,255,.06);color:var(--text-2);border:1px solid rgba(255,255,255,.12)">' + label + '</span>';
|
||||
}
|
||||
});
|
||||
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';
|
||||
|
||||
@@ -30,6 +30,19 @@
|
||||
var geomSim = null;
|
||||
var qualSim = null;
|
||||
|
||||
/* Контент-движок, Фаза 3 (ленивая загрузка): часть глобалов с экземплярами
|
||||
симуляций объявляется внутри их собственных НЫНЕ ЛЕНИВЫХ файлов, поэтому до
|
||||
первого открытия такой симуляции они не существуют. Legacy-«дробовик»
|
||||
_pauseAllSims()/closeSim() ссылается на них по голому имени, что до загрузки
|
||||
любого файла бросало ReferenceError (напр. cirSim). Предсоздаём эти имена как
|
||||
свойства window (null), чтобы guard'ы безопасно давали false; при загрузке
|
||||
файла симуляции его собственный var/присваивание обновит тот же глобал. */
|
||||
['cirSim','reacSim','flaskSim','newtonSim','sandboxSim','crystalSim','orbitalsSim',
|
||||
'stereoSim','angryBirdsSim','trigSim','pendSim','radioactiveSim','heSim',
|
||||
'periodicSim','organicSim','_solutionsSim','mirrorSim'].forEach(function (_n) {
|
||||
if (!(_n in window)) window[_n] = null;
|
||||
});
|
||||
|
||||
var ALL_SIM_BODIES = ['sim-graph','sim-proj','sim-coll','sim-tri','sim-trigcircle','sim-emfield',
|
||||
'sim-molphys',
|
||||
'sim-circuit','sim-chemistry','sim-dynamics',
|
||||
@@ -52,6 +65,7 @@
|
||||
// Pause all animation-loop sims (non-destructive). Called when switching
|
||||
// between sims so a previously opened sim doesn't keep rendering offscreen.
|
||||
function _pauseAllSims() {
|
||||
if (window.LabRegistry) window.LabRegistry.stopActive();
|
||||
if (pSim) pSim.pause();
|
||||
if (cSim) cSim.pause();
|
||||
if (gasSim) gasSim.stop();
|
||||
@@ -105,58 +119,34 @@
|
||||
// load theory for this sim
|
||||
loadTheory(id.includes(':') ? id.split(':')[0] : id);
|
||||
|
||||
if (id === 'graph') _openGraph();
|
||||
if (id === 'projectile') _openProjectile();
|
||||
if (id === 'collision') _openCollision();
|
||||
if (id === 'triangle') _openTriangle();
|
||||
if (id === 'trigcircle') _openTrigCircle();
|
||||
if (id === 'magnetic') _openEMField('B'); // backward compat: #magnetic → emfield B-mode
|
||||
if (id === 'coulomb') _openEMField('E'); // backward compat: #coulomb → emfield E-mode
|
||||
if (id === 'emfield') _openEMField('E');
|
||||
if (id.startsWith('emfield:')) { _openEMField(id.split(':')[1]); }
|
||||
if (id === 'molphys') _openMolPhys();
|
||||
if (id.startsWith('molphys:')) { _openMolPhys(id.split(':')[1]); }
|
||||
if (id === 'circuit') _openCircuit();
|
||||
if (id === 'chemistry') _openChemistry();
|
||||
if (id.startsWith('chemistry:')) { _openChemistry(id.split(':')[1]); }
|
||||
if (id === 'dynamics') _openDynamics();
|
||||
if (id.startsWith('dynamics:')) { _openDynamics(id.split(':')[1]); }
|
||||
if (id === 'crystal') _openCrystal();
|
||||
if (id === 'orbitals') _openOrbitals();
|
||||
if (id === 'stereo') _openStereo();
|
||||
if (id.startsWith('stereo:')) { _openStereo(id.split(':')[1]); }
|
||||
if (id === 'chemsandbox') _openChemSandbox();
|
||||
if (id === 'celldivision') _openCellDivision();
|
||||
if (id === 'photosynthesis') _openPhotosynthesis();
|
||||
if (id === 'angrybirds') _openAngryBirds();
|
||||
if (id === 'quadratic') _openQuadratic();
|
||||
if (id === 'normaldist') _openNormalDist();
|
||||
if (id === 'graphtransform') _openGraphTransform();
|
||||
if (id === 'pendulum') _openPendulum();
|
||||
if (id === 'equilibrium') _openEquilibrium();
|
||||
if (id === 'opticsbench') _openOpticsBench('lens');
|
||||
if (id.startsWith('opticsbench:')) _openOpticsBench(id.split(':')[1]);
|
||||
if (id === 'thinlens') _openOpticsBench('lens'); // backward compat
|
||||
if (id === 'mirrors') _openOpticsBench('mirror'); // backward compat
|
||||
if (id === 'refraction') _openOpticsBench('refraction'); // backward compat
|
||||
if (id === 'isoprocess') _openIsoprocess();
|
||||
if (id === 'titration') _openTitration();
|
||||
if (id === 'probability') _openProbability();
|
||||
if (id === 'bohratom') _openBohrAtom();
|
||||
if (id === 'electrolysis') _openElectrolysis();
|
||||
if (id === 'race') _openRace();
|
||||
if (id === 'waves') _openWaves();
|
||||
if (id === 'hydrostatics') _openHydro();
|
||||
if (id.startsWith('hydrostatics:')) _openHydro(id.split(':')[1]);
|
||||
if (id === 'radioactive') _openRadioactive();
|
||||
if (id === 'geometry') _openGeometry();
|
||||
if (id === 'logic') _openLogic();
|
||||
if (id === 'heatengine') _openHeatEngine();
|
||||
if (id === 'stoichiometry') _openStoich();
|
||||
if (id === 'qualanalysis') _openQualAnalysis();
|
||||
if (id === 'periodic') _openPeriodic();
|
||||
if (id === 'organic') _openOrganic();
|
||||
if (id === 'solutions') _openSolutions();
|
||||
// Фаза 5: чип «Связано с программой» (курикулумные связи симуляции).
|
||||
if (typeof _loadRelated === 'function') _loadRelated(id.includes(':') ? id.split(':')[0] : id);
|
||||
|
||||
// ── Контент-движок (Фаза 1): диспетчеризация через реестр ──
|
||||
// Все каталожные симуляции зарегистрированы в _register-all.js.
|
||||
// Алиасы deep-link (magnetic/coulomb/thinlens/mirrors/refraction) нормализуем
|
||||
// в канонический id[:arg] перед обращением к реестру.
|
||||
var _aliases = window.LAB_SIM_ALIASES || {};
|
||||
var _cid = _aliases[id.split(':')[0]] || id;
|
||||
if (window.LabRegistry && window.LabRegistry.has(_cid)) {
|
||||
const _m = window.LabRegistry.get(_cid);
|
||||
const _arg = _cid.includes(':') ? _cid.split(':')[1] : undefined;
|
||||
window.LabRegistry.setActive(_m);
|
||||
// Фаза 3: open() может вернуть Promise (ленивая загрузка кода). Иконки
|
||||
// перерисовываем после фактической инициализации тела симуляции; ошибку
|
||||
// асинхронной загрузки ловим через .catch (sync try/catch её не поймает).
|
||||
try {
|
||||
const _r = _m.open({ id: _cid, arg: _arg });
|
||||
if (_r && typeof _r.then === 'function') {
|
||||
_r.then(function () { if (window.lucide) lucide.createIcons(); })
|
||||
.catch(function (e) { console.error('[LabRegistry] open failed:', _cid, e); });
|
||||
} else if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
} catch (e) { console.error('[LabRegistry] open failed:', _cid, e); }
|
||||
return;
|
||||
}
|
||||
if (window.console) console.warn('[LabRegistry] неизвестная симуляция:', id);
|
||||
}
|
||||
|
||||
function _simShow(elId) {
|
||||
@@ -210,6 +200,7 @@
|
||||
}
|
||||
|
||||
function closeSim() {
|
||||
if (window.LabRegistry) window.LabRegistry.destroyActive();
|
||||
if (pSim) pSim.pause();
|
||||
if (cSim) cSim.pause();
|
||||
if (mSim && mSim.particleOn) mSim.toggleParticle();
|
||||
|
||||
Reference in New Issue
Block a user