import {Injectable} from '@angular/core'; import * as THREE from 'three'; import {RendererService} from './renderer.service'; import {Particle, ParticleData, ParticleInfo, ParticleType} from '../models/particle.model'; /** * Service responsible for managing particles in the scene */ @Injectable({ providedIn: 'root' }) export class ParticleManagerService { private particles: THREE.Mesh[] = []; private particleData: ParticleData = { particle_name: '', display_name: '', particle_type: ParticleType.TRAIL, lore: '', display_item: 'DIRT', permission: '', package_permission: '', frame_delay: 1, repeat: 1, repeat_delay: 0, random_offset: 0, stationary: true, frames: { 'frame1': [] } }; private currentFrame: string = 'frame1'; private frames: string[] = ['frame1']; private selectedColor: string = '#ff0000'; private selectedParticle: Particle = Particle.DUST; private selectedSize: number = 1; constructor(private rendererService: RendererService) { } /** * Adds a particle at the specified position */ addParticle(x: number, y: number, z: number): void { // Create a visual representation of the particle const particleGeometry = new THREE.SphereGeometry(0.03, 16, 16); const particleMaterial = new THREE.MeshBasicMaterial({color: this.selectedColor}); const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial); particleMesh.position.set(x, y, z); this.rendererService.scene.add(particleMesh); this.particles.push(particleMesh); // Add to particle data const hexColor = this.selectedColor.replace('#', ''); //TODO make this work for more than just type DUST const particleInfo: ParticleInfo = { particle_type: this.selectedParticle, x: x, y: y, z: z, color: hexColor, // color_gradient_end: hexColor2, extra: 1, size: this.selectedSize }; if (!this.particleData.frames[this.currentFrame]) { this.particleData.frames[this.currentFrame] = []; } this.particleData.frames[this.currentFrame].push(particleInfo); } /** * Clears all particle visuals from the scene */ clearParticleVisuals(): void { for (const particle of this.particles) { this.rendererService.scene.remove(particle); } this.particles = []; } /** * Renders particles for a specific frame */ renderFrameParticles(frameId: string): void { if (!this.particleData.frames[frameId]) return; for (const particleInfo of this.particleData.frames[frameId]) { const particleGeometry = new THREE.SphereGeometry(0.03, 16, 16); const color = this.getColor(particleInfo); const particleMaterial = new THREE.MeshBasicMaterial({color}); const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial); particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z); this.rendererService.scene.add(particleMesh); this.particles.push(particleMesh); } } /** * Removes a particle from a specific frame */ removeParticle(frameId: string, index: number): void { if (this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index) { this.particleData.frames[frameId].splice(index, 1); // Update visuals if this is the current frame if (frameId === this.currentFrame) { this.clearParticleVisuals(); this.renderFrameParticles(frameId); } } } highlightParticle(frameId: string, index: number): void { if (!(this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index)) { return; } const particleInfo = this.particleData.frames[frameId][index]; const color = this.getColor(particleInfo); const particleMaterial = new THREE.MeshBasicMaterial({color}); const particleGeometry = new THREE.SphereGeometry(0.03, 16, 16); const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial); particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z); this.rendererService.scene.add(particleMesh); this.particles.push(particleMesh); this.animatePulse(particleMesh, 3, () => { this.rendererService.scene.remove(particleMesh); this.clearParticleVisuals(); this.renderFrameParticles(this.currentFrame); }); } private getColor(particleInfo: ParticleInfo) { if (particleInfo.color) { const r = parseInt(particleInfo.color.substring(0, 2), 16) / 255; const g = parseInt(particleInfo.color.substring(2, 4), 16) / 255; const b = parseInt(particleInfo.color.substring(4, 6), 16) / 255; return new THREE.Color(r, g, b); } else { return new THREE.Color(255, 0, 0); } } private animatePulse(mesh: THREE.Mesh, cycles: number, onComplete: () => void): void { const duration = 300; const maxScale = 0.08 / 0.03; const startTime = performance.now(); const animate = (time: number) => { const elapsed = (time - startTime) % duration; const t = elapsed / (duration / 2); const scaleFactor = t <= 1 ? 1 + (maxScale - 1) * t : maxScale - (maxScale - 1) * (t - 1); mesh.scale.setScalar(scaleFactor); if (time - startTime >= duration * cycles) { onComplete(); } else { requestAnimationFrame(animate); } }; requestAnimationFrame(animate); } /** * Sets the selected color for new particles */ setSelectedColor(color: string): void { this.selectedColor = color; } /** * Gets the selected color */ getSelectedColor(): string { return this.selectedColor; } /** * Gets the particle data */ getParticleData(): ParticleData { return this.particleData; } /** * Sets the particle data */ setParticleData(data: ParticleData): void { this.particleData = data; } /** * Gets the current frame */ getCurrentFrame(): string { return this.currentFrame; } /** * Sets the current frame */ setCurrentFrame(frameId: string): void { this.currentFrame = frameId; } /** * Gets all frames */ getFrames(): string[] { return this.frames; } /** * Sets all frames */ setFrames(frames: string[]): void { this.frames = frames; } /** * Generates JSON output of the particle data */ generateJson(): string { return JSON.stringify(this.particleData, null, 2); } }