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.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:'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:'★', 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$ ✓. Тетраэдр: $4 - 6 + 4 = 2$ ✓.</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(){
|
||||
|
||||
Reference in New Issue
Block a user