Compare commits
8 Commits
37d7c37f3b
...
63aa7fd550
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63aa7fd550 | ||
|
|
dc1b29e52c | ||
|
|
9b8c4891f4 | ||
|
|
b4fcbed781 | ||
|
|
dc1ed8ffee | ||
|
|
57fb00f685 | ||
|
|
08a42801df | ||
|
|
be78b66c5a |
|
|
@ -1,15 +1,15 @@
|
|||
<div class="card-div">
|
||||
<mat-card class="particle-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Particle Properties</mat-card-title>
|
||||
<mat-card-title>Particle input style</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="particle-properties">
|
||||
<div class="property-row">
|
||||
<div class="color-picker">
|
||||
<input type="color" [(ngModel)]="selectedColor">
|
||||
<span>Current color: {{ selectedColor }}</span>
|
||||
</div>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Current color: {{ selectedColor }}</mat-label>
|
||||
<input type="color" class="color-input" matInput [(ngModel)]="selectedColor">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="type-field">
|
||||
<mat-label>Select Particle Type</mat-label>
|
||||
<input type="text"
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
.particle-properties {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.property-row {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
|
@ -22,11 +22,8 @@
|
|||
max-width: 40ch;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.color-input {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.color-picker input[type="color"] {
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -7,11 +7,22 @@
|
|||
<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">
|
||||
<input required matInput [(ngModel)]="particleData.particle_name" placeholder="Enter particle name">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Display Name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.display_name" placeholder="Enter display name">
|
||||
<input required matInput [(ngModel)]="particleData.display_name" placeholder="Enter display name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Permission name</mat-label>
|
||||
<input required matInput [(ngModel)]="particleData.permission" placeholder="Enter permission">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Package name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.package_permission" placeholder="Enter package permission">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
|
|
@ -26,25 +37,14 @@
|
|||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Display Item</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.display_item" placeholder="Enter display item">
|
||||
<input required matInput [(ngModel)]="particleData.display_item" placeholder="Enter display item">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="lore-double">
|
||||
<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>Permission name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.permission" placeholder="Enter permission">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Package name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.package_permission" placeholder="Enter package permission">
|
||||
<textarea required matInput [(ngModel)]="particleData.lore" placeholder="Enter lore"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {Component, inject} 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";
|
||||
|
|
@ -24,17 +24,14 @@ import {ParticleManagerService} from '../../services/particle-manager.service';
|
|||
MatSelect,
|
||||
ReactiveFormsModule,
|
||||
FormsModule
|
||||
],
|
||||
],
|
||||
templateUrl: './properties.component.html',
|
||||
styleUrl: './properties.component.scss'
|
||||
})
|
||||
export class PropertiesComponent {
|
||||
public particleTypes = Object.values(ParticleType);
|
||||
|
||||
constructor(
|
||||
private particleManagerService: ParticleManagerService,
|
||||
) {
|
||||
}
|
||||
private readonly particleManagerService = inject(ParticleManagerService);
|
||||
|
||||
public get particleData(): ParticleData {
|
||||
return this.particleManagerService.getParticleData();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,18 @@
|
|||
<mat-icon>location_searching</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-mini-fab color="primary" (click)="toggleShowParticlesWhenIntersectingPlane()"
|
||||
[matTooltip]="onlyIntersecting ? 'Show all particles' : 'Show only intersecting particles'">
|
||||
<mat-icon>{{ onlyIntersecting ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-mini-fab color="primary" (click)="toggleShowCharacter()"
|
||||
[matTooltip]="showCharacter ? 'Hide character' : 'Show character'">
|
||||
<mat-icon>{{ showCharacter ? 'person' : 'person_off' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-mini-fab color="primary" (click)="togglePlaneLock()"
|
||||
[matTooltip]="isPlaneLocked ? 'Unlock Plane' : 'Lock Plane'">
|
||||
[matTooltip]="isPlaneLocked ? 'Unlock plane' : 'Lock plane'">
|
||||
<mat-icon>{{ isPlaneLocked ? 'lock' : 'lock_open' }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core';
|
||||
import {AfterViewInit, Component, ElementRef, inject, OnDestroy, ViewChild} from '@angular/core';
|
||||
import {MatMiniFabButton} from '@angular/material/button';
|
||||
|
||||
import {IntersectionPlaneService, PlaneOrientation} from '../../services/intersection-plane.service';
|
||||
|
|
@ -9,6 +9,7 @@ import {PlayerModelService} from '../../services/player-model.service';
|
|||
import {InputHandlerService} from '../../services/input-handler.service';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-render-container',
|
||||
|
|
@ -20,20 +21,18 @@ import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
|||
MatInput,
|
||||
MatFormField,
|
||||
MatLabel
|
||||
],
|
||||
],
|
||||
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,
|
||||
) {
|
||||
}
|
||||
private readonly intersectionPlaneService = inject(IntersectionPlaneService);
|
||||
private readonly playerModelService = inject(PlayerModelService);
|
||||
private readonly inputHandlerService = inject(InputHandlerService);
|
||||
private readonly rendererService = inject(RendererService);
|
||||
private readonly particleManagerService = inject(ParticleManagerService);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.initializeScene();
|
||||
|
|
@ -99,6 +98,14 @@ export class RenderContainerComponent implements AfterViewInit, OnDestroy {
|
|||
this.rendererService.resetCamera();
|
||||
}
|
||||
|
||||
public toggleShowParticlesWhenIntersectingPlane(): void {
|
||||
this.particleManagerService.onlyIntersectingParticles = !this.particleManagerService.onlyIntersectingParticles;
|
||||
}
|
||||
|
||||
public toggleShowCharacter(): void {
|
||||
this.playerModelService.showCharacter = !this.playerModelService.showCharacter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current plane orientation
|
||||
*/
|
||||
|
|
@ -106,6 +113,20 @@ export class RenderContainerComponent implements AfterViewInit, OnDestroy {
|
|||
return this.intersectionPlaneService.getCurrentOrientation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value indicating whether only intersecting particles are being considered.
|
||||
*/
|
||||
public get onlyIntersecting(): boolean {
|
||||
return this.particleManagerService.onlyIntersectingParticles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value indicating whether the character is being rendered.
|
||||
*/
|
||||
public get showCharacter(): boolean {
|
||||
return this.playerModelService.showCharacter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the plane orientation
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import * as THREE from 'three';
|
||||
import {RendererService} from './renderer.service';
|
||||
import {Subject} from 'rxjs';
|
||||
|
||||
/**
|
||||
* Represents the possible orientations of the intersection plane
|
||||
|
|
@ -27,6 +28,10 @@ export class IntersectionPlaneService {
|
|||
private planeLocked: boolean = false;
|
||||
private opacity: number = 0.05;
|
||||
|
||||
// Emits whenever plane position, orientation, or lock-affecting orientation updates change visuals
|
||||
public readonly planeChanged$ = new Subject<void>();
|
||||
private lastPlaneSignature: string | null = null;
|
||||
|
||||
constructor(private rendererService: RendererService) {
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +149,13 @@ export class IntersectionPlaneService {
|
|||
this.intersectionPlane.position.x = -position;
|
||||
break;
|
||||
}
|
||||
|
||||
// Notify listeners only if signature changed to avoid spamming during animation frames
|
||||
const signature = `${this.currentOrientation}|${this.planePosition}`;
|
||||
if (signature !== this.lastPlaneSignature) {
|
||||
this.lastPlaneSignature = signature;
|
||||
this.planeChanged$.next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import * as THREE from 'three';
|
||||
import {RendererService} from './renderer.service';
|
||||
import {Particle, ParticleData, ParticleInfo, ParticleType} from '../models/particle.model';
|
||||
import {IntersectionPlaneService, PlaneOrientation} from './intersection-plane.service';
|
||||
import {deepCopy} from '../../../util/deep-copy.util';
|
||||
|
||||
/**
|
||||
* Service responsible for managing particles in the scene
|
||||
|
|
@ -33,8 +35,18 @@ export class ParticleManagerService {
|
|||
private selectedColor: string = '#ff0000';
|
||||
private selectedParticle: Particle = Particle.DUST;
|
||||
private selectedSize: number = 1;
|
||||
private onlyIntersecting: boolean = false;
|
||||
|
||||
constructor(private rendererService: RendererService) {
|
||||
private readonly rendererService = inject(RendererService);
|
||||
private readonly intersectionPlaneService = inject(IntersectionPlaneService);
|
||||
|
||||
constructor() {
|
||||
this.intersectionPlaneService.planeChanged$.subscribe(() => {
|
||||
if (this.onlyIntersecting) {
|
||||
this.clearParticleVisuals();
|
||||
this.renderFrameParticles(this.currentFrame);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -88,7 +100,34 @@ export class ParticleManagerService {
|
|||
renderFrameParticles(frameId: string): void {
|
||||
if (!this.particleData.frames[frameId]) return;
|
||||
|
||||
const filter = this.onlyIntersecting;
|
||||
const orientation = this.intersectionPlaneService.getCurrentOrientation();
|
||||
const offset16 = this.intersectionPlaneService.getPlanePosition();
|
||||
const planePos = offset16 / 16; // convert from 1/16th units to world units
|
||||
const epsilon = 0.02; // tolerance for intersection
|
||||
|
||||
const isOnPlane = (p: ParticleInfo) => {
|
||||
if (!filter) return true;
|
||||
switch (orientation) {
|
||||
case PlaneOrientation.VERTICAL_ABOVE:
|
||||
case PlaneOrientation.VERTICAL_BELOW:
|
||||
// Horizontal plane at y = 0.8 +/- planePos
|
||||
return Math.abs(p.y - (0.8 + (orientation === PlaneOrientation.VERTICAL_BELOW ? planePos : -planePos))) <= epsilon;
|
||||
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||
return Math.abs(p.z - planePos) <= epsilon;
|
||||
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||
return Math.abs(p.z + planePos) <= epsilon;
|
||||
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||
return Math.abs(p.x - planePos) <= epsilon;
|
||||
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||
return Math.abs(p.x + planePos) <= epsilon;
|
||||
}
|
||||
};
|
||||
|
||||
for (const particleInfo of this.particleData.frames[frameId]) {
|
||||
if (!isOnPlane(particleInfo)) {
|
||||
continue;
|
||||
}
|
||||
const particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);
|
||||
|
||||
const color = this.getColor(particleInfo);
|
||||
|
|
@ -243,6 +282,23 @@ export class ParticleManagerService {
|
|||
* Generates JSON output of the particle data
|
||||
*/
|
||||
generateJson(): string {
|
||||
const particleData = deepCopy(this.particleData)
|
||||
if (this.particleData.package_permission) {
|
||||
particleData.package_permission = 'apart.set.' + this.particleData.package_permission.toLowerCase().replace(' ', '-');
|
||||
} else {
|
||||
particleData.package_permission = 'apart.set.none';
|
||||
}
|
||||
particleData.permission = 'apart.particle.' + this.particleData.permission.toLowerCase().replace(' ', '-');
|
||||
return JSON.stringify(this.particleData, null, 2);
|
||||
}
|
||||
|
||||
public get onlyIntersectingParticles(): boolean {
|
||||
return this.onlyIntersecting;
|
||||
}
|
||||
|
||||
public set onlyIntersectingParticles(value: boolean) {
|
||||
this.onlyIntersecting = value;
|
||||
this.clearParticleVisuals();
|
||||
this.renderFrameParticles(this.currentFrame);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import * as THREE from 'three';
|
||||
import {RendererService} from './renderer.service';
|
||||
|
||||
|
|
@ -6,13 +6,13 @@ import {RendererService} from './renderer.service';
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class PlayerModelService {
|
||||
private readonly rendererService = inject(RendererService);
|
||||
|
||||
private playerModel!: THREE.Group;
|
||||
private skinTexture!: THREE.Texture;
|
||||
private characterVisible: boolean = true;
|
||||
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
|
||||
|
|
@ -60,6 +60,15 @@ export class PlayerModelService {
|
|||
return this.playerModel;
|
||||
}
|
||||
|
||||
public get showCharacter(): boolean {
|
||||
return this.characterVisible;
|
||||
}
|
||||
|
||||
public set showCharacter(showCharacter: boolean) {
|
||||
this.playerModel.visible = showCharacter;
|
||||
this.characterVisible = showCharacter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple colored player model (without textures)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class RendererService {
|
|||
this.themeService.theme$.subscribe(theme => {
|
||||
this.currentTheme = theme;
|
||||
if (this.scene) {
|
||||
this.setBackgroundColor(theme);
|
||||
this.setBackgroundColor();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ export class RendererService {
|
|||
initializeRenderer(container: ElementRef): void {
|
||||
// Create scene
|
||||
this.scene = new THREE.Scene();
|
||||
this.setBackgroundColor(this.currentTheme);
|
||||
this.setBackgroundColor();
|
||||
|
||||
// Get container dimensions
|
||||
const containerWidth = container.nativeElement.clientWidth;
|
||||
|
|
@ -66,7 +66,7 @@ export class RendererService {
|
|||
this.addLights();
|
||||
}
|
||||
|
||||
private setBackgroundColor(theme: THEME_MODE) {
|
||||
private setBackgroundColor() {
|
||||
this.scene.background = new THREE.Color(this.currentTheme === THEME_MODE.DARK ? 0x242526 : 0xFBFBFE);
|
||||
}
|
||||
|
||||
|
|
|
|||
10
frontend/src/app/util/deep-copy.util.ts
Normal file
10
frontend/src/app/util/deep-copy.util.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export function deepCopy<T>(obj: T): T {
|
||||
// Structured cloning
|
||||
if (typeof globalThis?.structuredClone === 'function') {
|
||||
// @ts-ignore
|
||||
return globalThis.structuredClone(obj) as T;
|
||||
}
|
||||
|
||||
// Normal objects
|
||||
return JSON.parse(JSON.stringify(obj)) as T;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user