// ============================================================ // js/vendor/OrbitControls.js — eigen, compacte vervanging van de officiële // three.js OrbitControls-addon. Geschreven omdat we geen netwerktoegang // hebben om het origineel te vendoren; dekt precies de subset die wall.js // gebruikt (`new THREE.OrbitControls(camera, domElement)`, // `controls.target.set(...)`, `controls.update()`), plus interactie: // links-slepen = roteren, scrollwiel = zoomen, rechts-slepen = pannen; // touch: één vinger = roteren, twee vingers = knijpen (zoomen) en samen schuiven (pannen). // // Moet NA three.min.js geladen worden (leunt op window.THREE). // ============================================================ (function () { if (typeof THREE === 'undefined') { console.error('OrbitControls.js (eigen versie): THREE is niet geladen — controleer de scriptvolgorde.'); return; } function OrbitControls(camera, domElement) { const scope = this; this.object = camera; this.domElement = domElement || document; this.target = new THREE.Vector3(); this.minDistance = 0.1; this.maxDistance = 1000; this.minPolarAngle = 0.001; this.maxPolarAngle = Math.PI - 0.001; this.rotateSpeed = 1.0; this.zoomSpeed = 1.0; this.panSpeed = 1.0; this.enableRotate = true; this.enableZoom = true; this.enablePan = true; const offset = new THREE.Vector3(); const spherical = new THREE.Spherical(); let isRotating = false; let isPanning = false; let lastX = 0, lastY = 0; function getZoomScale() { return Math.pow(0.95, scope.zoomSpeed); } this.update = function () { offset.copy(scope.object.position).sub(scope.target); spherical.setFromVector3(offset); offset.setFromSpherical(spherical); scope.object.position.copy(scope.target).add(offset); scope.object.lookAt(scope.target); return true; }; function onPointerDown(event) { if (event.button === 2) { if (!scope.enablePan) return; isPanning = true; } else { if (!scope.enableRotate) return; isRotating = true; } lastX = event.clientX; lastY = event.clientY; } function rotate(dx, dy) { const w = scope.domElement.clientWidth || 1; const h = scope.domElement.clientHeight || 1; offset.copy(scope.object.position).sub(scope.target); spherical.setFromVector3(offset); spherical.theta -= (dx / w) * Math.PI * scope.rotateSpeed; spherical.phi -= (dy / h) * Math.PI * scope.rotateSpeed; spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); spherical.makeSafe(); offset.setFromSpherical(spherical); scope.object.position.copy(scope.target).add(offset); scope.object.lookAt(scope.target); } function pan(dx, dy) { const distance = scope.object.position.distanceTo(scope.target); const fov = (scope.object.fov || 50) * Math.PI / 180; const h = scope.domElement.clientHeight || 1; const factor = 2 * Math.tan(fov / 2) * distance / h; const te = scope.object.matrix.elements; const xAxis = new THREE.Vector3(te[0], te[1], te[2]).multiplyScalar(-dx * factor * scope.panSpeed); const yAxis = new THREE.Vector3(te[4], te[5], te[6]).multiplyScalar(dy * factor * scope.panSpeed); scope.target.add(xAxis).add(yAxis); scope.object.position.add(xAxis).add(yAxis); } function zoom(scale) { offset.copy(scope.object.position).sub(scope.target); const distance = Math.max(scope.minDistance, Math.min(scope.maxDistance, offset.length() * scale)); offset.setLength(distance); scope.object.position.copy(scope.target).add(offset); } function onPointerMove(event) { if (!isRotating && !isPanning) return; const dx = event.clientX - lastX; const dy = event.clientY - lastY; lastX = event.clientX; lastY = event.clientY; if (isRotating) rotate(dx, dy); else pan(dx, dy); } function onPointerUp() { isRotating = false; isPanning = false; } function onWheel(event) { if (!scope.enableZoom) return; event.preventDefault(); zoom(event.deltaY > 0 ? 1 / getZoomScale() : getZoomScale()); } // Touch: midden en afstand van de vingers; bij een wissel van het aantal vingers opnieuw beginnen. let touch = null; function touchState(list) { const a = list[0], b = list[1] || list[0]; return { n: list.length, x: (a.clientX + b.clientX) / 2, y: (a.clientY + b.clientY) / 2, d: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY) }; } function onTouchStart(event) { event.preventDefault(); touch = touchState(event.touches); } function onTouchMove(event) { if (!touch) return; event.preventDefault(); const t = touchState(event.touches); if (t.n !== touch.n) { touch = t; return; } if (t.n === 1) { if (scope.enableRotate) rotate(t.x - touch.x, t.y - touch.y); } else { if (scope.enablePan) pan(t.x - touch.x, t.y - touch.y); if (scope.enableZoom && t.d > 0 && touch.d > 0) zoom(touch.d / t.d); } touch = t; } function onTouchEnd(event) { touch = event.touches.length ? touchState(event.touches) : null; } function onContextMenu(event) { event.preventDefault(); } this.domElement.addEventListener('mousedown', onPointerDown); window.addEventListener('mousemove', onPointerMove); window.addEventListener('mouseup', onPointerUp); this.domElement.addEventListener('wheel', onWheel, { passive: false }); this.domElement.addEventListener('contextmenu', onContextMenu); this.domElement.addEventListener('touchstart', onTouchStart, { passive: false }); this.domElement.addEventListener('touchmove', onTouchMove, { passive: false }); this.domElement.addEventListener('touchend', onTouchEnd); this.domElement.addEventListener('touchcancel', onTouchEnd); this.dispose = function () { scope.domElement.removeEventListener('mousedown', onPointerDown); window.removeEventListener('mousemove', onPointerMove); window.removeEventListener('mouseup', onPointerUp); scope.domElement.removeEventListener('wheel', onWheel); scope.domElement.removeEventListener('contextmenu', onContextMenu); scope.domElement.removeEventListener('touchstart', onTouchStart); scope.domElement.removeEventListener('touchmove', onTouchMove); scope.domElement.removeEventListener('touchend', onTouchEnd); scope.domElement.removeEventListener('touchcancel', onTouchEnd); }; } THREE.OrbitControls = OrbitControls; })();