Compare commits

...

8 Commits

13 changed files with 176 additions and 110 deletions

View File

@ -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"

View File

@ -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"] {

View File

@ -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();
});
});

View File

@ -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>

View File

@ -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";
@ -31,10 +31,7 @@ import {ParticleManagerService} from '../../services/particle-manager.service';
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();

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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',
@ -27,13 +28,11 @@ import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
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
*/

View File

@ -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();
}
}
/**

View File

@ -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);
}
}

View File

@ -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)
*/

View File

@ -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);
}

View 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;
}