20 Commits
Author SHA1 Message Date
auto fdb57289f8 Fix weird interactions between plane and player model 2025-06-22 20:20:40 +02:00
auto 60c1329163 Switch to steve 2025-06-22 20:17:02 +02:00
auto 0e71c0f581 Fix textures overlapping and glitching for legs 2025-06-22 20:15:35 +02:00
auto eb67a33331 Change default camera position 2025-06-22 20:12:25 +02:00
auto 39f20796ce Simplify UV mapping logic and remove unnecessary face rotation handling 2025-06-22 20:08:42 +02:00
auto e00165c56f Fix uv rotation 2025-06-22 20:04:26 +02:00
auto 02c6497700 Fix mapping 2025-06-22 20:01:11 +02:00
auto 0efd476676 Initial attempt at textures 2025-06-22 19:53:27 +02:00
auto 237518638c Increase particles-list height in FramesComponent to 550px for improved visibility. 2025-06-22 19:34:20 +02:00
auto fea1a98cea Adjust RendererService to use dynamic container height and update OrbitControls limits 2025-06-22 19:27:55 +02:00
auto ecd9b3d824 Modularize renderer and plane control functionality into RenderContainerComponent 2025-06-22 19:23:54 +02:00
auto 9808b5d63d Add manual plane orientation controls with lock/unlock functionality
Implemented a UI overlay in `ParticlesComponent` for manual plane orientation selection with buttons for different orientations. Added lock/unlock toggle to control automatic orientation adjustment. Refactored `IntersectionPlaneService` to support locked state and manual orientation updates. Updated styles and layout to integrate the new controls seamlessly.
2025-06-22 19:16:32 +02:00
auto c13b7077a7 Remove unused OnInit lifecycle hook from ParticlesComponent and cleanup redundant comments. 2025-06-22 19:08:28 +02:00
auto 023ae809ef Modularize ParticlesComponent by separating particle, frame, and property functionalities into dedicated components
Refactored `ParticlesComponent` to use `ParticleComponent`, `FramesComponent`, and `PropertiesComponent` for better organization and reusability. Updated layout, improved UI structure, and centralized particle management logic. Enhanced clipboard functionality with a new JSON copy feature.
2025-06-22 19:06:52 +02:00
auto 3a6f137c9a Update ParticlesComponent to allow dynamic plane position limits and improve slider configuration
Modified plane position logic to use dynamic min/max offsets based on orientation. Enhanced slider component with binding to these offsets and updated display text for clarity. Refactored `IntersectionPlaneService` to expose new methods (`getMaxOffset`, `getMinOffset`) and adjusted position defaults.
2025-06-22 17:59:29 +02:00
auto cb8447a096 Correct default plane locations 2025-06-22 17:38:28 +02:00
auto 3e98e1a498 add plane orientation for looking from above/below 2025-06-22 17:32:32 +02:00
auto 4c31a91bb4 Add modular services for Three.js integration in ParticlesComponent
Implemented dedicated Angular services (e.g., RendererService, InputHandlerService, PlayerModelService) to modularize and simplify Three.js integration within the `ParticlesComponent`. Refactored component logic to delegate rendering, input handling, and model creation to respective services. Updated particle data models and removed redundant logic from the component.
2025-06-22 17:26:10 +02:00
auto d6faaba01c Update security configuration to allow unrestricted access
Changed default security rule to permit all requests instead of requiring authentication. Adjusted `SecurityConfig` to ensure open access for all endpoints.
2025-06-22 01:15:45 +02:00
auto 56175e62d6 Remove RemoveTrailingPeriodPipe import from AboutComponent 2025-06-22 01:10:53 +02:00
30 changed files with 1737 additions and 715 deletions
@@ -40,7 +40,7 @@ public class SecurityConfig {
.requestMatchers("/team/**", "/history/**").permitAll()
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.anyRequest().authenticated()
.anyRequest().permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+1 -3
View File
@@ -2,15 +2,13 @@ import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service';
import {CommonModule} from '@angular/common';
import {HeaderComponent} from '../header/header.component';
import {RemoveTrailingPeriodPipe} from "../util/RemoveTrailingPeriodPipe";
@Component({
selector: 'app-about',
standalone: true,
imports: [
CommonModule,
HeaderComponent,
RemoveTrailingPeriodPipe
HeaderComponent
],
templateUrl: './about.component.html',
styleUrl: './about.component.scss'
@@ -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 {
height: 550px;
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();
}
}
@@ -0,0 +1,41 @@
<div #rendererContainer class="renderer-container">
<div class="plane-controls-overlay">
<button mat-mini-fab color="primary" (click)="togglePlaneLock()"
[matTooltip]="isPlaneLocked ? 'Unlock Plane' : 'Lock Plane'">
<mat-icon>{{ isPlaneLocked ? 'lock' : 'lock_open' }}</mat-icon>
</button>
<div *ngIf="isPlaneLocked" class="plane-orientation-buttons">
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_ABOVE)"
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_ABOVE"
matTooltip="Vertical Above">
<mat-icon>arrow_upward</mat-icon>
</button>
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_BELOW)"
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_BELOW"
matTooltip="Vertical Below">
<mat-icon>arrow_downward</mat-icon>
</button>
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_FRONT)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_FRONT"
matTooltip="Horizontal Front">
<mat-icon>arrow_forward</mat-icon>
</button>
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_BEHIND)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_BEHIND"
matTooltip="Horizontal Behind">
<mat-icon>arrow_back</mat-icon>
</button>
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_RIGHT)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_RIGHT"
matTooltip="Horizontal Right">
<mat-icon>arrow_right</mat-icon>
</button>
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_LEFT)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_LEFT"
matTooltip="Horizontal Left">
<mat-icon>arrow_left</mat-icon>
</button>
</div>
</div>
</div>
@@ -0,0 +1,46 @@
.renderer-container {
height: 1000px;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
position: relative; /* Added for absolute positioning of overlay */
}
.plane-controls-overlay {
position: absolute;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
z-index: 10;
}
.plane-orientation-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 10px;
margin-top: 10px;
}
.plane-orientation-buttons button {
opacity: 0.7;
transition: opacity 0.2s, transform 0.2s;
}
.plane-orientation-buttons button:hover {
opacity: 1;
transform: scale(1.1);
}
.plane-orientation-buttons button.active {
opacity: 1;
transform: scale(1.1);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RenderContainerComponent } from './render-container.component';
describe('RenderContainerComponent', () => {
let component: RenderContainerComponent;
let fixture: ComponentFixture<RenderContainerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RenderContainerComponent]
})
.compileComponents();
fixture = TestBed.createComponent(RenderContainerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,105 @@
import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core';
import {MatMiniFabButton} from '@angular/material/button';
import {NgIf} from '@angular/common';
import {IntersectionPlaneService, PlaneOrientation} from '../../services/intersection-plane.service';
import {MatIcon} from '@angular/material/icon';
import {MatTooltip} from '@angular/material/tooltip';
import {RendererService} from '../../services/renderer.service';
import {PlayerModelService} from '../../services/player-model.service';
import {InputHandlerService} from '../../services/input-handler.service';
@Component({
selector: 'app-render-container',
imports: [
MatIcon,
MatMiniFabButton,
MatTooltip,
NgIf
],
templateUrl: './render-container.component.html',
styleUrl: './render-container.component.scss'
})
export class RenderContainerComponent implements AfterViewInit, OnDestroy {
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
constructor(
private intersectionPlaneService: IntersectionPlaneService,
private playerModelService: PlayerModelService,
private inputHandlerService: InputHandlerService,
private rendererService: RendererService,
) {
}
ngAfterViewInit(): void {
this.initializeScene();
this.animate();
}
/**
* Clean up resources when component is destroyed
*/
ngOnDestroy(): void {
if (this.rendererService.renderer) {
this.inputHandlerService.cleanup(this.rendererService.renderer.domElement);
}
}
/**
* Initialize the 3D scene and all related components
*/
private initializeScene(): void {
this.rendererService.initializeRenderer(this.rendererContainer);
this.playerModelService.loadSkinTexture('/public/img/skins/steve.png')
.then(() => {
// Then create the player model with the texture applied
this.playerModelService.createPlayerModel();
});
this.intersectionPlaneService.createIntersectionPlane();
this.inputHandlerService.initializeInputHandlers(this.rendererService.renderer.domElement);
}
/**
* Animation loop
*/
private animate(): void {
requestAnimationFrame(this.animate.bind(this));
this.intersectionPlaneService.updatePlaneOrientation(this.rendererService.camera);
this.rendererService.render();
}
/**
* Get whether the plane is locked
*/
public get isPlaneLocked(): boolean {
return this.intersectionPlaneService.isPlaneLocked();
}
/**
* Toggle the plane locked state
*/
public togglePlaneLock(): void {
const newLockedState = !this.isPlaneLocked;
this.intersectionPlaneService.setPlaneLocked(newLockedState);
}
/**
* Get the current plane orientation
*/
public get currentPlaneOrientation(): PlaneOrientation {
return this.intersectionPlaneService.getCurrentOrientation();
}
/**
* Set the plane orientation
*/
public setPlaneOrientation(orientation: PlaneOrientation): void {
this.intersectionPlaneService.setPlaneOrientation(orientation);
}
/**
* Get all available plane orientations
*/
public get planeOrientations(): typeof PlaneOrientation {
return PlaneOrientation;
}
}
@@ -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,31 @@
<main>
<section class="darkmodeSection">
<section class="column">
<div class="renderer-section row column">
<div #rendererContainer class="renderer-container column row"></div>
<div class="renderer-section column">
<div class="flex row">
<div class="flex side-column">
<app-particle-properties></app-particle-properties>
</div>
<div class="flex middle-column">
<app-render-container></app-render-container>
<div class="plane-controls">
<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)">
</mat-slider>
<span>{{ planePosition }}/16 blocks</span>
<span>{{ planePosition }} offset from center</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
<div class="flex side-column">
<app-particle></app-particle>
<app-frames></app-frames>
<div>
<button mat-fab extended (click)="copyJson()">
<mat-icon>content_copy</mat-icon>
Copy JSON to clipboard
</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>
@@ -4,16 +4,16 @@
display: flex;
}
.renderer-container {
width: 100%;
height: 400px;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
.side-column {
flex: 1;
flex-direction: column;
gap: 20px;
}
.middle-column {
flex: 2;
flex-direction: column;
gap: 20px;
}
.plane-controls {
@@ -30,12 +30,6 @@
flex: 1;
}
.controls-section {
flex: 1;
min-width: 300px;
gap: 20px;
}
.form-row {
margin-bottom: 15px;
}
@@ -43,86 +37,3 @@
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;
}
+45 -438
View File
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {Component, ElementRef, ViewChild} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
@@ -11,43 +11,17 @@ import {MatTabsModule} from '@angular/material/tabs';
import {MatCardModule} from '@angular/material/card';
import {MatIconModule} from '@angular/material/icon';
import {HeaderComponent} from '../header/header.component';
import * as THREE from 'three';
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
// Define particle types
export enum ParticleType {
REDSTONE = 'REDSTONE',
// Other particle types can be added later
}
// Services
import {IntersectionPlaneService} from './services/intersection-plane.service';
import {ParticleManagerService} from './services/particle-manager.service';
// 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[];
};
}
// Models
import {PropertiesComponent} from './components/properties/properties.component';
import {ParticleComponent} from './components/particle/particle.component';
import {FramesComponent} from './components/frames/frames.component';
import {MatSnackBar} from '@angular/material/snack-bar';
import {RenderContainerComponent} from './components/render-container/render-container.component';
@Component({
selector: 'app-particles',
@@ -65,430 +39,63 @@ interface ParticleData {
MatTabsModule,
MatCardModule,
MatIconModule,
HeaderComponent
HeaderComponent,
PropertiesComponent,
ParticleComponent,
FramesComponent,
RenderContainerComponent,
],
templateUrl: './particles.component.html',
styleUrl: './particles.component.scss'
})
export class ParticlesComponent implements OnInit, AfterViewInit {
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
export class ParticlesComponent {
@ViewChild('planeSlider') planeSlider!: ElementRef;
// Three.js objects
private scene!: THREE.Scene;
private camera!: THREE.PerspectiveCamera;
private renderer!: THREE.WebGLRenderer;
private controls!: OrbitControls;
private playerModel!: THREE.Group;
private intersectionPlane!: THREE.Mesh;
private particles: THREE.Mesh[] = [];
private raycaster = new THREE.Raycaster();
private mouse = new THREE.Vector2();
// Particle data
public particleData: ParticleData = {
particle_name: '',
display_name: '',
particle_type: ParticleType.REDSTONE,
lore: '',
display_item: 'REDSTONE',
permission: '',
package_permission: '',
frame_delay: 1,
repeat: 1,
repeat_delay: 0,
random_offset: 0,
stationary: true,
frames: {
'frame1': []
}
};
// UI state
public currentFrame: string = 'frame1';
public planePosition: number = 8; // Position in 1/16th of a block
public selectedColor: string = '#ff0000';
public particleTypes = Object.values(ParticleType);
public frames: string[] = ['frame1'];
constructor() {
constructor(
private intersectionPlaneService: IntersectionPlaneService,
private particleManagerService: ParticleManagerService,
private matSnackBar: MatSnackBar,
) {
}
ngOnInit(): void {
// Initialize component
}
ngAfterViewInit(): void {
this.initThreeJS();
this.animate();
}
// Initialize Three.js scene
private initThreeJS(): void {
// Create scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xf0f0f0);
// Get container dimensions
const containerWidth = this.rendererContainer.nativeElement.clientWidth;
const containerHeight = 400; // Fixed height as defined in CSS
// Create camera
this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000);
this.camera.position.set(0, 1, 3);
this.camera.lookAt(0, 1, 0);
// Create renderer
this.renderer = new THREE.WebGLRenderer({antialias: true});
this.renderer.setSize(containerWidth, containerHeight);
// Center the canvas in the container
this.renderer.domElement.style.display = 'block';
this.renderer.domElement.style.margin = 'auto';
this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
// Initialize orbit controls
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true; // Add smooth damping effect
this.controls.dampingFactor = 0.05;
this.controls.minDistance = 2; // Minimum zoom distance
this.controls.maxDistance = 10; // Maximum zoom distance
this.controls.target.set(0, 1, 0); // Set target to player's center
this.controls.update();
// Add lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
this.scene.add(directionalLight);
// Create player model (simple representation)
this.createPlayerModel();
// Create intersection plane
this.createIntersectionPlane();
// Add event listeners
this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
this.renderer.domElement.addEventListener('mouseup', this.onMouseUp.bind(this));
this.renderer.domElement.addEventListener('mousemove', this.onMouseMove.bind(this));
window.addEventListener('resize', this.onWindowResize.bind(this));
}
// Create a simple player model
private createPlayerModel(): void {
this.playerModel = new THREE.Group();
// Head
const headGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const headMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.y = 1.35;
this.playerModel.add(head);
// Body
const bodyGeometry = new THREE.BoxGeometry(0.5, 0.7, 0.25);
const bodyMaterial = new THREE.MeshLambertMaterial({color: 0x0000ff});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.75;
this.playerModel.add(body);
// Arms
const armGeometry = new THREE.BoxGeometry(0.2, 0.7, 0.25);
const armMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
const leftArm = new THREE.Mesh(armGeometry, armMaterial);
leftArm.position.set(-0.35, 0.75, 0);
this.playerModel.add(leftArm);
const rightArm = new THREE.Mesh(armGeometry, armMaterial);
rightArm.position.set(0.35, 0.75, 0);
this.playerModel.add(rightArm);
// Legs
const legGeometry = new THREE.BoxGeometry(0.25, 0.7, 0.25);
const legMaterial = new THREE.MeshLambertMaterial({color: 0x000000});
const leftLeg = new THREE.Mesh(legGeometry, legMaterial);
leftLeg.position.set(-0.125, 0.15, 0);
this.playerModel.add(leftLeg);
const rightLeg = new THREE.Mesh(legGeometry, legMaterial);
rightLeg.position.set(0.125, 0.15, 0);
this.playerModel.add(rightLeg);
this.scene.add(this.playerModel);
}
// Create the intersection plane
private createIntersectionPlane(): void {
const planeGeometry = new THREE.PlaneGeometry(3, 3);
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0x00AA00,
transparent: true,
opacity: 0.05,
side: THREE.DoubleSide
});
this.intersectionPlane = new THREE.Mesh(planeGeometry, planeMaterial);
this.intersectionPlane.position.z = 0;
// Center the plane vertically with the player (player is about 2 blocks tall)
this.intersectionPlane.position.y = 1;
this.scene.add(this.intersectionPlane);
}
// Update plane position based on slider
/**
* 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 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;
}
const value = Number(slider.value);
this.intersectionPlaneService.updatePlanePosition(value);
}
// Track if mouse is being dragged
private isDragging = false;
private mouseDownTime = 0;
// Handle mouse down event
private onMouseDown(event: MouseEvent): void {
this.isDragging = false;
this.mouseDownTime = Date.now();
/**
* Get the current plane position
*/
public get planePosition(): number {
return this.intersectionPlaneService.getPlanePosition();
}
// Handle mouse up event
private onMouseUp(event: MouseEvent): void {
// If mouse was down for less than 200ms and didn't move much, consider it a click, not a drag
if (Date.now() - this.mouseDownTime < 200 && !this.isDragging) {
this.handlePlaneClick(event);
}
this.isDragging = false;
public set planePosition(newPlanePosition: number) {
this.intersectionPlaneService.updatePlanePosition(newPlanePosition);
}
// Handle mouse move event
private onMouseMove(event: MouseEvent): void {
// If mouse moves while button is pressed, it's a drag
if (event.buttons > 0) {
this.isDragging = true;
}
public get maxOffset(): number {
return this.intersectionPlaneService.getMaxOffset();
}
// Handle mouse click on the plane
private handlePlaneClick(event: MouseEvent): void {
// Calculate mouse position in normalized device coordinates
const rect = this.renderer.domElement.getBoundingClientRect();
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// Update the picking ray with the camera and mouse position
this.raycaster.setFromCamera(this.mouse, this.camera);
// Calculate objects intersecting the picking ray
const intersects = this.raycaster.intersectObject(this.intersectionPlane);
if (intersects.length > 0) {
const point = intersects[0].point;
this.addParticle(point.x, point.y, point.z);
}
public get minOffset(): number {
return this.intersectionPlaneService.getMinOffset();
}
// 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] = [];
/**
* Generate JSON output
*/
public generateJson(): string {
return this.particleManagerService.generateJson();
}
this.particleData.frames[this.currentFrame].push(particleInfo);
}
// Handle window resize
private onWindowResize(): void {
const containerWidth = this.rendererContainer.nativeElement.clientWidth;
const containerHeight = 400; // Fixed height as defined in CSS
this.camera.aspect = containerWidth / containerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(containerWidth, containerHeight);
}
// Animation loop
private animate(): void {
requestAnimationFrame(this.animate.bind(this));
// Update controls
if (this.controls) {
this.controls.update();
}
// Update plane orientation based on camera position
if (this.intersectionPlane && this.camera) {
// Calculate the angle between camera and player (in the XZ plane)
const cameraAngle = Math.atan2(
this.camera.position.x,
this.camera.position.z
);
// Determine which quadrant the camera is in with a 45-degree offset
// Adding Math.PI/4 (45 degrees) to the angle before determining the quadrant
// This shifts the quadrant boundaries by 45 degrees
const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4;
// Rotate the plane to face the camera
if (quadrant === 0) {
this.intersectionPlane.rotation.y = 0; // Camera in front
this.updateFrameMaterial(0x00AA00);
} else if (quadrant === 1) {
this.intersectionPlane.rotation.y = Math.PI / 2; // Camera on right
this.updateFrameMaterial(0x0000AA);
} else if (quadrant === 2) {
this.intersectionPlane.rotation.y = Math.PI; // Camera behind
this.updateFrameMaterial(0x00AA00);
} else {
this.intersectionPlane.rotation.y = -Math.PI / 2; // Camera on left
this.updateFrameMaterial(0x0000AA);
}
}
this.renderer.render(this.scene, this.camera);
}
public updateFrameMaterial(color: number) {
this.intersectionPlane.material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.05,
side: THREE.DoubleSide
public copyJson() {
navigator.clipboard.writeText(this.generateJson()).then(() => {
this.matSnackBar.open('Copied to clipboard', '', {duration: 2000})
});
}
// 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();
}
}
}
}
@@ -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();
}
}
}
}
@@ -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,256 @@
import {Injectable} from '@angular/core';
import * as THREE from 'three';
import {RendererService} from './renderer.service';
/**
* Represents the possible orientations of the intersection plane
*/
export 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;
private planeLocked: boolean = false;
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);
this.intersectionPlane.renderOrder = 1;
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;
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;
if (!this.planeLocked) {
this.currentOrientation = this.determinePlaneOrientation(camera);
}
this.updateIntersectionPlaneOrientation()
//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;
}
/**
* Gets the current plane orientation
*/
public getCurrentOrientation(): PlaneOrientation {
return this.currentOrientation;
}
/**
* Sets the plane orientation manually
*/
public setPlaneOrientation(orientation: PlaneOrientation): void {
this.currentOrientation = orientation;
this.updateIntersectionPlaneOrientation();
this.updatePlanePosition(this.planePosition);
}
private updateIntersectionPlaneOrientation() {
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;
}
}
/**
* Gets whether the plane orientation is locked
*/
public isPlaneLocked(): boolean {
return this.planeLocked;
}
/**
* Sets whether the plane orientation is locked
*/
public setPlaneLocked(locked: boolean): void {
this.planeLocked = locked;
}
}
@@ -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);
}
}
@@ -0,0 +1,254 @@
import {Injectable} from '@angular/core';
import * as THREE from 'three';
import {RendererService} from './renderer.service';
@Injectable({
providedIn: 'root'
})
export class PlayerModelService {
private playerModel!: THREE.Group;
private skinTexture!: THREE.Texture;
private textureLoaded = false;
constructor(private rendererService: RendererService) {
}
/**
* Loads a Minecraft skin texture from a URL
* @param textureUrl The URL of the skin texture to load
* @returns A promise that resolves when the texture is loaded
*/
loadSkinTexture(textureUrl: string): Promise<void> {
return new Promise((resolve) => {
const loader = new THREE.TextureLoader();
loader.load(textureUrl, (texture) => {
// Set texture parameters
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
this.skinTexture = texture;
this.textureLoaded = true;
// If the player model already exists, rebuild it with textures
if (this.playerModel) {
// Remove old model
this.rendererService.scene.remove(this.playerModel);
// Create new model with textures
this.createPlayerModel();
}
resolve();
});
});
}
/**
* Creates a player model with Minecraft-style textures and adds it to the scene
*/
createPlayerModel(): THREE.Group {
this.playerModel = new THREE.Group();
if (this.textureLoaded) {
// Create textured model if texture is loaded
this.createTexturedPlayerModel();
} else {
// Create simple colored model if no texture is loaded
this.createSimplePlayerModel();
}
this.playerModel.renderOrder = 0;
this.rendererService.scene.add(this.playerModel);
return this.playerModel;
}
/**
* Creates a simple colored player model (without textures)
*/
private createSimplePlayerModel(): void {
// 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);
}
/**
* Creates a textured player model using the Minecraft skin
*/
private createTexturedPlayerModel(): void {
// Create the player with properly mapped textures
// Head - 8x8x8 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.5, 0.5, 0.5, // width, height, depth
[
{x: 8, y: 0, w: 8, h: 8}, // top
{x: 16, y: 0, w: 8, h: 8}, // bottom
{x: 16, y: 8, w: 8, h: 8}, // right
{x: 0, y: 8, w: 8, h: 8}, // left
{x: 8, y: 8, w: 8, h: 8}, // front
{x: 24, y: 8, w: 8, h: 8} // back
],
{x: 0, y: 1.35, z: 0} // position
));
// Body - 8x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.5, 0.7, 0.25, // width, height, depth
[
{x: 20, y: 16, w: 8, h: 4}, // top
{x: 28, y: 16, w: 8, h: 4}, // bottom
{x: 28, y: 20, w: 4, h: 12}, // right
{x: 16, y: 20, w: 4, h: 12}, // left
{x: 20, y: 20, w: 8, h: 12}, // front
{x: 32, y: 20, w: 8, h: 12} // back
],
{x: 0, y: 0.75, z: 0} // position
));
// Left Arm - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.2, 0.7, 0.25, // width, height, depth
[
{x: 44, y: 16, w: 4, h: 4}, // top
{x: 48, y: 16, w: 4, h: 4}, // bottom
{x: 48, y: 20, w: 4, h: 12}, // right
{x: 40, y: 20, w: 4, h: 12}, // left
{x: 44, y: 20, w: 4, h: 12}, // front
{x: 52, y: 20, w: 4, h: 12} // back
],
{x: -0.35, y: 0.75, z: 0} // position
));
// Right Arm - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.2, 0.7, 0.25, // width, height, depth
[
{x: 44, y: 16, w: 4, h: 4}, // top - mirror of left arm
{x: 48, y: 16, w: 4, h: 4}, // bottom - mirror of left arm
{x: 40, y: 20, w: 4, h: 12}, // right - mirror of left arm's left
{x: 48, y: 20, w: 4, h: 12}, // left - mirror of left arm's right
{x: 44, y: 20, w: 4, h: 12}, // front - same as left arm
{x: 52, y: 20, w: 4, h: 12} // back - same as left arm
],
{x: 0.35, y: 0.75, z: 0} // position
));
// Left Leg - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.26, 0.7, 0.26, // width, height, depth
[
{x: 4, y: 16, w: 4, h: 4}, // top
{x: 8, y: 16, w: 4, h: 4}, // bottom
{x: 8, y: 20, w: 4, h: 12}, // right
{x: 0, y: 20, w: 4, h: 12}, // left
{x: 4, y: 20, w: 4, h: 12}, // front
{x: 12, y: 20, w: 4, h: 12} // back
],
{x: -0.125, y: 0.15, z: 0} // position
));
// Right Leg - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.26, 0.7, 0.26, // width, height, depth
[
{x: 4, y: 16, w: 4, h: 4}, // top - mirror of left leg
{x: 8, y: 16, w: 4, h: 4}, // bottom - mirror of left leg
{x: 0, y: 20, w: 4, h: 12}, // right - mirror of left leg's left
{x: 8, y: 20, w: 4, h: 12}, // left - mirror of left leg's right
{x: 4, y: 20, w: 4, h: 12}, // front - same as left leg
{x: 12, y: 20, w: 4, h: 12} // back - same as left leg
],
{x: 0.125, y: 0.15, z: 0} // position
));
}
/**
* Creates a box with proper UV mapping for a Minecraft character part
* @param width Width of the box
* @param height Height of the box
* @param depth Depth of the box
* @param uvMapping Array of UV coordinates for each face (top, bottom, right, left, front, back)
* @param position Position of the box
* @returns THREE.Mesh with properly mapped textures
*/
private createBoxWithUvMapping(
width: number,
height: number,
depth: number,
uvMapping: Array<{ x: number, y: number, w: number, h: number }>,
position: { x: number, y: number, z: number }
): THREE.Mesh {
const geometry = new THREE.BoxGeometry(width, height, depth);
// Remap the custom face order to BoxGeometry face order: px, nx, py, ny, pz, nz
const faceOrder = [2, 3, 0, 1, 4, 5]; // right, left, top, bottom, front, back
const textureWidth = 64;
const textureHeight = 64;
const uv = geometry.attributes['uv'];
for (let i = 0; i < 6; i++) {
const face = uvMapping[faceOrder[i]];
const x1 = face.x / textureWidth;
const y1 = 1 - face.y / textureHeight;
const x2 = (face.x + face.w) / textureWidth;
const y2 = 1 - (face.y + face.h) / textureHeight;
let uvs: [number, number][] = [
[x1, y1], // top-left
[x2, y1], // top-right
[x1, y2], // bottom-left
[x2, y2] // bottom-right
];
const uvOffset = i * 8;
for (let j = 0; j < 4; j++) {
uv.array[uvOffset + j * 2] = uvs[j][0];
uv.array[uvOffset + j * 2 + 1] = uvs[j][1];
}
}
uv.needsUpdate = true;
const material = new THREE.MeshBasicMaterial({
map: this.skinTexture,
transparent: true,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(position.x, position.y, position.z);
return mesh;
}
}
@@ -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 = container.nativeElement.clientHeight;
// Create camera
this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000);
this.camera.position.set(-1, 2, 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 = 0.5;
this.controls.maxDistance = 4;
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
View File
@@ -1,4 +1,5 @@
@import '@angular/material/prebuilt-themes/azure-blue.css';
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
:root {
--white: #FFFFFF;