diff --git a/js/viewer.js b/js/viewer.js index 282d379..2b623c7 100644 --- a/js/viewer.js +++ b/js/viewer.js @@ -11,6 +11,13 @@ const _tmpV1 = new THREE.Vector3(); const _tmpV2 = new THREE.Vector3(); const _tmpV3 = new THREE.Vector3(); const _tmpV4 = new THREE.Vector3(); +const _tmpV5 = new THREE.Vector3(); +const _tmpV6 = new THREE.Vector3(); +const _tmpPlane = new THREE.Plane(); +const _tmpRay1 = new THREE.Ray(); +const _tmpRay2 = new THREE.Ray(); +const _tmpNdc = new THREE.Vector2(); +const _tmpSize = new THREE.Vector2(); let renderer, orthoCamera, perspCamera, camera, scene, controls, meshGroup, ambientLight, dirLight1, dirLight2, grid; let _isPerspective = false; @@ -210,7 +217,8 @@ export function initViewer(canvas) { const _orbitRaycaster = new THREE.Raycaster(); let _customPivot = null; // active pivot for the current drag let _lastKnownPivot = null; // persists between drags as fallback - let _lastPointer = null; + const _lastPointer = { x: 0, y: 0 }; + let _pointerActive = false; // Small red sphere shown at the orbit centre during a drag const _pivotMarker = new THREE.Mesh( @@ -225,10 +233,11 @@ export function initViewer(canvas) { if (e.button !== 0 || !controls.enabled) return; if (!currentMesh) return; const rect = renderer.domElement.getBoundingClientRect(); - const ndc = new THREE.Vector2( + _tmpNdc.set( ((e.clientX - rect.left) / rect.width) * 2 - 1, ((e.clientY - rect.top) / rect.height) * -2 + 1, ); + const ndc = _tmpNdc; _orbitRaycaster.setFromCamera(ndc, camera); const hits = _orbitRaycaster.intersectObject(currentMesh); if (hits.length) { @@ -239,7 +248,9 @@ export function initViewer(canvas) { } else { return; // no pivot available yet, fall back to OrbitControls default } - _lastPointer = { x: e.clientX, y: e.clientY }; + _lastPointer.x = e.clientX; + _lastPointer.y = e.clientY; + _pointerActive = true; controls.enableRotate = false; // we'll rotate manually // Show marker, sized as ~1.5 % of the visible frustum height @@ -253,10 +264,11 @@ export function initViewer(canvas) { }); document.addEventListener('pointermove', (e) => { - if (!_customPivot || !_lastPointer || !controls.enabled) return; + if (!_customPivot || !_pointerActive || !controls.enabled) return; const dx = e.clientX - _lastPointer.x; const dy = e.clientY - _lastPointer.y; - _lastPointer = { x: e.clientX, y: e.clientY }; + _lastPointer.x = e.clientX; + _lastPointer.y = e.clientY; if (dx === 0 && dy === 0) return; const rotSpeed = 0.005; @@ -290,7 +302,7 @@ export function initViewer(canvas) { document.addEventListener('pointerup', () => { if (_customPivot) { _customPivot = null; - _lastPointer = null; + _pointerActive = false; controls.enableRotate = true; // Re-sync up vector for OrbitControls camera.up.set(0, 0, 1); @@ -302,20 +314,23 @@ export function initViewer(canvas) { // Pinch-to-zoom + two-finger pan for touch devices let _pinchDist = null; - let _pinchMid = null; // { x, y } client coords of two-finger midpoint + const _pinchMid = { x: 0, y: 0 }; // client coords of two-finger midpoint + let _pinchActive = false; renderer.domElement.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { const t0 = e.touches[0], t1 = e.touches[1]; _pinchDist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY); - _pinchMid = { x: (t0.clientX + t1.clientX) / 2, y: (t0.clientY + t1.clientY) / 2 }; + _pinchMid.x = (t0.clientX + t1.clientX) / 2; + _pinchMid.y = (t0.clientY + t1.clientY) / 2; + _pinchActive = true; controls.enabled = false; // suppress OrbitControls during two-finger gesture e.preventDefault(); } }, { passive: false }); renderer.domElement.addEventListener('touchmove', (e) => { - if (e.touches.length !== 2 || _pinchDist === null) return; + if (e.touches.length !== 2 || !_pinchActive) return; e.preventDefault(); const t0 = e.touches[0], t1 = e.touches[1]; const rect = renderer.domElement.getBoundingClientRect(); @@ -334,16 +349,13 @@ export function initViewer(canvas) { if (_isPerspective) { // Pan on the plane through controls.target perpendicular to the view direction const camDir = _tmpV1.copy(controls.target).sub(camera.position).normalize(); - const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(camDir, controls.target); - const ray1 = new THREE.Ray(); - const ray2 = new THREE.Ray(); + _tmpPlane.setFromNormalAndCoplanarPoint(camDir, controls.target); _tmpV2.set(prevNdcX, prevNdcY, 0.5).unproject(camera); - ray1.set(camera.position, _tmpV2.sub(camera.position).normalize()); + _tmpRay1.set(camera.position, _tmpV2.sub(camera.position).normalize()); _tmpV3.set(curNdcX, curNdcY, 0.5).unproject(camera); - ray2.set(camera.position, _tmpV3.sub(camera.position).normalize()); - const p1 = new THREE.Vector3(), p2 = new THREE.Vector3(); - if (ray1.intersectPlane(plane, p1) && ray2.intersectPlane(plane, p2)) { - _tmpV4.subVectors(p1, p2); + _tmpRay2.set(camera.position, _tmpV3.sub(camera.position).normalize()); + if (_tmpRay1.intersectPlane(_tmpPlane, _tmpV5) && _tmpRay2.intersectPlane(_tmpPlane, _tmpV6)) { + _tmpV4.subVectors(_tmpV5, _tmpV6); camera.position.add(_tmpV4); controls.target.add(_tmpV4); } @@ -375,7 +387,8 @@ export function initViewer(canvas) { } _pinchDist = newDist; - _pinchMid = { x: midX, y: midY }; + _pinchMid.x = midX; + _pinchMid.y = midY; controls.update(); _needsRender = true; }, { passive: false }); @@ -383,7 +396,7 @@ export function initViewer(canvas) { renderer.domElement.addEventListener('touchend', (e) => { if (e.touches.length < 2) { _pinchDist = null; - _pinchMid = null; + _pinchActive = false; controls.enabled = true; } }); @@ -581,7 +594,7 @@ export function setMeshGeometry(geometry) { export function getGrid() { return grid; } function fitCamera(sphere) { - const sz = renderer.getSize(new THREE.Vector2()); + const sz = renderer.getSize(_tmpSize); const aspect = sz.x / sz.y; const halfH = sphere.radius * 1.4; @@ -602,7 +615,7 @@ function fitCamera(sphere) { perspCamera.updateProjectionMatrix(); // Isometric-ish view from front-right-above in Z-up space - const dir = new THREE.Vector3(0.6, -1.2, 0.8).normalize(); + const dir = _tmpV5.set(0.6, -1.2, 0.8).normalize(); controls.target.copy(sphere.center); // Ortho: position doesn't affect rendered size, just direction @@ -650,13 +663,13 @@ export function setProjection(perspective) { const halfH = orthoCamera.top / orthoCamera.zoom; const fovRad = THREE.MathUtils.degToRad(perspCamera.fov / 2); const dist = halfH / Math.tan(fovRad); - const dir = new THREE.Vector3().subVectors(oldCam.position, controls.target).normalize(); + const dir = _tmpV5.subVectors(oldCam.position, controls.target).normalize(); newCam.position.copy(controls.target).addScaledVector(dir, dist); } camera = newCam; controls.object = camera; - const sz = renderer.getSize(new THREE.Vector2()); + const sz = renderer.getSize(_tmpSize); const aspect = sz.x / sz.y; if (perspective) { perspCamera.aspect = aspect; @@ -672,13 +685,13 @@ export function setProjection(perspective) { } export function setSceneBackground(hexColor) { - if (scene) scene.background = new THREE.Color(hexColor); + if (scene) scene.background.set(hexColor); requestRender(); } export function setViewerTheme(isLight) { if (!scene) return; - scene.background = new THREE.Color(isLight ? 0xf0f0f5 : 0x111114); + scene.background.set(isLight ? 0xf0f0f5 : 0x111114); const savedZ = grid ? grid.position.z : 0; if (grid) { scene.remove(grid);