import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import {CommonModule} from '@angular/common'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MatButtonModule} from '@angular/material/button'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatSelectModule} from '@angular/material/select'; import {MatSliderModule} from '@angular/material/slider'; import {MatCheckboxModule} from '@angular/material/checkbox'; import {MatTabsModule} from '@angular/material/tabs'; import {MatCardModule} from '@angular/material/card'; import {MatIconModule} from '@angular/material/icon'; import {HeaderComponent} from '../header/header.component'; import * as THREE from 'three'; import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js'; // Define particle types export enum ParticleType { REDSTONE = 'REDSTONE', // Other particle types can be added later } // Interface for particle information interface ParticleInfo { particle_type: string; x: number; y: number; z: number; color: string; extra: number; } // Interface for the complete particle data interface ParticleData { particle_name: string; display_name: string; particle_type: string; lore: string; display_item: string; permission: string; package_permission: string; frame_delay: number; repeat: number; repeat_delay: number; random_offset: number; stationary: boolean; frames: { [frameId: string]: ParticleInfo[]; }; } @Component({ selector: 'app-particles', standalone: true, imports: [ CommonModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatInputModule, MatFormFieldModule, MatSelectModule, MatSliderModule, MatCheckboxModule, MatTabsModule, MatCardModule, MatIconModule, HeaderComponent ], templateUrl: './particles.component.html', styleUrl: './particles.component.scss' }) export class ParticlesComponent implements OnInit, AfterViewInit { @ViewChild('rendererContainer') rendererContainer!: ElementRef; @ViewChild('planeSlider') planeSlider!: ElementRef; // Three.js objects private scene!: THREE.Scene; private camera!: THREE.PerspectiveCamera; private renderer!: THREE.WebGLRenderer; private controls!: OrbitControls; private playerModel!: THREE.Group; private intersectionPlane!: THREE.Mesh; private particles: THREE.Mesh[] = []; private raycaster = new THREE.Raycaster(); private mouse = new THREE.Vector2(); // Particle data public particleData: ParticleData = { particle_name: '', display_name: '', particle_type: ParticleType.REDSTONE, lore: '', display_item: 'REDSTONE', permission: '', package_permission: '', frame_delay: 1, repeat: 1, repeat_delay: 0, random_offset: 0, stationary: true, frames: { 'frame1': [] } }; // UI state public currentFrame: string = 'frame1'; public planePosition: number = 8; // Position in 1/16th of a block public selectedColor: string = '#ff0000'; public particleTypes = Object.values(ParticleType); public frames: string[] = ['frame1']; constructor() { } ngOnInit(): void { // Initialize component } ngAfterViewInit(): void { this.initThreeJS(); this.animate(); } // Initialize Three.js scene private initThreeJS(): void { // Create scene this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0xf0f0f0); // Get container dimensions const containerWidth = this.rendererContainer.nativeElement.clientWidth; const containerHeight = 400; // Fixed height as defined in CSS // Create camera this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000); this.camera.position.set(0, 1, 3); this.camera.lookAt(0, 1, 0); // Create renderer this.renderer = new THREE.WebGLRenderer({antialias: true}); this.renderer.setSize(containerWidth, containerHeight); // Center the canvas in the container this.renderer.domElement.style.display = 'block'; this.renderer.domElement.style.margin = 'auto'; this.rendererContainer.nativeElement.appendChild(this.renderer.domElement); // Initialize orbit controls this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.enableDamping = true; // Add smooth damping effect this.controls.dampingFactor = 0.05; this.controls.minDistance = 2; // Minimum zoom distance this.controls.maxDistance = 10; // Maximum zoom distance this.controls.target.set(0, 1, 0); // Set target to player's center this.controls.update(); // Add lights const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); this.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(1, 1, 1); this.scene.add(directionalLight); // Create player model (simple representation) this.createPlayerModel(); // Create intersection plane this.createIntersectionPlane(); // Add event listeners this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this)); this.renderer.domElement.addEventListener('mouseup', this.onMouseUp.bind(this)); this.renderer.domElement.addEventListener('mousemove', this.onMouseMove.bind(this)); window.addEventListener('resize', this.onWindowResize.bind(this)); } // Create a simple player model private createPlayerModel(): void { this.playerModel = new THREE.Group(); // Head const headGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5); const headMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa}); const head = new THREE.Mesh(headGeometry, headMaterial); head.position.y = 1.35; this.playerModel.add(head); // Body const bodyGeometry = new THREE.BoxGeometry(0.5, 0.7, 0.25); const bodyMaterial = new THREE.MeshLambertMaterial({color: 0x0000ff}); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.position.y = 0.75; this.playerModel.add(body); // Arms const armGeometry = new THREE.BoxGeometry(0.2, 0.7, 0.25); const armMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa}); const leftArm = new THREE.Mesh(armGeometry, armMaterial); leftArm.position.set(-0.35, 0.75, 0); this.playerModel.add(leftArm); const rightArm = new THREE.Mesh(armGeometry, armMaterial); rightArm.position.set(0.35, 0.75, 0); this.playerModel.add(rightArm); // Legs const legGeometry = new THREE.BoxGeometry(0.25, 0.7, 0.25); const legMaterial = new THREE.MeshLambertMaterial({color: 0x000000}); const leftLeg = new THREE.Mesh(legGeometry, legMaterial); leftLeg.position.set(-0.125, 0.15, 0); this.playerModel.add(leftLeg); const rightLeg = new THREE.Mesh(legGeometry, legMaterial); rightLeg.position.set(0.125, 0.15, 0); this.playerModel.add(rightLeg); this.scene.add(this.playerModel); } // Create the intersection plane private createIntersectionPlane(): void { const planeGeometry = new THREE.PlaneGeometry(3, 3); const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00AA00, transparent: true, opacity: 0.05, side: THREE.DoubleSide }); this.intersectionPlane = new THREE.Mesh(planeGeometry, planeMaterial); this.intersectionPlane.position.z = 0; // Center the plane vertically with the player (player is about 2 blocks tall) this.intersectionPlane.position.y = 1; this.scene.add(this.intersectionPlane); } // Update plane position based on slider public updatePlanePosition(event: Event): void { // Access the value from the slider element const slider = event.target as HTMLInputElement; this.planePosition = Number(slider.value); // Convert from 1/16th block to Three.js units const zPosition = (this.planePosition / 16) - 0.5; // Center at 0 this.intersectionPlane.position.z = zPosition; } // Track if mouse is being dragged private isDragging = false; private mouseDownTime = 0; // Handle mouse down event private onMouseDown(event: MouseEvent): void { this.isDragging = false; this.mouseDownTime = Date.now(); } // Handle mouse up event private onMouseUp(event: MouseEvent): void { // If mouse was down for less than 200ms and didn't move much, consider it a click, not a drag if (Date.now() - this.mouseDownTime < 200 && !this.isDragging) { this.handlePlaneClick(event); } this.isDragging = false; } // Handle mouse move event private onMouseMove(event: MouseEvent): void { // If mouse moves while button is pressed, it's a drag if (event.buttons > 0) { this.isDragging = true; } } // Handle mouse click on the plane private handlePlaneClick(event: MouseEvent): void { // Calculate mouse position in normalized device coordinates const rect = this.renderer.domElement.getBoundingClientRect(); this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; // Update the picking ray with the camera and mouse position this.raycaster.setFromCamera(this.mouse, this.camera); // Calculate objects intersecting the picking ray const intersects = this.raycaster.intersectObject(this.intersectionPlane); if (intersects.length > 0) { const point = intersects[0].point; this.addParticle(point.x, point.y, point.z); } } // Add a particle at the specified position private 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.scene.add(particleMesh); this.particles.push(particleMesh); // Add to particle data const hexColor = this.selectedColor.replace('#', ''); const r = parseInt(hexColor.substring(0, 2), 16) / 255; const g = parseInt(hexColor.substring(2, 4), 16) / 255; const b = parseInt(hexColor.substring(4, 6), 16) / 255; const particleInfo: ParticleInfo = { particle_type: ParticleType.REDSTONE, x: x, y: y, z: z, color: `${r},${g},${b}`, extra: 1 }; if (!this.particleData.frames[this.currentFrame]) { this.particleData.frames[this.currentFrame] = []; } this.particleData.frames[this.currentFrame].push(particleInfo); } // Handle window resize private onWindowResize(): void { const containerWidth = this.rendererContainer.nativeElement.clientWidth; const containerHeight = 400; // Fixed height as defined in CSS this.camera.aspect = containerWidth / containerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(containerWidth, containerHeight); } // Animation loop private animate(): void { requestAnimationFrame(this.animate.bind(this)); // Update controls if (this.controls) { this.controls.update(); } // Update plane orientation based on camera position if (this.intersectionPlane && this.camera) { // Calculate the angle between camera and player (in the XZ plane) const cameraAngle = Math.atan2( this.camera.position.x, this.camera.position.z ); // Determine which quadrant the camera is in with a 45-degree offset // Adding Math.PI/4 (45 degrees) to the angle before determining the quadrant // This shifts the quadrant boundaries by 45 degrees const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4; // Rotate the plane to face the camera if (quadrant === 0) { this.intersectionPlane.rotation.y = 0; // Camera in front this.updateFrameMaterial(0x00AA00); } else if (quadrant === 1) { this.intersectionPlane.rotation.y = Math.PI / 2; // Camera on right this.updateFrameMaterial(0x0000AA); } else if (quadrant === 2) { this.intersectionPlane.rotation.y = Math.PI; // Camera behind this.updateFrameMaterial(0x00AA00); } else { this.intersectionPlane.rotation.y = -Math.PI / 2; // Camera on left this.updateFrameMaterial(0x0000AA); } } this.renderer.render(this.scene, this.camera); } public updateFrameMaterial(color: number) { this.intersectionPlane.material = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.05, side: THREE.DoubleSide }); } // Add a new frame public addFrame(): void { const frameId = `frame${this.frames.length + 1}`; this.frames.push(frameId); this.particleData.frames[frameId] = []; this.currentFrame = frameId; this.clearParticleVisuals(); } // Switch to a different frame public switchFrame(frameId: string): void { this.currentFrame = frameId; this.clearParticleVisuals(); this.renderFrameParticles(frameId); } // Clear particle visuals from the scene private clearParticleVisuals(): void { for (const particle of this.particles) { this.scene.remove(particle); } this.particles = []; } // Render particles for a specific frame private 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); // Parse color const colorParts = particleInfo.color.split(','); const color = new THREE.Color( parseFloat(colorParts[0]), parseFloat(colorParts[1]), parseFloat(colorParts[2]) ); const particleMaterial = new THREE.MeshBasicMaterial({color}); const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial); particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z); this.scene.add(particleMesh); this.particles.push(particleMesh); } } // Generate JSON output public generateJson(): string { return JSON.stringify(this.particleData, null, 2); } // Remove a particle public 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); } } } // Remove a frame public removeFrame(frameId: string): void { const index = this.frames.indexOf(frameId); if (index !== -1) { this.frames.splice(index, 1); delete this.particleData.frames[frameId]; // Switch to first frame if we removed the current one if (frameId === this.currentFrame && this.frames.length > 0) { this.switchFrame(this.frames[0]); } else if (this.frames.length === 0) { // If no frames left, add one this.addFrame(); } } } }