Add particle creator component with Three.js integration CREDITS: Junie
Introduced the `ParticlesComponent` for creating and visualizing particles using Three.js. Added associated routes, HTML, SCSS, and tests. Updated `package.json` to include `three` and `@types/three` as dependencies. Adjusted app navigation to include the particles page.
This commit is contained in:
parent
32a454c034
commit
52d8658be3
|
|
@ -22,8 +22,10 @@
|
|||
"@angular/platform-browser": "^19.2.0",
|
||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||
"@angular/router": "^19.2.0",
|
||||
"@types/three": "^0.177.0",
|
||||
"ngx-cookie-service": "^19.1.2",
|
||||
"rxjs": "~7.8.0",
|
||||
"three": "^0.177.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ export const routes: Routes = [
|
|||
path: '',
|
||||
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
||||
},
|
||||
{
|
||||
path: 'particles',
|
||||
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
||||
|
|
@ -105,6 +109,8 @@ export const routes: Routes = [
|
|||
path: 'forms',
|
||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||
},
|
||||
{
|
||||
path: 'particles',
|
||||
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
189
frontend/src/app/particles/particles.component.html
Normal file
189
frontend/src/app/particles/particles.component.html
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<app-header current_page="particles" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Particle Creator</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="column">
|
||||
<div class="renderer-section row column">
|
||||
<div #rendererContainer class="renderer-container column row"></div>
|
||||
<div class="plane-controls">
|
||||
<label>Plane Position (Z-axis):</label>
|
||||
<mat-slider min="0" max="16" step="1" #planeSlider>
|
||||
<input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
|
||||
</mat-slider>
|
||||
<span>{{ planePosition }}/16 blocks</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column controls-section">
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Particle Properties</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Particle Name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.particle_name" placeholder="Enter particle name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Display Name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.display_name" placeholder="Enter display name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Particle Type</mat-label>
|
||||
<mat-select [(ngModel)]="particleData.particle_type">
|
||||
<mat-option *ngFor="let type of particleTypes" [value]="type">{{ type }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Lore</mat-label>
|
||||
<textarea matInput [(ngModel)]="particleData.lore" placeholder="Enter lore"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Display Item</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.display_item" placeholder="Enter display item">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Permission</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.permission" placeholder="Enter permission">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Package Permission</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.package_permission" placeholder="Enter package permission">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Frame Delay</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.frame_delay" placeholder="Enter frame delay">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Repeat</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.repeat" placeholder="Enter repeat count">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Repeat Delay</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.repeat_delay"
|
||||
placeholder="Enter repeat delay">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Random Offset</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.random_offset"
|
||||
placeholder="Enter random offset">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-checkbox [(ngModel)]="particleData.stationary">Stationary</mat-checkbox>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<mat-card class="color-picker-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Particle Color</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="color-picker">
|
||||
<input type="color" [(ngModel)]="selectedColor">
|
||||
<span>Selected Color: {{ selectedColor }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div class="column frames-section">
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Frames</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="frames-container">
|
||||
<mat-tab-group [selectedIndex]="frames.indexOf(currentFrame)"
|
||||
(selectedIndexChange)="switchFrame(frames[$event])">
|
||||
<mat-tab *ngFor="let frameId of frames" [label]="frameId">
|
||||
<div class="frame-content">
|
||||
<h3>Particles in {{ frameId }}</h3>
|
||||
<div class="particles-list">
|
||||
<div *ngFor="let particle of particleData.frames[frameId]; let i = index" class="particle-item">
|
||||
<span>Particle {{ i + 1 }}: ({{ particle.x.toFixed(2) }}, {{ particle.y.toFixed(2) }}
|
||||
, {{ particle.z.toFixed(2) }})</span>
|
||||
<button mat-icon-button color="warn" (click)="removeParticle(frameId, i)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!particleData.frames[frameId] || particleData.frames[frameId].length === 0"
|
||||
class="no-particles">
|
||||
No particles in this frame. Click on the plane to add particles.
|
||||
</div>
|
||||
</div>
|
||||
<div class="frame-actions">
|
||||
<button mat-raised-button color="warn" (click)="removeFrame(frameId)"
|
||||
[disabled]="frames.length <= 1">
|
||||
Remove Frame
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<div class="add-frame">
|
||||
<button mat-raised-button color="primary" (click)="addFrame()">
|
||||
Add New Frame
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div class="column json-output">
|
||||
|
||||
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>JSON Output</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<pre>{{ generateJson() }}</pre>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
125
frontend/src/app/particles/particles.component.scss
Normal file
125
frontend/src/app/particles/particles.component.scss
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
.renderer-section {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.renderer-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.plane-controls {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.plane-controls mat-slider {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-picker-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.color-picker input[type="color"] {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.frames-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.frames-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.frame-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.particles-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.particle-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.particle-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.no-particles {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.frame-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.add-frame {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.json-output {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.json-output pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
max-height: 300px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
23
frontend/src/app/particles/particles.component.spec.ts
Normal file
23
frontend/src/app/particles/particles.component.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ParticlesComponent } from './particles.component';
|
||||
|
||||
describe('ParticlesComponent', () => {
|
||||
let component: ParticlesComponent;
|
||||
let fixture: ComponentFixture<ParticlesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ParticlesComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ParticlesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
382
frontend/src/app/particles/particles.component.ts
Normal file
382
frontend/src/app/particles/particles.component.ts
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
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';
|
||||
|
||||
// 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 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);
|
||||
|
||||
// Create camera
|
||||
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 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(window.innerWidth * 0.6, window.innerHeight * 0.6);
|
||||
this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
|
||||
|
||||
// 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('click', this.onMouseClick.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(2, 2);
|
||||
const planeMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0x00ff00,
|
||||
transparent: true,
|
||||
opacity: 0.2,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
this.intersectionPlane = new THREE.Mesh(planeGeometry, planeMaterial);
|
||||
this.intersectionPlane.position.z = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
// Handle mouse click on the plane
|
||||
private onMouseClick(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 {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(window.innerWidth * 0.6, window.innerHeight * 0.6);
|
||||
}
|
||||
|
||||
// Animation loop
|
||||
private animate(): void {
|
||||
requestAnimationFrame(this.animate.bind(this));
|
||||
|
||||
// Rotate player model slightly for better view
|
||||
if (this.playerModel) {
|
||||
this.playerModel.rotation.y += 0.005;
|
||||
}
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user