import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; let controls; let _canvas; let _camera = null; let _timmyGroup = null; let _applySlap = null; const _raycaster = new THREE.Raycaster(); const _pointer = new THREE.Vector2(); const _noCtxMenu = e => e.preventDefault(); // ── registerSlapTarget ──────────────────────────────────────────────────────── // Call after initAgents() with Timmy's group and the applySlap function. // The capture-phase pointerdown listener will hit-test against the group and // call applySlap(hitPoint) — also suppressing the OrbitControls drag. export function registerSlapTarget(timmyGroup, applyFn) { _timmyGroup = timmyGroup; _applySlap = applyFn; } export function initInteraction(camera, renderer) { _camera = camera; controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.screenSpacePanning = false; controls.minDistance = 5; controls.maxDistance = 80; controls.maxPolarAngle = Math.PI / 2.1; controls.target.set(0, 0, 0); controls.update(); _canvas = renderer.domElement; _canvas.addEventListener('contextmenu', _noCtxMenu); // Capture phase so we intercept before OrbitControls' bubble-phase handler. // If Timmy is hit we call stopImmediatePropagation() to suppress the orbit drag. _canvas.addEventListener('pointerdown', _onPointerDown, { capture: true }); // touchstart fallback for older mobile browsers that lack Pointer Events if (!window.PointerEvent) { _canvas.addEventListener('touchstart', _onTouchStart, { capture: true, passive: false }); } } function _hitTest(clientX, clientY) { if (!_timmyGroup || !_applySlap || !_camera) return false; const rect = _canvas.getBoundingClientRect(); _pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1; _pointer.y = ((clientY - rect.top) / rect.height) * -2 + 1; _raycaster.setFromCamera(_pointer, _camera); const hits = _raycaster.intersectObject(_timmyGroup, true); if (hits.length > 0) { _applySlap(hits[0].point); // 150 ms lockout to avoid accidental orbit drag immediately after a slap if (controls) { controls.enabled = false; setTimeout(() => { if (controls) controls.enabled = true; }, 150); } return true; } return false; } function _onPointerDown(event) { if (_hitTest(event.clientX, event.clientY)) { event.stopImmediatePropagation(); // block OrbitControls drag } } function _onTouchStart(event) { if (!event.touches || event.touches.length === 0) return; const t = event.touches[0]; if (_hitTest(t.clientX, t.clientY)) { event.stopImmediatePropagation(); event.preventDefault(); // suppress subsequent mouse events (ghost click) } } export function updateControls() { if (controls) controls.update(); } /** * Dispose OrbitControls event listeners. * Called before context-loss teardown. */ export function disposeInteraction() { if (_canvas) { _canvas.removeEventListener('contextmenu', _noCtxMenu); _canvas.removeEventListener('pointerdown', _onPointerDown, { capture: true }); if (!window.PointerEvent) { _canvas.removeEventListener('touchstart', _onTouchStart, { capture: true }); } _canvas = null; } if (controls) { controls.dispose(); controls = null; } _camera = null; _timmyGroup = null; _applySlap = null; }