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:
@@ -714,4 +714,74 @@ function formatLabel(s){
|
|||||||
S.Scene = Scene;
|
S.Scene = Scene;
|
||||||
S.formatLabel = formatLabel;
|
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);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ const PARAS = [
|
|||||||
{ id:'p1', num:'§ 1', name:'Пространственные фигуры', sub:'Призма · пирамида · цилиндр · конус · шар' },
|
{ id:'p1', num:'§ 1', name:'Пространственные фигуры', sub:'Призма · пирамида · цилиндр · конус · шар' },
|
||||||
{ id:'p2', num:'§ 2', name:'Прямые и плоскости', sub:'3 аксиомы стереометрии + следствия' },
|
{ id:'p2', num:'§ 2', name:'Прямые и плоскости', sub:'3 аксиомы стереометрии + следствия' },
|
||||||
{ id:'p3', num:'§ 3', name:'Построения сечений', sub:'Метод следов · max 6-угольник у куба' },
|
{ id:'p3', num:'§ 3', name:'Построения сечений', sub:'Метод следов · max 6-угольник у куба' },
|
||||||
{ id:'final', num:'★', name:'Финал раздела', sub:'4 интегрированных босса', final:true }
|
{ id:'final', num:'★', name:'Финал раздела', sub:'4 интегрированных босса', final:true }
|
||||||
];
|
];
|
||||||
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
|
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
|
||||||
|
|
||||||
@@ -579,7 +579,7 @@ function buildP1(){
|
|||||||
html += makeCard('rule', 'Формула Эйлера', '§ 1.3',
|
html += makeCard('rule', 'Формула Эйлера', '§ 1.3',
|
||||||
'<p>Для любого <b>выпуклого многогранника</b>:</p>'
|
'<p>Для любого <b>выпуклого многогранника</b>:</p>'
|
||||||
+ '<p style="text-align:center;margin:8px 0">$$В - Р + Г = 2$$</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$ ✓. Тетраэдр: $4 - 6 + 4 = 2$ ✓.</p>'
|
||||||
+ '<details class="spoiler"><summary>Свойства основных тел</summary><div class="spoiler-body">'
|
+ '<details class="spoiler"><summary>Свойства основных тел</summary><div class="spoiler-body">'
|
||||||
+ '<ul>'
|
+ '<ul>'
|
||||||
+ '<li>$n$-угольная призма: $В=2n$, $Р=3n$, $Г=n+2$.</li>'
|
+ '<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.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.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'});
|
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(){
|
function buildPrismDirect(){
|
||||||
|
|||||||
Reference in New Issue
Block a user