From 51fcb6e4b77c32ba456d45b70c66c59e42b4e887 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 13 Jun 2026 11:07:14 +0300 Subject: [PATCH] =?UTF-8?q?feat(labs):=20=D0=A4=D0=B0=D0=B7=D0=B02=20?= =?UTF-8?q?=E2=80=94=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5/=D0=B2=D0=BE=D0=B7=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2=20=D1=81=D0=B8=D0=BC=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Поверх getState/applyState: в обычном режиме параметры активной симуляции персистятся в localStorage (lab-sim-state-v1, дедуп, кап 8КБ, flush на pagehide) и восстанавливаются при открытии. В embed/онлайн-уроке персист выключен — состоянием управляет учитель. applyState обёрнут в try/catch (старые формы не ломают). Co-Authored-By: Claude Opus 4.8 --- frontend/js/labs/lab-glue.js | 31 +++++++++++++++++++++++++ plans/simulations-improvement/README.md | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/js/labs/lab-glue.js b/frontend/js/labs/lab-glue.js index d472611..391dcd1 100644 --- a/frontend/js/labs/lab-glue.js +++ b/frontend/js/labs/lab-glue.js @@ -330,8 +330,39 @@ const SIMS = [ // Map simId → { getState, applyState } registered by openSim handlers const _simStateRegistry = {}; + /* ── Локальный персист параметров симуляции (Фаза 2) ────────────────── + Поверх того же getState/applyState: в обычном (не embed) режиме сохраняем + состояние активной симуляции в localStorage и восстанавливаем при открытии. + В embed/онлайн-уроке состоянием управляет учитель — персист отключён. */ + const _LAB_STATE_KEY = 'lab-sim-state-v1'; + function _loadSavedStates() { try { return JSON.parse(localStorage.getItem(_LAB_STATE_KEY) || '{}') || {}; } catch (e) { return {}; } } + function _saveSavedStates(m) { try { localStorage.setItem(_LAB_STATE_KEY, JSON.stringify(m)); } catch (e) {} } + let _persistSimId = null, _persistInterval = null, _lastPersisted = null; + function _stopPersist() { if (_persistInterval) { clearInterval(_persistInterval); _persistInterval = null; } _persistSimId = null; _lastPersisted = null; } + function _persistNow() { + if (!_persistSimId) return; + const reg = _simStateRegistry[_persistSimId]; + if (!reg || !reg.getState) return; + try { + const s = reg.getState(); + if (s == null) return; + const json = JSON.stringify(s); + if (json === _lastPersisted || json.length > 8000) return; + _lastPersisted = json; + const m = _loadSavedStates(); m[_persistSimId] = s; _saveSavedStates(m); + } catch (e) {} + } + function _startPersist(simId) { _stopPersist(); _persistSimId = simId; _persistInterval = setInterval(_persistNow, 2000); } + window.addEventListener('pagehide', _persistNow); + function _registerSimState(simId, getState, applyState) { _simStateRegistry[simId] = { getState, applyState }; + if (_embedMode) return; // в embed состоянием управляет учитель + // восстановить сохранённые параметры (после инициализации тела) + запустить персист + setTimeout(function () { + try { const saved = _loadSavedStates()[simId]; if (saved != null && applyState) applyState(saved); } catch (e) {} + _startPersist(simId); + }, 0); } let _lastEmittedState = null; diff --git a/plans/simulations-improvement/README.md b/plans/simulations-improvement/README.md index bce4fa0..c35e1ad 100644 --- a/plans/simulations-improvement/README.md +++ b/plans/simulations-improvement/README.md @@ -70,7 +70,7 @@ ## Прогресс - [x] Фаза 0 (фундамент заложен) — эконом-режим/reduced-motion (LabFX, тумблер), выбор симуляции из списка в редакторе урока, удалён мёртвый `SimUtil`, добавлены `LabPalette` (_palette.js) и `SimBase` (_simbase.js) как опциональные основания. **Адаптация симуляций к SimBase/LabPalette и удаление «дробовика» `_pauseAllSims/closeSim` — постепенно, по мере правок каждой симуляции (требует поштучной проверки, нет фронт-тестов).** - [~] Фаза 1 — сделано: фреймворк `LabTasks` (_tasks.js) + интеграция в теорию; задания на 17 симуляций. Осталось: XP за задания, deep-link на §, наполнение остальных. -- [~] Фаза 2 — сделано: «Сохранить кадр в Мои материалы» + «Скачать PNG» в топбаре лаборатории (захват активного canvas, переиспользует MaterialSave.image). Осталось: сохранение/возобновление параметров симуляции, измерительные инструменты. (3D/WebGL-кадр может быть пустым без preserveDrawingBuffer — доработать.) +- [~] Фаза 2 — сделано: «Сохранить кадр в Мои материалы» + «Скачать PNG»; сохранение/возобновление параметров симуляции (localStorage поверх getState/applyState, не в embed). Осталось: измерительные инструменты (линейка/транспортир). (3D/WebGL-кадр пустой без preserveDrawingBuffer — доработать.) - [ ] Фаза 3 - [ ] Фаза 4 - [ ] Фаза 5