feat(lab-content-engine): phase 1 - data-driven регистрация всех симуляций

- _register-all.js: строит манифесты из SIMS + THEORY + карта OPEN (40 id),
  регистрирует все симуляции в LabRegistry; LAB_SIM_ALIASES для deep-link
- openSim(): удалена if-цепочка (~60 строк), замена на нормализацию алиасов +
  диспетчеризацию через реестр (early return)
- lab.html: _pilots.js -> _register-all.js (defer, последним)
- _pilots.js удалён (поглощён _register-all.js)

Паритет проверен: исполняемый harness (40 регистраций, dispatch, алиасы,
:arg) ALL PASS; независимое ревью PASS (coverage 40/40, dispatch byte-for-byte).
Lifecycle пока на _pauseAllSims/closeSim (дробовик) — паритет.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 13:49:19 +03:00
parent 81d4c15442
commit ebb2a9b37b
3 changed files with 111 additions and 103 deletions
-45
View File
@@ -1,45 +0,0 @@
'use strict';
/*
* Пилотная регистрация в LabRegistry (Фаза 0 контент-движка).
*
* Доказывает паритет: каталог/открытие/теория этих 3 симуляций идут через реестр.
* Загружается ПОСЛЕДНИМ среди labs-скриптов, поэтому P_* (lab-glue.js),
* THEORY (lab-init.js) и _openXxx (graph/quadratic/pendulum.js) уже определены.
* preview задан функцией (ленивое вычисление в renderSims) — безопасно к порядку.
*
* В Фазе 1 регистрация переедет в сами sim-файлы, а этот файл будет удалён.
*/
(function () {
if (!window.LabRegistry) return;
var R = window.LabRegistry;
R.register({
id: 'graph', cat: 'math',
title: 'График функции', desc: 'Постройте и исследуйте графики функций',
preview: function () { return (typeof P_GRAPH !== 'undefined') ? P_GRAPH : ''; },
theory: (typeof THEORY !== 'undefined') ? THEORY.graph : null,
open: function () { _openGraph(); },
stop: function () { /* нет анимационного цикла */ },
destroy: function () { /* нет ресурсов для освобождения */ }
});
R.register({
id: 'quadratic', cat: 'math',
title: 'Квадратное уравнение', desc: 'Дискриминант, корни, теорема Виета',
preview: function () { return (typeof P_QUADRATIC !== 'undefined') ? P_QUADRATIC : ''; },
theory: (typeof THEORY !== 'undefined') ? THEORY.quadratic : null,
open: function () { _openQuadratic(); },
stop: function () { /* нет анимационного цикла */ },
destroy: function () { /* нет ресурсов для освобождения */ }
});
R.register({
id: 'pendulum', cat: 'phys',
title: 'Маятник', desc: 'Колебания, период, затухание',
preview: function () { return (typeof P_PENDULUM !== 'undefined') ? P_PENDULUM : ''; },
theory: (typeof THEORY !== 'undefined') ? THEORY.pendulum : null,
open: function () { _openPendulum(); },
stop: function () { if (typeof pendSim !== 'undefined' && pendSim && pendSim.stop) pendSim.stop(); },
destroy: function () { if (typeof pendSim !== 'undefined' && pendSim && pendSim.stop) pendSim.stop(); }
});
})();
+99
View File
@@ -0,0 +1,99 @@
'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,
open: open
// 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'
};
})();
+12 -58
View File
@@ -106,68 +106,22 @@
// load theory for this sim
loadTheory(id.includes(':') ? id.split(':')[0] : id);
// Контент-движок: мигрированные симуляции открываются через реестр.
if (window.LabRegistry && window.LabRegistry.has(id)) {
const _m = window.LabRegistry.get(id);
const _arg = id.includes(':') ? id.split(':')[1] : undefined;
// ── Контент-движок (Фаза 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);
try { _m.open({ id: id, arg: _arg }); } catch (e) { console.error('[LabRegistry] open failed:', id, e); }
try { _m.open({ id: _cid, arg: _arg }); }
catch (e) { console.error('[LabRegistry] open failed:', _cid, e); }
if (window.lucide) lucide.createIcons();
return;
}
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();
if (window.console) console.warn('[LabRegistry] неизвестная симуляция:', id);
}
function _simShow(elId) {