feat(stereo3d): drag-to-rotate для 3D-сцен Геометрии 10

STEREO3D.attachDragRotate(target, scene, onChange?) — мутирует scene.rotX/rotY на mouse/touch drag, по умолчанию пересобирает innerHTML контейнера через scene.render(). Применено к аннотированному кубу §1 (viz1-cube) в geometry_10_r1.html. Остальные сцены не затронуты.
This commit is contained in:
Maxim Dolgolyov
2026-05-29 21:45:33 +03:00
parent 96b5e46660
commit b3ea35049f
2 changed files with 76 additions and 3 deletions
+70
View File
@@ -714,4 +714,74 @@ function formatLabel(s){
S.Scene = Scene;
S.formatLabel = formatLabel;
/* ============================================================ */
/* Drag-to-rotate */
/* ============================================================ */
/*
* STEREO3D.attachDragRotate(target, scene, onChange?)
*
* target — SVGElement или контейнер-HTMLElement, на который повешен
* результат scene.render().
* scene — экземпляр STEREO3D.Scene (мы мутируем scene.rotX / scene.rotY).
* onChange — необязательный callback после изменения углов.
* Если не передан и target не SVG — по умолчанию
* пересобираем содержимое контейнера через scene.render().
*
* Возвращает функцию detach() — для удаления слушателей.
*/
S.attachDragRotate = function attachDragRotate(target, scene, onChange){
if (!target || !scene) return function(){};
// По умолчанию для контейнера-HTMLElement делаем re-render через innerHTML.
const isSvg = (target.tagName && String(target.tagName).toLowerCase() === 'svg');
const defaultOnChange = isSvg ? null : function(){ target.innerHTML = scene.render(); };
const cb = onChange || defaultOnChange;
let dragging = false, lastX = 0, lastY = 0;
try { target.style.touchAction = 'none'; } catch(_){}
try { target.style.cursor = 'grab'; } catch(_){}
function onDown(e){
dragging = true;
try { target.style.cursor = 'grabbing'; } catch(_){}
const p = (e.touches && e.touches[0]) ? e.touches[0] : e;
lastX = p.clientX; lastY = p.clientY;
if (e.preventDefault) e.preventDefault();
}
function onMove(e){
if (!dragging) return;
const p = (e.touches && e.touches[0]) ? e.touches[0] : e;
const dx = p.clientX - lastX, dy = p.clientY - lastY;
scene.rotY = (scene.rotY || 0) + dx * 0.012;
scene.rotX = (scene.rotX || 0) + dy * 0.012;
if (scene.rotX > 1.4) scene.rotX = 1.4;
if (scene.rotX < -1.4) scene.rotX = -1.4;
lastX = p.clientX; lastY = p.clientY;
if (cb) cb(scene);
if (e.preventDefault) e.preventDefault();
}
function onUp(){
if (!dragging) return;
dragging = false;
try { target.style.cursor = 'grab'; } catch(_){}
}
target.addEventListener('mousedown', onDown, { passive: false });
target.addEventListener('touchstart', onDown, { passive: false });
window.addEventListener('mousemove', onMove, { passive: false });
window.addEventListener('touchmove', onMove, { passive: false });
window.addEventListener('mouseup', onUp);
window.addEventListener('touchend', onUp);
window.addEventListener('touchcancel',onUp);
return function detach(){
target.removeEventListener('mousedown', onDown);
target.removeEventListener('touchstart', onDown);
window.removeEventListener('mousemove', onMove);
window.removeEventListener('touchmove', onMove);
window.removeEventListener('mouseup', onUp);
window.removeEventListener('touchend', onUp);
window.removeEventListener('touchcancel',onUp);
};
};
})();
+6 -3
View File
@@ -245,7 +245,7 @@ const PARAS = [
{ id:'p1', num:'§ 1', name:'Пространственные фигуры', sub:'Призма · пирамида · цилиндр · конус · шар' },
{ id:'p2', num:'§ 2', name:'Прямые и плоскости', sub:'3 аксиомы стереометрии + следствия' },
{ id:'p3', num:'§ 3', name:'Построения сечений', sub:'Метод следов · max 6-угольник у куба' },
{ id:'final', num:'', name:'Финал раздела', sub:'4 интегрированных босса', final:true }
{ id:'final', num:'&#9733;', name:'Финал раздела', sub:'4 интегрированных босса', final:true }
];
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
@@ -579,7 +579,7 @@ function buildP1(){
html += makeCard('rule', 'Формула Эйлера', '§ 1.3',
'<p>Для любого <b>выпуклого многогранника</b>:</p>'
+ '<p style="text-align:center;margin:8px 0">$$В - Р + Г = 2$$</p>'
+ '<p>Куб: $8 - 12 + 6 = 2$ . Тетраэдр: $4 - 6 + 4 = 2$ .</p>'
+ '<p>Куб: $8 - 12 + 6 = 2$ &#10003;. Тетраэдр: $4 - 6 + 4 = 2$ &#10003;.</p>'
+ '<details class="spoiler"><summary>Свойства основных тел</summary><div class="spoiler-body">'
+ '<ul>'
+ '<li>$n$-угольная призма: $В=2n$, $Р=3n$, $Г=n+2$.</li>'
@@ -948,7 +948,10 @@ function buildAnnotatedCube(){
sc.addCube({center:[0,0,0], size:2.0, labels:true});
sc.addEdge([-1,-1,-1],[1,1,1], {stroke:'#dc2626', width:3, dash:'5 3'});
sc.addFace([[-1,-1,-1],[1,-1,-1],[1,-1,1],[-1,-1,1]], {fill:'#a78bfa', opacity:0.30, stroke:'none'});
const el = document.getElementById('viz1-cube'); if(el) el.innerHTML = sc.render();
const el = document.getElementById('viz1-cube');
if(!el) return;
el.innerHTML = sc.render();
if (S.attachDragRotate && !el.__rotBound){ S.attachDragRotate(el, sc); el.__rotBound = true; }
}
function buildPrismDirect(){