Add toggle for showing only intersecting particles

This commit is contained in:
akastijn 2025-12-27 21:55:00 +01:00
parent b4fcbed781
commit 9b8c4891f4
4 changed files with 100 additions and 25 deletions

View File

@ -8,12 +8,17 @@
</div> </div>
<div class="button-row"> <div class="button-row">
<button mat-mini-fab color="primary" (click)="resetCamera()" <button mat-mini-fab color="primary" (click)="resetCamera()"
matTooltip="Reset camera"> matTooltip="Reset camera">
<mat-icon>location_searching</mat-icon> <mat-icon>location_searching</mat-icon>
</button> </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)="togglePlaneLock()" <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> <mat-icon>{{ isPlaneLocked ? 'lock' : 'lock_open' }}</mat-icon>
</button> </button>
</div> </div>
@ -21,33 +26,33 @@
@if (isPlaneLocked) { @if (isPlaneLocked) {
<div class="plane-orientation-buttons"> <div class="plane-orientation-buttons">
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_ABOVE)" <button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_ABOVE)"
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_ABOVE" [class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_ABOVE"
matTooltip="Vertical Above"> matTooltip="Vertical Above">
<mat-icon>arrow_upward</mat-icon> <mat-icon>arrow_upward</mat-icon>
</button> </button>
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_BELOW)" <button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_BELOW)"
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_BELOW" [class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_BELOW"
matTooltip="Vertical Below"> matTooltip="Vertical Below">
<mat-icon>arrow_downward</mat-icon> <mat-icon>arrow_downward</mat-icon>
</button> </button>
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_FRONT)" <button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_FRONT)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_FRONT" [class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_FRONT"
matTooltip="Horizontal Front"> matTooltip="Horizontal Front">
<mat-icon>arrow_forward</mat-icon> <mat-icon>arrow_forward</mat-icon>
</button> </button>
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_BEHIND)" <button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_BEHIND)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_BEHIND" [class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_BEHIND"
matTooltip="Horizontal Behind"> matTooltip="Horizontal Behind">
<mat-icon>arrow_back</mat-icon> <mat-icon>arrow_back</mat-icon>
</button> </button>
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_RIGHT)" <button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_RIGHT)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_RIGHT" [class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_RIGHT"
matTooltip="Horizontal Right"> matTooltip="Horizontal Right">
<mat-icon>arrow_right</mat-icon> <mat-icon>arrow_right</mat-icon>
</button> </button>
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_LEFT)" <button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_LEFT)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_LEFT" [class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_LEFT"
matTooltip="Horizontal Left"> matTooltip="Horizontal Left">
<mat-icon>arrow_left</mat-icon> <mat-icon>arrow_left</mat-icon>
</button> </button>
</div> </div>

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 {MatMiniFabButton} from '@angular/material/button';
import {IntersectionPlaneService, PlaneOrientation} from '../../services/intersection-plane.service'; 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 {InputHandlerService} from '../../services/input-handler.service';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {MatFormField, MatInput, MatLabel} from '@angular/material/input'; import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
import {ParticleManagerService} from '../../services/particle-manager.service';
@Component({ @Component({
selector: 'app-render-container', selector: 'app-render-container',
@ -20,20 +21,18 @@ import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
MatInput, MatInput,
MatFormField, MatFormField,
MatLabel MatLabel
], ],
templateUrl: './render-container.component.html', templateUrl: './render-container.component.html',
styleUrl: './render-container.component.scss' styleUrl: './render-container.component.scss'
}) })
export class RenderContainerComponent implements AfterViewInit, OnDestroy { export class RenderContainerComponent implements AfterViewInit, OnDestroy {
@ViewChild('rendererContainer') rendererContainer!: ElementRef; @ViewChild('rendererContainer') rendererContainer!: ElementRef;
constructor( private readonly intersectionPlaneService = inject(IntersectionPlaneService);
private intersectionPlaneService: IntersectionPlaneService, private readonly playerModelService = inject(PlayerModelService);
private playerModelService: PlayerModelService, private readonly inputHandlerService = inject(InputHandlerService);
private inputHandlerService: InputHandlerService, private readonly rendererService = inject(RendererService);
private rendererService: RendererService, private readonly particleManagerService = inject(ParticleManagerService);
) {
}
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.initializeScene(); this.initializeScene();
@ -99,6 +98,10 @@ export class RenderContainerComponent implements AfterViewInit, OnDestroy {
this.rendererService.resetCamera(); this.rendererService.resetCamera();
} }
public toggleShowParticlesWhenIntersectingPlane(): void {
this.particleManagerService.onlyIntersectingParticles = !this.particleManagerService.onlyIntersectingParticles;
}
/** /**
* Get the current plane orientation * Get the current plane orientation
*/ */
@ -106,6 +109,13 @@ export class RenderContainerComponent implements AfterViewInit, OnDestroy {
return this.intersectionPlaneService.getCurrentOrientation(); return this.intersectionPlaneService.getCurrentOrientation();
} }
/**
* Retrieves the value indicating whether only intersecting particles are being considered.
*/
public get onlyIntersecting(): boolean {
return this.particleManagerService.onlyIntersectingParticles;
}
/** /**
* Set the plane orientation * Set the plane orientation
*/ */

View File

@ -1,6 +1,7 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import * as THREE from 'three'; import * as THREE from 'three';
import {RendererService} from './renderer.service'; import {RendererService} from './renderer.service';
import {Subject} from 'rxjs';
/** /**
* Represents the possible orientations of the intersection plane * Represents the possible orientations of the intersection plane
@ -27,6 +28,10 @@ export class IntersectionPlaneService {
private planeLocked: boolean = false; private planeLocked: boolean = false;
private opacity: number = 0.05; 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) { constructor(private rendererService: RendererService) {
} }
@ -144,6 +149,13 @@ export class IntersectionPlaneService {
this.intersectionPlane.position.x = -position; this.intersectionPlane.position.x = -position;
break; 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,8 @@
import {Injectable} from '@angular/core'; import {inject, Injectable} from '@angular/core';
import * as THREE from 'three'; import * as THREE from 'three';
import {RendererService} from './renderer.service'; import {RendererService} from './renderer.service';
import {Particle, ParticleData, ParticleInfo, ParticleType} from '../models/particle.model'; import {Particle, ParticleData, ParticleInfo, ParticleType} from '../models/particle.model';
import {IntersectionPlaneService, PlaneOrientation} from './intersection-plane.service';
/** /**
* Service responsible for managing particles in the scene * Service responsible for managing particles in the scene
@ -33,8 +34,18 @@ export class ParticleManagerService {
private selectedColor: string = '#ff0000'; private selectedColor: string = '#ff0000';
private selectedParticle: Particle = Particle.DUST; private selectedParticle: Particle = Particle.DUST;
private selectedSize: number = 1; 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 +99,34 @@ export class ParticleManagerService {
renderFrameParticles(frameId: string): void { renderFrameParticles(frameId: string): void {
if (!this.particleData.frames[frameId]) return; 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]) { 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 particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);
const color = this.getColor(particleInfo); const color = this.getColor(particleInfo);
@ -245,4 +283,14 @@ export class ParticleManagerService {
generateJson(): string { generateJson(): string {
return JSON.stringify(this.particleData, null, 2); 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);
}
} }