Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,22 +250,28 @@ The painter system filters data by time range using Angular signals:
- Components show/hide based on time range
- Tween.js enables smooth animations

### 3. BVH Acceleration
### 3. Clipping (Angular Wedge / Z-axis)

- **Only `sceneGeometry` is clipped** — event data (`sceneEvent`) must NEVER be clipped.
- `sceneGeometry` is a `ClippingGroup` (WebGPU); clipping planes are set on the group, not on individual materials.
- `sceneEvent` is a regular `THREE.Group` — do not convert it to `ClippingGroup`.

### 4. BVH Acceleration

`three-mesh-bvh` provides fast raycasting for object selection:
- Lazy BVH computation on demand
- Frustum culling for performance
- Critical for large detector geometries

### 4. Service Singletons
### 5. Service Singletons

Angular services are singletons managing global state:
- Scene management (three.service)
- Event data (data-model.service)
- Configuration (config.service)
- URL parameters (url.service)

### 5. Security by Configuration (pyrobird)
### 6. Security by Configuration (pyrobird)

Restrictive defaults prevent unauthorized file access:
- Explicit opt-in for dangerous features
Expand Down
4 changes: 2 additions & 2 deletions firebird-ng/src/app/animation/animation-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
Mesh,
Scene,
Camera,
WebGLRenderer,
} from 'three';
import { WebGPURenderer } from 'three/webgpu';

