Compare commits
5 Commits
d6faaba01c
...
023ae809ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
023ae809ef | ||
|
|
3a6f137c9a | ||
|
|
cb8447a096 | ||
|
|
3e98e1a498 | ||
|
|
4c31a91bb4 |
|
|
@ -0,0 +1,41 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FramesComponent } from './frames.component';
|
||||||
|
|
||||||
|
describe('FramesComponent', () => {
|
||||||
|
let component: FramesComponent;
|
||||||
|
let fixture: ComponentFixture<FramesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [FramesComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FramesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatButton, MatIconButton} from "@angular/material/button";
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||||
|
import {MatTab, MatTabGroup} from "@angular/material/tabs";
|
||||||
|
import {NgForOf, NgIf} from "@angular/common";
|
||||||
|
import {ParticleData} from '../../models/particle.model';
|
||||||
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||||
|
import {FrameManagerService} from '../../services/frame-manager.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-frames',
|
||||||
|
imports: [
|
||||||
|
MatButton,
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
MatIcon,
|
||||||
|
MatIconButton,
|
||||||
|
MatTab,
|
||||||
|
MatTabGroup,
|
||||||
|
NgForOf,
|
||||||
|
NgIf
|
||||||
|
],
|
||||||
|
templateUrl: './frames.component.html',
|
||||||
|
styleUrl: './frames.component.scss'
|
||||||
|
})
|
||||||
|
export class FramesComponent {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private particleManagerService: ParticleManagerService,
|
||||||
|
private frameManagerService: FrameManagerService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the particle data
|
||||||
|
*/
|
||||||
|
public get particleData(): ParticleData {
|
||||||
|
return this.particleManagerService.getParticleData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current frame
|
||||||
|
*/
|
||||||
|
public get currentFrame(): string {
|
||||||
|
return this.particleManagerService.getCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all frames
|
||||||
|
*/
|
||||||
|
public get frames(): string[] {
|
||||||
|
return this.particleManagerService.getFrames();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new frame
|
||||||
|
*/
|
||||||
|
public addFrame(): void {
|
||||||
|
this.frameManagerService.addFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different frame
|
||||||
|
*/
|
||||||
|
public switchFrame(frameId: string): void {
|
||||||
|
this.frameManagerService.switchFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a particle
|
||||||
|
*/
|
||||||
|
public removeParticle(frameId: string, index: number): void {
|
||||||
|
this.particleManagerService.removeParticle(frameId, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a frame
|
||||||
|
*/
|
||||||
|
public removeFrame(frameId: string): void {
|
||||||
|
this.frameManagerService.removeFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {ParticleComponent} from './particle.component';
|
||||||
|
|
||||||
|
describe('ParticleComponent', () => {
|
||||||
|
let component: ParticleComponent;
|
||||||
|
let fixture: ComponentFixture<ParticleComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ParticleComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ParticleComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-particle',
|
||||||
|
imports: [
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './particle.component.html',
|
||||||
|
styleUrl: './particle.component.scss'
|
||||||
|
})
|
||||||
|
export class ParticleComponent {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private particleManagerService: ParticleManagerService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the selected color
|
||||||
|
*/
|
||||||
|
public get selectedColor(): string {
|
||||||
|
return this.particleManagerService.getSelectedColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the selected color
|
||||||
|
*/
|
||||||
|
public set selectedColor(color: string) {
|
||||||
|
this.particleManagerService.setSelectedColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PropertiesComponent } from './properties.component';
|
||||||
|
|
||||||
|
describe('PropertiesComponent', () => {
|
||||||
|
let component: PropertiesComponent;
|
||||||
|
let fixture: ComponentFixture<PropertiesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PropertiesComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PropertiesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||||
|
import {MatCheckbox} from "@angular/material/checkbox";
|
||||||
|
import {MatFormField, MatInput, MatLabel} from "@angular/material/input";
|
||||||
|
import {NgForOf} from "@angular/common";
|
||||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {ParticleData, ParticleType} from '../../models/particle.model';
|
||||||
|
import {MatSelect} from '@angular/material/select';
|
||||||
|
import {MatOption} from '@angular/material/core';
|
||||||
|
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-particle-properties',
|
||||||
|
imports: [
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
MatCheckbox,
|
||||||
|
MatFormField,
|
||||||
|
MatInput,
|
||||||
|
MatLabel,
|
||||||
|
MatOption,
|
||||||
|
MatSelect,
|
||||||
|
NgForOf,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './properties.component.html',
|
||||||
|
styleUrl: './properties.component.scss'
|
||||||
|
})
|
||||||
|
export class PropertiesComponent {
|
||||||
|
public particleTypes = Object.values(ParticleType);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private particleManagerService: ParticleManagerService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public get particleData(): ParticleData {
|
||||||
|
return this.particleManagerService.getParticleData();
|
||||||
|
}
|
||||||
|
}
|
||||||
40
frontend/src/app/particles/models/particle.model.ts
Normal file
40
frontend/src/app/particles/models/particle.model.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* Defines the types of particles available in the system
|
||||||
|
*/
|
||||||
|
export enum ParticleType {
|
||||||
|
REDSTONE = 'REDSTONE',
|
||||||
|
// Other particle types can be added later
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single particle's information
|
||||||
|
*/
|
||||||
|
export interface ParticleInfo {
|
||||||
|
particle_type: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
color: string;
|
||||||
|
extra: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the complete particle data structure
|
||||||
|
*/
|
||||||
|
export 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[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -8,180 +8,32 @@
|
||||||
<main>
|
<main>
|
||||||
<section class="darkmodeSection">
|
<section class="darkmodeSection">
|
||||||
<section class="column">
|
<section class="column">
|
||||||
<div class="renderer-section row column">
|
<div class="renderer-section column">
|
||||||
<div #rendererContainer class="renderer-container column row"></div>
|
<div class="flex row">
|
||||||
|
<div class="flex side-column">
|
||||||
|
<app-particle-properties></app-particle-properties>
|
||||||
|
</div>
|
||||||
|
<div class="flex middle-column">
|
||||||
|
<div #rendererContainer class="renderer-container">
|
||||||
|
</div>
|
||||||
<div class="plane-controls">
|
<div class="plane-controls">
|
||||||
<label>Plane Position (Z-axis):</label>
|
<label>Plane Position (Z-axis):</label>
|
||||||
<mat-slider min="0" max="16" step="1" #planeSlider>
|
<mat-slider [min]="minOffset" [max]="maxOffset" step="1" #planeSlider>
|
||||||
<input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
|
<input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
|
||||||
</mat-slider>
|
</mat-slider>
|
||||||
<span>{{ planePosition }}/16 blocks</span>
|
<span>{{ planePosition }} offset from center</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="flex side-column">
|
||||||
<div class="column controls-section">
|
<app-particle></app-particle>
|
||||||
<mat-card>
|
<app-frames></app-frames>
|
||||||
<mat-card-header>
|
<div>
|
||||||
<mat-card-title>Particle Properties</mat-card-title>
|
<button mat-fab extended (click)="copyJson()">
|
||||||
</mat-card-header>
|
<mat-icon>content_copy</mat-icon>
|
||||||
<mat-card-content>
|
Copy JSON to clipboard
|
||||||
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.renderer-container {
|
.renderer-container {
|
||||||
width: 100%;
|
height: 1000px;
|
||||||
height: 400px;
|
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -16,6 +15,18 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-column {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle-column {
|
||||||
|
flex: 2;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.plane-controls {
|
.plane-controls {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
@ -30,12 +41,6 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-section {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 300px;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
.form-row {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
@ -43,86 +48,3 @@
|
||||||
mat-form-field {
|
mat-form-field {
|
||||||
width: 100%;
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
import {AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
import {MatButtonModule} from '@angular/material/button';
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
|
@ -11,43 +11,19 @@ import {MatTabsModule} from '@angular/material/tabs';
|
||||||
import {MatCardModule} from '@angular/material/card';
|
import {MatCardModule} from '@angular/material/card';
|
||||||
import {MatIconModule} from '@angular/material/icon';
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
import {HeaderComponent} from '../header/header.component';
|
import {HeaderComponent} from '../header/header.component';
|
||||||
import * as THREE from 'three';
|
|
||||||
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
|
|
||||||
|
|
||||||
// Define particle types
|
// Services
|
||||||
export enum ParticleType {
|
import {RendererService} from './services/renderer.service';
|
||||||
REDSTONE = 'REDSTONE',
|
import {PlayerModelService} from './services/player-model.service';
|
||||||
// Other particle types can be added later
|
import {IntersectionPlaneService} from './services/intersection-plane.service';
|
||||||
}
|
import {ParticleManagerService} from './services/particle-manager.service';
|
||||||
|
import {InputHandlerService} from './services/input-handler.service';
|
||||||
|
|
||||||
// Interface for particle information
|
// Models
|
||||||
interface ParticleInfo {
|
import {PropertiesComponent} from './components/properties/properties.component';
|
||||||
particle_type: string;
|
import {ParticleComponent} from './components/particle/particle.component';
|
||||||
x: number;
|
import {FramesComponent} from './components/frames/frames.component';
|
||||||
y: number;
|
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||||
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({
|
@Component({
|
||||||
selector: 'app-particles',
|
selector: 'app-particles',
|
||||||
|
|
@ -65,430 +41,122 @@ interface ParticleData {
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
HeaderComponent
|
HeaderComponent,
|
||||||
|
PropertiesComponent,
|
||||||
|
ParticleComponent,
|
||||||
|
FramesComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './particles.component.html',
|
templateUrl: './particles.component.html',
|
||||||
styleUrl: './particles.component.scss'
|
styleUrl: './particles.component.scss'
|
||||||
})
|
})
|
||||||
export class ParticlesComponent implements OnInit, AfterViewInit {
|
export class ParticlesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
|
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
|
||||||
@ViewChild('planeSlider') planeSlider!: ElementRef;
|
@ViewChild('planeSlider') planeSlider!: ElementRef;
|
||||||
|
|
||||||
// Three.js objects
|
constructor(
|
||||||
private scene!: THREE.Scene;
|
private rendererService: RendererService,
|
||||||
private camera!: THREE.PerspectiveCamera;
|
private playerModelService: PlayerModelService,
|
||||||
private renderer!: THREE.WebGLRenderer;
|
private intersectionPlaneService: IntersectionPlaneService,
|
||||||
private controls!: OrbitControls;
|
private particleManagerService: ParticleManagerService,
|
||||||
private playerModel!: THREE.Group;
|
private inputHandlerService: InputHandlerService,
|
||||||
private intersectionPlane!: THREE.Mesh;
|
private matSnackBar: MatSnackBar,
|
||||||
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() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize component
|
||||||
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// Initialize component
|
// No initialization needed here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Three.js scene after view is initialized
|
||||||
|
*/
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.initThreeJS();
|
this.initializeScene();
|
||||||
this.animate();
|
this.animate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Three.js scene
|
/**
|
||||||
private initThreeJS(): void {
|
* Clean up resources when component is destroyed
|
||||||
// Create scene
|
*/
|
||||||
this.scene = new THREE.Scene();
|
ngOnDestroy(): void {
|
||||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
// Clean up event listeners
|
||||||
|
if (this.rendererService.renderer) {
|
||||||
|
this.inputHandlerService.cleanup(this.rendererService.renderer.domElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get container dimensions
|
/**
|
||||||
const containerWidth = this.rendererContainer.nativeElement.clientWidth;
|
* Initialize the 3D scene and all related components
|
||||||
const containerHeight = 400; // Fixed height as defined in CSS
|
*/
|
||||||
|
private initializeScene(): void {
|
||||||
|
// Initialize renderer
|
||||||
|
this.rendererService.initializeRenderer(this.rendererContainer);
|
||||||
|
|
||||||
// Create camera
|
// Create player model
|
||||||
this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000);
|
this.playerModelService.createPlayerModel();
|
||||||
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
|
// Create intersection plane
|
||||||
this.createIntersectionPlane();
|
this.intersectionPlaneService.createIntersectionPlane();
|
||||||
|
|
||||||
// Add event listeners
|
// Initialize input handlers
|
||||||
this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
|
this.inputHandlerService.initializeInputHandlers(this.rendererService.renderer.domElement);
|
||||||
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 {
|
* Update plane position based on slider
|
||||||
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 {
|
public updatePlanePosition(event: Event): void {
|
||||||
// Access the value from the slider element
|
|
||||||
const slider = event.target as HTMLInputElement;
|
const slider = event.target as HTMLInputElement;
|
||||||
this.planePosition = Number(slider.value);
|
const value = Number(slider.value);
|
||||||
// Convert from 1/16th block to Three.js units
|
this.intersectionPlaneService.updatePlanePosition(value);
|
||||||
const position = (this.planePosition / 16) - 0.5; // Center at 0
|
|
||||||
|
|
||||||
// Apply position based on the plane's current rotation
|
|
||||||
// This ensures the plane always moves towards/away from the viewer
|
|
||||||
const rotation = this.intersectionPlane.rotation.y;
|
|
||||||
|
|
||||||
if (Math.abs(rotation) < 0.1 || Math.abs(rotation - Math.PI) < 0.1) {
|
|
||||||
// Camera in front (0) or behind (PI)
|
|
||||||
// For camera behind, we need to invert the direction
|
|
||||||
const direction = Math.abs(rotation) < 0.1 ? 1 : -1;
|
|
||||||
this.intersectionPlane.position.z = position * direction;
|
|
||||||
// Reset x position to avoid cumulative changes
|
|
||||||
this.intersectionPlane.position.x = 0;
|
|
||||||
} else {
|
|
||||||
// Camera on right (PI/2) or left (-PI/2)
|
|
||||||
// For camera on left, we need to invert the direction
|
|
||||||
const direction = rotation > 0 ? 1 : -1;
|
|
||||||
this.intersectionPlane.position.x = position * direction;
|
|
||||||
// Reset z position to avoid cumulative changes
|
|
||||||
this.intersectionPlane.position.z = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track if mouse is being dragged
|
/**
|
||||||
private isDragging = false;
|
* Get the current plane position
|
||||||
private mouseDownTime = 0;
|
*/
|
||||||
|
public get planePosition(): number {
|
||||||
// Handle mouse down event
|
return this.intersectionPlaneService.getPlanePosition();
|
||||||
private onMouseDown(event: MouseEvent): void {
|
|
||||||
this.isDragging = false;
|
|
||||||
this.mouseDownTime = Date.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle mouse up event
|
public set planePosition(newPlanePosition: number) {
|
||||||
private onMouseUp(event: MouseEvent): void {
|
this.intersectionPlaneService.updatePlanePosition(newPlanePosition);
|
||||||
// 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
|
public get maxOffset(): number {
|
||||||
private onMouseMove(event: MouseEvent): void {
|
return this.intersectionPlaneService.getMaxOffset();
|
||||||
// If mouse moves while button is pressed, it's a drag
|
|
||||||
if (event.buttons > 0) {
|
|
||||||
this.isDragging = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle mouse click on the plane
|
public get minOffset(): number {
|
||||||
private handlePlaneClick(event: MouseEvent): void {
|
return this.intersectionPlaneService.getMinOffset();
|
||||||
// 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 {
|
* Animation loop
|
||||||
// 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 {
|
private animate(): void {
|
||||||
requestAnimationFrame(this.animate.bind(this));
|
requestAnimationFrame(this.animate.bind(this));
|
||||||
|
|
||||||
// Update controls
|
|
||||||
if (this.controls) {
|
|
||||||
this.controls.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update plane orientation based on camera position
|
// Update plane orientation based on camera position
|
||||||
if (this.intersectionPlane && this.camera) {
|
this.intersectionPlaneService.updatePlaneOrientation(this.rendererService.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
|
// Render the scene
|
||||||
// Adding Math.PI/4 (45 degrees) to the angle before determining the quadrant
|
this.rendererService.render();
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate JSON output
|
||||||
|
*/
|
||||||
|
public generateJson(): string {
|
||||||
|
return this.particleManagerService.generateJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderer.render(this.scene, this.camera);
|
public copyJson() {
|
||||||
}
|
navigator.clipboard.writeText(this.generateJson()).then(() => {
|
||||||
|
this.matSnackBar.open('Copied to clipboard', '', {duration: 2000})
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
64
frontend/src/app/particles/services/frame-manager.service.ts
Normal file
64
frontend/src/app/particles/services/frame-manager.service.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ParticleManagerService } from './particle-manager.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for managing animation frames
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class FrameManagerService {
|
||||||
|
|
||||||
|
constructor(private particleManager: ParticleManagerService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new frame
|
||||||
|
*/
|
||||||
|
addFrame(): void {
|
||||||
|
const frames = this.particleManager.getFrames();
|
||||||
|
const frameId = `frame${frames.length + 1}`;
|
||||||
|
|
||||||
|
frames.push(frameId);
|
||||||
|
this.particleManager.setFrames(frames);
|
||||||
|
|
||||||
|
const particleData = this.particleManager.getParticleData();
|
||||||
|
particleData.frames[frameId] = [];
|
||||||
|
this.particleManager.setParticleData(particleData);
|
||||||
|
|
||||||
|
this.switchFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to a different frame
|
||||||
|
*/
|
||||||
|
switchFrame(frameId: string): void {
|
||||||
|
this.particleManager.setCurrentFrame(frameId);
|
||||||
|
this.particleManager.clearParticleVisuals();
|
||||||
|
this.particleManager.renderFrameParticles(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a frame
|
||||||
|
*/
|
||||||
|
removeFrame(frameId: string): void {
|
||||||
|
const frames = this.particleManager.getFrames();
|
||||||
|
const index = frames.indexOf(frameId);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
frames.splice(index, 1);
|
||||||
|
this.particleManager.setFrames(frames);
|
||||||
|
|
||||||
|
const particleData = this.particleManager.getParticleData();
|
||||||
|
delete particleData.frames[frameId];
|
||||||
|
this.particleManager.setParticleData(particleData);
|
||||||
|
|
||||||
|
// Switch to first frame if we removed the current one
|
||||||
|
if (frameId === this.particleManager.getCurrentFrame() && frames.length > 0) {
|
||||||
|
this.switchFrame(frames[0]);
|
||||||
|
} else if (frames.length === 0) {
|
||||||
|
// If no frames left, add one
|
||||||
|
this.addFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
frontend/src/app/particles/services/input-handler.service.ts
Normal file
105
frontend/src/app/particles/services/input-handler.service.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { Injectable, ElementRef } from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { RendererService } from './renderer.service';
|
||||||
|
import { IntersectionPlaneService } from './intersection-plane.service';
|
||||||
|
import { ParticleManagerService } from './particle-manager.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for handling user input interactions
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class InputHandlerService {
|
||||||
|
private raycaster = new THREE.Raycaster();
|
||||||
|
private mouse = new THREE.Vector2();
|
||||||
|
private isDragging = false;
|
||||||
|
private mouseDownTime = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private rendererService: RendererService,
|
||||||
|
private intersectionPlaneService: IntersectionPlaneService,
|
||||||
|
private particleManagerService: ParticleManagerService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes input event listeners
|
||||||
|
*/
|
||||||
|
initializeInputHandlers(rendererElement: HTMLElement): void {
|
||||||
|
rendererElement.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||||
|
rendererElement.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||||
|
rendererElement.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||||
|
window.addEventListener('resize', this.onWindowResize.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles mouse down event
|
||||||
|
*/
|
||||||
|
private onMouseDown(event: MouseEvent): void {
|
||||||
|
this.isDragging = false;
|
||||||
|
this.mouseDownTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles mouse click on the plane
|
||||||
|
*/
|
||||||
|
private handlePlaneClick(event: MouseEvent): void {
|
||||||
|
// Calculate mouse position in normalized device coordinates
|
||||||
|
const rect = this.rendererService.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.rendererService.camera);
|
||||||
|
|
||||||
|
// Calculate objects intersecting the picking ray
|
||||||
|
const intersects = this.raycaster.intersectObject(this.intersectionPlaneService.getIntersectionPlane());
|
||||||
|
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
const point = intersects[0].point;
|
||||||
|
this.particleManagerService.addParticle(point.x, point.y, point.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles window resize event
|
||||||
|
*/
|
||||||
|
private onWindowResize(): void {
|
||||||
|
// This is delegated to the renderer service
|
||||||
|
const container = this.rendererService.renderer.domElement.parentElement;
|
||||||
|
if (container) {
|
||||||
|
this.rendererService.onWindowResize(new ElementRef(container));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes event listeners
|
||||||
|
*/
|
||||||
|
cleanup(rendererElement: HTMLElement): void {
|
||||||
|
rendererElement.removeEventListener('mousedown', this.onMouseDown.bind(this));
|
||||||
|
rendererElement.removeEventListener('mouseup', this.onMouseUp.bind(this));
|
||||||
|
rendererElement.removeEventListener('mousemove', this.onMouseMove.bind(this));
|
||||||
|
window.removeEventListener('resize', this.onWindowResize.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import {RendererService} from './renderer.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the possible orientations of the intersection plane
|
||||||
|
*/
|
||||||
|
enum PlaneOrientation {
|
||||||
|
VERTICAL_ABOVE,
|
||||||
|
VERTICAL_BELOW,
|
||||||
|
HORIZONTAL_FRONT,
|
||||||
|
HORIZONTAL_BEHIND,
|
||||||
|
HORIZONTAL_RIGHT,
|
||||||
|
HORIZONTAL_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for managing the intersection plane
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class IntersectionPlaneService {
|
||||||
|
private intersectionPlane!: THREE.Mesh;
|
||||||
|
private planePosition: number = 0; // Position in 1/16th of a block
|
||||||
|
private currentOrientation: PlaneOrientation = PlaneOrientation.HORIZONTAL_FRONT;
|
||||||
|
|
||||||
|
constructor(private rendererService: RendererService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the intersection plane and adds it to the scene
|
||||||
|
*/
|
||||||
|
createIntersectionPlane(): THREE.Mesh {
|
||||||
|
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.rendererService.scene.add(this.intersectionPlane);
|
||||||
|
|
||||||
|
return this.intersectionPlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the plane orientation based on camera position
|
||||||
|
*/
|
||||||
|
private determinePlaneOrientation(camera: THREE.Camera): PlaneOrientation {
|
||||||
|
// Check if camera is looking from above or below first
|
||||||
|
const verticalAngle = Math.atan2(
|
||||||
|
camera.position.y,
|
||||||
|
Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Threshold angle for considering the camera to be above/below (about 45 degrees)
|
||||||
|
const verticalThreshold = Math.PI / 4;
|
||||||
|
|
||||||
|
if (verticalAngle > verticalThreshold) {
|
||||||
|
return PlaneOrientation.VERTICAL_ABOVE;
|
||||||
|
} else if (verticalAngle < -verticalThreshold) {
|
||||||
|
return PlaneOrientation.VERTICAL_BELOW;
|
||||||
|
} else {
|
||||||
|
// Calculate the angle between camera and player (in the XZ plane)
|
||||||
|
const cameraAngle = Math.atan2(
|
||||||
|
camera.position.x,
|
||||||
|
camera.position.z
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine which quadrant the camera is in with a 45-degree offset
|
||||||
|
const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4;
|
||||||
|
|
||||||
|
// Return the appropriate orientation based on quadrant
|
||||||
|
switch (quadrant) {
|
||||||
|
case 0:
|
||||||
|
return PlaneOrientation.HORIZONTAL_FRONT;
|
||||||
|
case 1:
|
||||||
|
return PlaneOrientation.HORIZONTAL_RIGHT;
|
||||||
|
case 2:
|
||||||
|
return PlaneOrientation.HORIZONTAL_BEHIND;
|
||||||
|
case 3:
|
||||||
|
return PlaneOrientation.HORIZONTAL_LEFT;
|
||||||
|
default:
|
||||||
|
return PlaneOrientation.HORIZONTAL_FRONT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the plane orientation based on camera position
|
||||||
|
*/
|
||||||
|
public updatePlaneOrientation(camera: THREE.Camera): void {
|
||||||
|
if (!this.intersectionPlane) return;
|
||||||
|
|
||||||
|
this.currentOrientation = this.determinePlaneOrientation(camera);
|
||||||
|
|
||||||
|
// Apply rotation and material based on orientation
|
||||||
|
switch (this.currentOrientation) {
|
||||||
|
case PlaneOrientation.VERTICAL_ABOVE:
|
||||||
|
this.intersectionPlane.rotation.x = -Math.PI / 2;
|
||||||
|
this.intersectionPlane.rotation.y = 0;
|
||||||
|
this.updatePlaneMaterial(0xAA0000);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.VERTICAL_BELOW:
|
||||||
|
this.intersectionPlane.rotation.x = Math.PI / 2;
|
||||||
|
this.intersectionPlane.rotation.y = 0;
|
||||||
|
this.updatePlaneMaterial(0xAA0000);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||||
|
this.intersectionPlane.rotation.x = 0;
|
||||||
|
this.intersectionPlane.rotation.y = 0;
|
||||||
|
this.updatePlaneMaterial(0x00AA00);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||||
|
this.intersectionPlane.rotation.x = 0;
|
||||||
|
this.intersectionPlane.rotation.y = Math.PI;
|
||||||
|
this.updatePlaneMaterial(0x00AA00);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||||
|
this.intersectionPlane.rotation.x = 0;
|
||||||
|
this.intersectionPlane.rotation.y = Math.PI / 2;
|
||||||
|
this.updatePlaneMaterial(0x0000AA);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||||
|
this.intersectionPlane.rotation.x = 0;
|
||||||
|
this.intersectionPlane.rotation.y = -Math.PI / 2;
|
||||||
|
this.updatePlaneMaterial(0x0000AA);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Restrict plane position to the new bounds and update it
|
||||||
|
this.planePosition = Math.max(this.getMinOffset(), Math.min(this.getMaxOffset(), this.planePosition));
|
||||||
|
this.updatePlanePosition(this.planePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the plane position based on slider value
|
||||||
|
*/
|
||||||
|
public updatePlanePosition(value: number): void {
|
||||||
|
this.planePosition = value;
|
||||||
|
// Convert from 1/16th block to Three.js units
|
||||||
|
const position = (this.planePosition / 16)
|
||||||
|
|
||||||
|
this.intersectionPlane.position.y = 0.8;
|
||||||
|
this.intersectionPlane.position.x = 0;
|
||||||
|
this.intersectionPlane.position.z = 0;
|
||||||
|
|
||||||
|
// Position based on the current orientation
|
||||||
|
switch (this.currentOrientation) {
|
||||||
|
case PlaneOrientation.VERTICAL_ABOVE:
|
||||||
|
this.intersectionPlane.position.y = 0.8 - position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.VERTICAL_BELOW:
|
||||||
|
this.intersectionPlane.position.y = 0.8 + position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||||
|
this.intersectionPlane.position.z = position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||||
|
this.intersectionPlane.position.z = -position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||||
|
this.intersectionPlane.position.x = position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||||
|
this.intersectionPlane.position.x = -position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the plane material color
|
||||||
|
*/
|
||||||
|
private updatePlaneMaterial(color: number): void {
|
||||||
|
this.intersectionPlane.material = new THREE.MeshBasicMaterial({
|
||||||
|
color: color,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.05,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the intersection plane
|
||||||
|
*/
|
||||||
|
public getIntersectionPlane(): THREE.Mesh {
|
||||||
|
return this.intersectionPlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current plane position
|
||||||
|
*/
|
||||||
|
public getPlanePosition(): number {
|
||||||
|
return this.planePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMaxOffset(): number {
|
||||||
|
switch (this.currentOrientation) {
|
||||||
|
case PlaneOrientation.VERTICAL_ABOVE:
|
||||||
|
case PlaneOrientation.VERTICAL_BELOW:
|
||||||
|
return 16;
|
||||||
|
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||||
|
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||||
|
return 8;
|
||||||
|
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||||
|
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMinOffset(): number {
|
||||||
|
return this.getMaxOffset() * -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
185
frontend/src/app/particles/services/particle-manager.service.ts
Normal file
185
frontend/src/app/particles/services/particle-manager.service.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { RendererService } from './renderer.service';
|
||||||
|
import { 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.REDSTONE,
|
||||||
|
lore: '',
|
||||||
|
display_item: 'REDSTONE',
|
||||||
|
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';
|
||||||
|
|
||||||
|
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('#', '');
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
// 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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
frontend/src/app/particles/services/player-model.service.ts
Normal file
71
frontend/src/app/particles/services/player-model.service.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { RendererService } from './renderer.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for creating and managing the player model
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PlayerModelService {
|
||||||
|
private playerModel!: THREE.Group;
|
||||||
|
|
||||||
|
constructor(private rendererService: RendererService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a simple player model and adds it to the scene
|
||||||
|
*/
|
||||||
|
createPlayerModel(): THREE.Group {
|
||||||
|
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.rendererService.scene.add(this.playerModel);
|
||||||
|
|
||||||
|
return this.playerModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the player model
|
||||||
|
*/
|
||||||
|
getPlayerModel(): THREE.Group {
|
||||||
|
return this.playerModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
frontend/src/app/particles/services/renderer.service.ts
Normal file
91
frontend/src/app/particles/services/renderer.service.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { ElementRef, Injectable } from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for managing the Three.js rendering environment
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RendererService {
|
||||||
|
scene!: THREE.Scene;
|
||||||
|
camera!: THREE.PerspectiveCamera;
|
||||||
|
renderer!: THREE.WebGLRenderer;
|
||||||
|
controls!: OrbitControls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the Three.js scene, camera, renderer, and controls
|
||||||
|
*/
|
||||||
|
initializeRenderer(container: ElementRef): void {
|
||||||
|
// Create scene
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
this.scene.background = new THREE.Color(0xf0f0f0);
|
||||||
|
|
||||||
|
// Get container dimensions
|
||||||
|
const containerWidth = container.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';
|
||||||
|
|
||||||
|
container.nativeElement.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
// Initialize orbit controls
|
||||||
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
|
this.controls.enableDamping = true;
|
||||||
|
this.controls.dampingFactor = 0.05;
|
||||||
|
this.controls.minDistance = 2;
|
||||||
|
this.controls.maxDistance = 10;
|
||||||
|
this.controls.target.set(0, 1, 0);
|
||||||
|
this.controls.update();
|
||||||
|
|
||||||
|
// Add lights
|
||||||
|
this.addLights();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds lighting to the scene
|
||||||
|
*/
|
||||||
|
private addLights(): void {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles window resize events
|
||||||
|
*/
|
||||||
|
onWindowResize(container: ElementRef): void {
|
||||||
|
const containerWidth = container.nativeElement.clientWidth;
|
||||||
|
const containerHeight = 400; // Fixed height as defined in CSS
|
||||||
|
|
||||||
|
this.camera.aspect = containerWidth / containerHeight;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
this.renderer.setSize(containerWidth, containerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the scene
|
||||||
|
*/
|
||||||
|
render(): void {
|
||||||
|
if (this.controls) {
|
||||||
|
this.controls.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
@import '@angular/material/prebuilt-themes/azure-blue.css';
|
@import '@angular/material/prebuilt-themes/azure-blue.css';
|
||||||
|
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--white: #FFFFFF;
|
--white: #FFFFFF;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user