From ccfb6116c038ac34f86ba5b145189e740d6b7025 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 11:34:51 +0300 Subject: [PATCH] =?UTF-8?q?feat(stereo3d):=20=D0=A4=D0=B0=D0=B7=D0=B0=205?= =?UTF-8?q?=20=E2=80=94=20deep-link=20=D1=84=D0=B8=D0=B3=D1=83=D1=80=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D1=83=D1=87=D0=B5=D0=B1=D0=BD=D0=B8=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20+=20=D0=BA=D0=BB=D0=B0=D0=B2=D0=B8=D0=B0=D1=82=D1=83?= =?UTF-8?q?=D1=80=D0=BD=D0=B0=D1=8F=20a11y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - openSim('stereo:
') и /lab?stereofig=
открывают нужное тело (без изменения общего hash-роутера) - клавиатура на canvas: стрелки=орбита, +/-=зум, R/Home=сброс - aria-live на readout; bump stereo.js?v=8 - дробление файла на модули отложено по решению пользователя (в бэклоге) Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/labs/lab-init.js | 1 + frontend/js/labs/stereo.js | 27 ++++++++++++++++++++++++++- frontend/lab.html | 2 +- plans/STEREO_3D_IMPROVEMENT.md | 10 ++++++---- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/frontend/js/labs/lab-init.js b/frontend/js/labs/lab-init.js index f47c4d2..9a46798 100644 --- a/frontend/js/labs/lab-init.js +++ b/frontend/js/labs/lab-init.js @@ -124,6 +124,7 @@ 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(); diff --git a/frontend/js/labs/stereo.js b/frontend/js/labs/stereo.js index 11ba084..3550290 100644 --- a/frontend/js/labs/stereo.js +++ b/frontend/js/labs/stereo.js @@ -112,6 +112,23 @@ class StereoSim { this._invalidate(); }, { passive: false }); + // Keyboard navigation (a11y) — works when the canvas is focused. + on(el, 'keydown', e => { + const STEP = 0.12; + let handled = true; + switch (e.key) { + case 'ArrowLeft': this._rotY -= STEP; this._autoSpin = false; break; + case 'ArrowRight': this._rotY += STEP; this._autoSpin = false; break; + case 'ArrowUp': this._rotX = Math.min(1.4, this._rotX + STEP); this._autoSpin = false; break; + case 'ArrowDown': this._rotX = Math.max(-1.4, this._rotX - STEP); this._autoSpin = false; break; + case '+': case '=': this._dist = Math.max(4, this._dist - 1); break; + case '-': case '_': this._dist = Math.min(40, this._dist + 1); break; + case 'r': case 'R': case 'Home': this.resetView(); break; + default: handled = false; + } + if (handled) { e.preventDefault(); this._idleTime = 0; this._invalidate(); } + }); + // WebGL context loss / restore — keep the page alive if the GPU resets. on(el, 'webglcontextlost', e => { e.preventDefault(); this._contextLost = true; this.stop(); }, false); on(el, 'webglcontextrestored', () => { @@ -3706,10 +3723,14 @@ class StereoSim { dodecahedron: ['a'], }; - function _openStereo() { + function _openStereo(figureType) { document.getElementById('sim-topbar-title').textContent = 'Стереометрия 3D'; _simShow('sim-stereo'); document.getElementById('stereo-stats').style.display = ''; + // Deep-link from a textbook: openSim('stereo:pyramid') or /lab?stereofig=pyramid + if (!figureType) { + try { figureType = new URLSearchParams(location.search).get('stereofig') || null; } catch (_) {} + } requestAnimationFrame(() => requestAnimationFrame(() => { if (!stereoSim) { stereoSim = new StereoSim(document.getElementById('stereo-container')); @@ -3718,6 +3739,10 @@ class StereoSim { stereoSim.fit(); stereoSim.play(); } + if (figureType && STEREO_PARAM_MAP[figureType]) { + const btn = document.querySelector(`.stereo-fig-btn[onclick*="'${figureType}'"]`); + setStereoFigure(figureType, btn); + } _stereoShowParams(stereoSim.figureType || 'cube'); _stereoUpdateUI(stereoSim.info()); _stereoUpdateFormulas(); diff --git a/frontend/lab.html b/frontend/lab.html index 6e0b251..eb48d2e 100644 --- a/frontend/lab.html +++ b/frontend/lab.html @@ -4819,7 +4819,7 @@ - + diff --git a/plans/STEREO_3D_IMPROVEMENT.md b/plans/STEREO_3D_IMPROVEMENT.md index b8b31f8..967ec27 100644 --- a/plans/STEREO_3D_IMPROVEMENT.md +++ b/plans/STEREO_3D_IMPROVEMENT.md @@ -43,11 +43,13 @@ Бэклог: подсветка грани по ховеру (нужен точный raycast по логическим граням); градиентный/бумажный фон (учесть захват в скриншоте). -## Фаза 5 — Интеграция и архитектура +## Фаза 5 — Интеграция и архитектура — ГОТОВО (без дробления файла, по решению пользователя) -- [ ] 5.1 Разбить `stereo.js` на модули (builders / sections / tools / ui), вынести константы и цвета. -- [ ] 5.2 Связать симуляцию с учебниками 10–11 (открывать тело/сечение из §-канвы и задач). -- [ ] 5.3 Доступность: клавиатура, `aria`, фокус. +- [~] 5.1 Дробление `stereo.js` на модули — **отложено по решению пользователя** (риск регрессий, не видно пользователю). Остаётся в бэклоге. +- [x] 5.2 Deep-link фигуры из учебников без изменения общего hash-роутера: `openSim('stereo:
')` и `/lab?stereofig=
#sim/stereo`. `_openStereo(figureType)` применяет фигуру и подсвечивает кнопку. Допустимые: cube, parallelepiped, prism, pyramid, truncpyramid, tetrahedron, octahedron, icosahedron, dodecahedron, cylinder, cone, trunccone, sphere. +- [x] 5.3 a11y: клавиатурная навигация по сфокусированному canvas — стрелки (орбита), +/− (зум), R/Home (сброс); `tabindex`/`role`/`aria-label` на canvas (Фаза 0), `aria-pressed`/`aria-label` на кнопках вида (Фаза 1), `aria-live` на readout. + +Бэклог Фазы 5: модульное дробление файла; deep-link конкретного сечения/инструмента (не только фигуры). ---