// Animation Task Interface
export interface AnimationTask {
Expand Down Expand Up @@ -133,7 +133,7 @@ export class AnimationManager {
constructor(
private scene: Scene,
private activeCamera: Camera,
private renderer: WebGLRenderer
private renderer: WebGPURenderer
) {}

registerSequence(sequence: AnimationSequence) {
Expand Down
5 changes: 3 additions & 2 deletions firebird-ng/src/app/animation/default-animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
Sphere,
SphereGeometry,
TubeGeometry,
Vector3, WebGLRenderer
Vector3,
} from "three";
import { WebGPURenderer } from "three/webgpu";

export class CameraMoveTask implements AnimationTask {
name = 'CameraMove';
Expand Down Expand Up @@ -397,7 +398,7 @@ export class AnimateEventWithClippingTask implements AnimationTask {

constructor(
private scene: Scene,
private renderer: WebGLRenderer,
private renderer: WebGPURenderer,
private EVENT_DATA_ID: string,
tweenDuration: number,
onEnd?: () => void,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { Component, ElementRef, OnInit, OnDestroy, NgZone } from '@angular/core'
import { GizmoOptions, ViewportGizmo } from 'three-viewport-gizmo';
import { ThreeService } from '../../services/three.service';
import {
Matrix4,
OrthographicCamera,
PerspectiveCamera,
Scene,
WebGLRenderer
Vector3,
Vector4,
} from 'three';
import { WebGPURenderer } from 'three/webgpu';

@Component({
selector: 'app-cube-viewport-control',
Expand All @@ -18,7 +21,7 @@ export class CubeViewportControlComponent implements OnInit, OnDestroy {
// References to externally provided objects
public camera!: PerspectiveCamera | OrthographicCamera;
public scene!: Scene;
public renderer!: WebGLRenderer;
public renderer!: WebGPURenderer;

// The ViewportGizmo instance
public gizmo!: ViewportGizmo;
Expand Down Expand Up @@ -59,19 +62,21 @@ export class CubeViewportControlComponent implements OnInit, OnDestroy {
public initWithExternalScene(
scene: Scene,
camera: PerspectiveCamera | OrthographicCamera,
renderer: WebGLRenderer
renderer: WebGPURenderer
): void {
this.scene = scene;
this.camera = camera;
this.renderer = renderer;
this.lastCamera = camera;

const container = this.elRef.nativeElement.querySelector('.three-container');
// Use the renderer's parent element as container so the gizmo overlay
// is positioned relative to the actual canvas, not an empty div.
const container = renderer.domElement.parentElement ?? this.elRef.nativeElement;
const gizmoConfig: GizmoOptions = {
container: container,
size: 90,
type: 'cube',
offset: { top: 85 },
offset: { top: 115 },
background: { color: 0x444444, hover: { color: 0x444444 } },
corners: {
color: 0x333333,
Expand All @@ -86,6 +91,61 @@ export class CubeViewportControlComponent implements OnInit, OnDestroy {
// Create gizmo with custom config (autoPlace = false)
this.gizmo = new ViewportGizmo(this.camera, this.renderer, gizmoConfig);

// Patch domUpdate for WebGPU: the library computes viewport Y using
// WebGL's bottom-left origin, but WebGPU uses top-left.
// See: https://github.com/nicivore/three-viewport-gizmo/issues/13
const gizmo = this.gizmo as any;
const _v4 = new Vector4();
gizmo.domUpdate = function () {
gizmo._domRect = gizmo._domElement.getBoundingClientRect();
const r = gizmo.renderer;
const n = gizmo._domRect;
const i = r.domElement.getBoundingClientRect();
const isWebGPU = !!(r && r.isWebGPURenderer);
const y = isWebGPU
? (n.top - i.top)
: (r.domElement.clientHeight - (n.top - i.top + n.height));
gizmo._viewport.splice(0, 4, n.left - i.left, y, n.width, n.height);
r.getViewport(_v4).toArray(gizmo._originalViewport);
if (r.getScissorTest()) {
r.getScissor(_v4).toArray(gizmo._originalScissor);
}
return gizmo;
};

// Patch _setOrientation for collider coordinate convention:
// Beam axis = Z (horizontal), Y = up. When clicking "Top" the camera
// should look down Y with Z pointing right. The library's default uses
// this.up (Y-up) for lookAt which is degenerate when viewing along Y.
const _tempPos = new Vector3();
const _tempMat = new Matrix4();
const _zAxis = new Vector3(0, 0, 1);
const _yAxis = new Vector3(0, 1, 0);

gizmo._setOrientation = function (position: Vector3) {
const cam = gizmo.camera;
const target = gizmo.target;

// Choose an up vector that isn't parallel to the view direction
const absY = Math.abs(position.y);
const upVec = absY > 0.9 ? _zAxis : _yAxis;

_tempPos.copy(position).multiplyScalar(gizmo._distance);
_tempMat.setPosition(_tempPos).lookAt(_tempPos, gizmo.position, upVec);
gizmo._targetQuaternion.setFromRotationMatrix(_tempMat);

_tempPos.add(target);
_tempMat.lookAt(_tempPos, target, upVec);
gizmo._quaternionEnd.setFromRotationMatrix(_tempMat);

_tempMat.setPosition(cam.position).lookAt(cam.position, target, upVec);
gizmo._quaternionStart.setFromRotationMatrix(_tempMat);

gizmo.animating = true;
gizmo._clock.start();
gizmo.dispatchEvent({ type: 'start' });
};

// Bind the method to maintain 'this' context
this.handleControlsChange = this.handleControlsChange.bind(this);

Expand Down
56 changes: 28 additions & 28 deletions firebird-ng/src/app/data-pipelines/three-event.processor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Color, Line, Object3D, Vector3} from "three";
import {LineMaterial} from "three/examples/jsm/lines/LineMaterial.js";
import {Line2NodeMaterial} from "three/webgpu";
import {LineGeometry} from "three/examples/jsm/lines/LineGeometry.js";
import {Line2} from "three/examples/jsm/lines/Line2.js";

Expand Down Expand Up @@ -40,7 +40,7 @@ export enum NeonTrackColors {
export class ThreeEventProcessor {

/** This is primer, all other DASHED line materials take this and clone and change color */
dashedLineMaterial = new LineMaterial( {
dashedLine2NodeMaterial = new Line2NodeMaterial( {
color: 0xffff00,
linewidth: 20, // in world units with size attenuation, pixels otherwise
worldUnits: true,
Expand All @@ -52,7 +52,7 @@ export class ThreeEventProcessor {
} );

/** This is primer, all other SOLID line materials take this and clone and change color */
solidLineMaterial = new LineMaterial( {
solidLine2NodeMaterial = new Line2NodeMaterial( {
color: 0xffff00,
linewidth: 20, // in world units with size attenuation, pixels otherwise
worldUnits: true,
Expand All @@ -61,61 +61,61 @@ export class ThreeEventProcessor {
alphaToCoverage: true,
} );

gammaMaterial: LineMaterial;
opticalMaterial: LineMaterial;
electronMaterial: LineMaterial;
piPlusMaterial: LineMaterial;
piMinusMaterial: LineMaterial;
piZeroMaterial: LineMaterial;
protonMaterial: LineMaterial;
neutronMaterial: LineMaterial;
posChargeMaterial: LineMaterial;
negChargeMaterial: LineMaterial;
zeroChargeMaterial: LineMaterial;
scatteredElectronMaterial: LineMaterial;
gammaMaterial: Line2NodeMaterial;
opticalMaterial: Line2NodeMaterial;
electronMaterial: Line2NodeMaterial;
piPlusMaterial: Line2NodeMaterial;
piMinusMaterial: Line2NodeMaterial;
piZeroMaterial: Line2NodeMaterial;
protonMaterial: Line2NodeMaterial;
neutronMaterial: Line2NodeMaterial;
posChargeMaterial: Line2NodeMaterial;
negChargeMaterial: Line2NodeMaterial;
zeroChargeMaterial: Line2NodeMaterial;
scatteredElectronMaterial: Line2NodeMaterial;

constructor() {
this.gammaMaterial = this.dashedLineMaterial.clone();
this.gammaMaterial = this.dashedLine2NodeMaterial.clone();
this.gammaMaterial.color = new Color(NeonTrackColors.Yellow);
this.gammaMaterial.dashSize = 50;
this.gammaMaterial.gapSize = 50;

this.opticalMaterial = this.solidLineMaterial.clone();
this.opticalMaterial = this.solidLine2NodeMaterial.clone();
this.opticalMaterial.color = new Color(NeonTrackColors.Yellow);

this.electronMaterial = this.solidLineMaterial.clone();
this.electronMaterial = this.solidLine2NodeMaterial.clone();
this.electronMaterial.color = new Color(NeonTrackColors.Blue);

this.scatteredElectronMaterial = this.electronMaterial.clone();
this.scatteredElectronMaterial.linewidth = 30;

this.piPlusMaterial = this.solidLineMaterial.clone();
this.piPlusMaterial = this.solidLine2NodeMaterial.clone();
this.piPlusMaterial.color = new Color(NeonTrackColors.Pink);

this.piMinusMaterial = this.solidLineMaterial.clone();
this.piMinusMaterial = this.solidLine2NodeMaterial.clone();
this.piMinusMaterial.color = new Color(NeonTrackColors.Teal);

this.piZeroMaterial = this.dashedLineMaterial.clone();
this.piZeroMaterial = this.dashedLine2NodeMaterial.clone();
this.piZeroMaterial.color = new Color(NeonTrackColors.Salad);

this.protonMaterial = this.solidLineMaterial.clone();
this.protonMaterial = this.solidLine2NodeMaterial.clone();
this.protonMaterial.color = new Color(NeonTrackColors.Violet);

this.neutronMaterial = this.dashedLineMaterial.clone();
this.neutronMaterial = this.dashedLine2NodeMaterial.clone();
this.neutronMaterial.color = new Color(NeonTrackColors.Green);

this.posChargeMaterial = this.solidLineMaterial.clone();
this.posChargeMaterial = this.solidLine2NodeMaterial.clone();
this.posChargeMaterial.color = new Color(NeonTrackColors.Red);

this.negChargeMaterial = this.solidLineMaterial.clone();
this.negChargeMaterial = this.solidLine2NodeMaterial.clone();
this.negChargeMaterial.color = new Color(NeonTrackColors.DeepBlue);

this.zeroChargeMaterial = this.dashedLineMaterial.clone();
this.zeroChargeMaterial = this.dashedLine2NodeMaterial.clone();
this.zeroChargeMaterial.color = new Color(NeonTrackColors.Gray);

}

getMaterial(pdgName: string, charge: number): LineMaterial {
getMaterial(pdgName: string, charge: number): Line2NodeMaterial {
switch (pdgName) {
case "gamma":
return this.gammaMaterial;
Expand Down Expand Up @@ -196,7 +196,7 @@ export class ThreeEventProcessor {
material = this.scatteredElectronMaterial;
}

let line = new Line2( geometry, material );
let line = new Line2( geometry, material as any );

// line.scale.set( 1, 1, 1 );
line.name = "TrackLine2";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@


#eventDisplay {
position: relative;
padding-top: 105px;
margin-top: -105px;
overflow: hidden;
Expand Down
18 changes: 9 additions & 9 deletions firebird-ng/src/app/pages/main-display/main-display.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,7 @@ export class MainDisplayComponent implements OnInit, AfterViewInit, OnDestroy {
}


async ngOnInit() {
// Initialize the ThreeService scene/camera/renderer/controls
this.eventDisplay.initThree('eventDisplay');

// The facade will be initialized in three.service
this.facade.initializeScene()


ngOnInit() {
this.controller.buttonY.onPress.subscribe((value) => {
if (value) {
// TODO this.cycleGeometry();
Expand All @@ -173,7 +166,14 @@ export class MainDisplayComponent implements OnInit, AfterViewInit, OnDestroy {


// 2) AFTER VIEW INIT => handle resizing with DisplayShell or window
ngAfterViewInit(): void {
async ngAfterViewInit(): Promise<void> {

// Initialize the ThreeService scene/camera/renderer/controls
// Must happen in ngAfterViewInit so the DOM container #eventDisplay exists
await this.eventDisplay.initThree('eventDisplay');

// The facade will be initialized in three.service
this.facade.initializeScene()

if (this.isAutoLoadOnInit) {
// Load JSON based data files
Expand Down
12 changes: 7 additions & 5 deletions firebird-ng/src/app/pages/playground/playground.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {AfterViewInit, Component, OnInit, ViewChild, ElementRef} from '@angular/core';
import * as THREE from "three"
import { WebGPURenderer } from "three/webgpu";
import * as TWEEN from '@tweenjs/tween.js';

@Component({
Expand All @@ -13,7 +14,7 @@ export class PlaygroundComponent implements OnInit, AfterViewInit {

private scene!: THREE.Scene;
private camera!: THREE.PerspectiveCamera;
private renderer!: THREE.WebGLRenderer;
private renderer!: WebGPURenderer;
private particle1!: THREE.Mesh;
private particle2!: THREE.Mesh;
private lines: THREE.Line[] = [];
Expand Down Expand Up @@ -46,19 +47,20 @@ export class PlaygroundComponent implements OnInit, AfterViewInit {

ngOnInit(): void { }

ngAfterViewInit(): void {
this.initThreeJS();
async ngAfterViewInit(): Promise<void> {
await this.initThreeJS();
this.initCurvesAndLines();
this.animate();
}

initThreeJS(): void {
async initThreeJS(): Promise<void> {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.position.z = 5;

this.renderer = new THREE.WebGLRenderer();
this.renderer = new WebGPURenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
await this.renderer.init();
this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);

const geometry = new THREE.SphereGeometry(0.1, 32, 32);
Expand Down
Loading
Loading