476 lines
15 KiB
TypeScript
476 lines
15 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
}
|
|
}
|