import * as THREE from 'three'; import type { Point } from '@/types'; import type { SpriteEffectArea } from '@/types/spriteEffect'; import { SpriteEffectInstance } from './SpriteEffectInstance'; /** * 터치/마우스 상태 (스프라이트 이펙트 전용) */ export interface SpriteEffectTouchState { /** 마우스/터치 위치 (정규화 좌표, null이면 미접촉) */ position: Point | null; /** 드래그 중 여부 */ isDragging: boolean; } /** * 점이 원 안에 있는지 확인 */ const isPointInCircle = (point: Point, center: Point, radius: number, resolution?: { x: number; y: number }): boolean => { const aspect = resolution ? resolution.x / resolution.y : 1; const dx = point.x - center.x; const dy = (point.y - center.y) / aspect; return dx * dx + dy * dy <= radius * radius; }; /** * 스프라이트 이펙트 전체 관리자 * ImageDistortion 컴포넌트에서 생성하여 사용하는 최상위 진입점 * 왜곡 영역(DistortionArea)과 독립적으로 이펙트 영역을 관리 */ export class SpriteEffectManager { /** 모든 이펙트 메쉬를 담는 그룹 */ private effectGroup: THREE.Group; /** 영역ID+이펙트ID → 인스턴스 맵 */ private instances: Map = new Map(); /** 이전 프레임에서 터치 중이던 영역 ID 세트 (버스트 감지용) */ private previousTouchingAreas: Set = new Set(); constructor() { this.effectGroup = new THREE.Group(); this.effectGroup.renderOrder = 1; } /** * Three.js 씬에 이펙트 그룹 추가 */ attachToScene(scene: THREE.Scene): void { scene.add(this.effectGroup); } /** * 이펙트 영역 설정 변경을 감지하여 인스턴스 생성/제거 */ syncEffects(effectAreas: SpriteEffectArea[]): void { console.log('[SpriteEffectManager] syncEffects 호출:', effectAreas.length, '개 영역'); const activeKeys = new Set(); for (const area of effectAreas) { for (const effectConfig of area.effects) { const key = `${area.id}::${effectConfig.id}`; activeKeys.add(key); // 이미 존재하면 설정만 업데이트 const existing = this.instances.get(key); if (existing) { existing.updateConfig(effectConfig); continue; } // 새 인스턴스 생성 console.log('[SpriteEffectManager] 인스턴스 생성:', key, effectConfig.emoji ?? effectConfig.spriteUrl); const instance = new SpriteEffectInstance(effectConfig); this.instances.set(key, instance); this.effectGroup.add(instance.group); } } // 더 이상 사용되지 않는 인스턴스 제거 for (const [key, instance] of this.instances) { if (!activeKeys.has(key)) { instance.dispose(); this.effectGroup.remove(instance.group); this.instances.delete(key); } } } /** * 매 프레임 업데이트 * @param effectAreas 이펙트 영역 배열 * @param deltaTime 초 단위 프레임 시간 * @param touchState 마우스/터치 상태 */ update(effectAreas: SpriteEffectArea[], deltaTime: number, touchState: SpriteEffectTouchState, resolution?: { x: number; y: number }): void { // 현재 터치 중인 영역 감지 const currentTouchingAreas = new Set(); if (touchState.isDragging && touchState.position) { for (const area of effectAreas) { const radius = area.radius ?? 0.1; if (isPointInCircle(touchState.position, area.position, radius, resolution)) { currentTouchingAreas.add(area.id); } } } // 각 영역의 이펙트 업데이트 for (const area of effectAreas) { for (const effectConfig of area.effects) { const key = `${area.id}::${effectConfig.id}`; const instance = this.instances.get(key); if (!instance) continue; // touch 이펙트: 새로 터치된 영역이면 버스트 if (effectConfig.trigger === 'touch') { const isNewTouch = currentTouchingAreas.has(area.id) && !this.previousTouchingAreas.has(area.id); if (isNewTouch) { instance.triggerBurst(touchState.position ?? area.position); } } // 매 프레임 업데이트 (ambient 방출 + 파티클 물리) instance.update(deltaTime, area.position, resolution); } } // 터치 상태 갱신 this.previousTouchingAreas = currentTouchingAreas; } /** * 리소스 정리 */ dispose(): void { for (const [, instance] of this.instances) { instance.dispose(); } this.instances.clear(); this.previousTouchingAreas.clear(); // effectGroup을 부모에서 제거 if (this.effectGroup.parent) { this.effectGroup.parent.remove(this.effectGroup); } } }