Skip to content
Open
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
20 changes: 20 additions & 0 deletions packages/phoenix-event-display/src/event-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { URLOptionsManager } from './managers/url-options-manager';
import type {
PhoenixEventData,
PhoenixEventsData,
PileupEvent,
} from './lib/types/event-data';

declare global {
Expand Down Expand Up @@ -803,6 +804,25 @@ export class EventDisplay {
this.graphicsLibrary.animateClippingWithCollision(tweenDuration, onEnd);
}

/**
* Animate multiple pp interactions from a single bunch crossing (pileup),
* sequenced in time order using the timestamp stored on each interaction.
*
* Each interaction produces a brief vertex flash and then its tracks are
* drawn out proportionally to its timestamp relative to the bunch crossing.
*
* @param pileupEvent Object containing all pileup interactions with timestamps.
* @param totalDuration Total duration of the full pileup animation in ms. Default 6000.
* @param onEnd Callback fired when all interactions have finished animating.
*/
public animatePileup(
pileupEvent: PileupEvent,
totalDuration: number = 6000,
onEnd?: () => void,
): void {
this.graphicsLibrary.animatePileup(pileupEvent, totalDuration, onEnd);
}

/**
* Add label to a 3D object.
* @param label Label to add to the event object.
Expand Down
34 changes: 34 additions & 0 deletions packages/phoenix-event-display/src/lib/types/event-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,37 @@ export interface PhoenixEventsData {
/** Individual event data. */
[eventKey: string]: PhoenixEventData;
}

// ─────────────────────────────────────────────
// Pileup types (added for issue #235)
// ─────────────────────────────────────────────

/**
* Represents a single pp interaction in a pileup bunch crossing.
* Each interaction has a timestamp (in picoseconds) that is used
* to sequence its animation relative to the other interactions.
*/
export interface PileupInteraction {
/**
* Timestamp of this interaction in picoseconds, relative to
* the bunch crossing reference time (t=0).
*/
timestamp: number;
/** 3D position of the primary vertex for this interaction. */
vertex: { x: number; y: number; z: number };
/** Tracks originating from this interaction (Phoenix TrackParams format). */
tracks?: TrackParams[];
/** Jets originating from this interaction (Phoenix JetParams format). */
jets?: JetParams[];
}

/**
* A full pileup event containing multiple simultaneous pp interactions
* from a single bunch crossing (up to ~200 at HL-LHC).
* Feed this to AnimationsManager.animatePileup() to visualise the
* time-ordered sequence of collisions.
*/
export interface PileupEvent {
/** All pp interactions in this bunch crossing. */
interactions: PileupInteraction[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { SceneManager } from './scene-manager';
import { RendererManager } from './renderer-manager';
import { TracksMesh } from '../../loaders/objects/tracks';
import type { PileupEvent } from '../../lib/types/event-data';

/** Type for animation preset. */
export interface AnimationPreset {
Expand Down Expand Up @@ -153,7 +154,7 @@ export class AnimationsManager {
onEnd?: () => void,
onAnimationStart?: () => void,
) {
// 🔥 Hide labels at the start of the animation
// Hide labels at the start of the animation
const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID);
if (labelsGroup) labelsGroup.visible = false;

Expand Down Expand Up @@ -319,11 +320,11 @@ export class AnimationsManager {
tween.easing(Easing.Quartic.Out).start();
}

// 🔥 FINAL animation end handler
// FINAL animation end handler
animationSphereTweenClone.onComplete(() => {
onAnimationSphereUpdate(new Sphere(new Vector3(), Infinity));

// 🔥 Show labels again when the animation ends
// Show labels again when the animation ends
const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID);
if (labelsGroup) labelsGroup.visible = true;

Expand All @@ -349,7 +350,7 @@ export class AnimationsManager {
return;
}

// 🔥 Hide labels at the start of the animation
// Hide labels at the start of the animation
const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID);
if (labelsGroup) labelsGroup.visible = false;

Expand Down Expand Up @@ -570,7 +571,7 @@ export class AnimationsManager {
const { positions, animateEventAfterInterval, collisionDuration } =
animationPreset;

// 🔥 Hide labels at the start of the preset animation
// Hide labels at the start of the preset animation
const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID);
if (labelsGroup) labelsGroup.visible = false;

Expand Down Expand Up @@ -598,14 +599,194 @@ export class AnimationsManager {
previousTween = tween;
});

// 🔥 When animation finishes, show labels again
// When animation finishes, show labels again
previousTween.onComplete(() => {
const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID);
if (labelsGroup) labelsGroup.visible = true;

onEnd?.(); // Call original callback
onEnd?.();
});

firstTween.start();
}

// ─────────────────────────────────────────────────────────────────────────
// Pileup animation (added for issue #235)
// ─────────────────────────────────────────────────────────────────────────

/**
* Animate multiple pp interactions (pileup) from a single bunch crossing,
* sequenced in time order using the timestamp stored on each interaction.
*
* Each interaction gets a brief vertex flash (collideParticles) and then
* its associated tracks are drawn out via draw-range tweens — exactly the
* same mechanism used in animateEvent(), but fired at a delay proportional
* to the interaction's relative timestamp.
*
* @param pileupEvent Object containing all pileup interactions with timestamps.
* @param totalDuration Total wall-clock duration of the full animation in ms.
* @param onEnd Callback fired when the last interaction has finished animating.
*/
// ─────────────────────────────────────────────────────────────────────────
// Pileup animation (final corrected version)
// ─────────────────────────────────────────────────────────────────────────

public animatePileup(
pileupEvent: PileupEvent,
totalDuration: number = 8000,
onEnd?: () => void,
): void {
const { interactions } = pileupEvent;

if (!interactions || interactions.length === 0) {
onEnd?.();
return;
}

// Hide labels during animation
const labelsGroup = this.scene.getObjectByName(SceneManager.LABELS_ID);
if (labelsGroup) labelsGroup.visible = false;

// Sort interactions by timestamp
const sorted = [...interactions].sort((a, b) => a.timestamp - b.timestamp);

const minT = sorted[0].timestamp;
const maxT = sorted[sorted.length - 1].timestamp;
const timeRange = maxT - minT || 1;

// More spacing for better visual clarity
const sequencingWindow = totalDuration * 0.8;
const trackDuration = totalDuration * 0.5;

let completedCount = 0;
const totalCount = sorted.length;

const onInteractionComplete = () => {
completedCount++;
if (completedCount === totalCount) {
if (labelsGroup) labelsGroup.visible = true;
onEnd?.();
}
};

for (const interaction of sorted) {
const delay =
((interaction.timestamp - minT) / timeRange) * sequencingWindow;

setTimeout(() => {
// Flash at actual vertex position (FIXED)
const { x, y, z } = interaction.vertex;

const flashGeo = new SphereGeometry(10, 16, 16);
const flashMat = new MeshBasicMaterial({
color: new Color(0x88ccff),
transparent: true,
opacity: 1,
});

const flash = new Mesh(flashGeo, flashMat);
flash.position.set(x, y, z);

this.scene.add(flash);

new Tween(flashMat, this.tweenGroup)
.to({ opacity: 0 }, 500)
.onComplete(() => {
this.scene.remove(flash);
})
.start();

// Animate tracks (if available)
this._animateInteractionTracks(
interaction,
trackDuration,
onInteractionComplete,
);
}, delay);
}
}

/**
* Animate tracks near a specific interaction vertex
*/
private _animateInteractionTracks(
interaction: PileupEvent['interactions'][number],
duration: number,
onEnd?: () => void,
): void {
const eventData = this.scene.getObjectByName(SceneManager.EVENT_DATA_ID);
if (!eventData) {
onEnd?.();
return;
}

const vertexPos = new Vector3(
interaction.vertex.x,
interaction.vertex.y,
interaction.vertex.z,
);

const PROXIMITY_THRESHOLD = 50;
const allTweens: any[] = [];

eventData.traverse((obj: any) => {
if (!obj.geometry) return;
if (obj.name !== 'Track' && obj.name !== 'LineHit') return;

const worldPos = new Vector3();
obj.getWorldPosition(worldPos);

if (worldPos.distanceTo(vertexPos) > PROXIMITY_THRESHOLD) return;

if (obj.geometry instanceof TracksMesh) {
obj.material.progress = 0;

const tween = new Tween(obj.material, this.tweenGroup)
.to({ progress: 1 }, duration)
.easing(Easing.Quartic.Out);

tween.onComplete(() => {
obj.material.progress = 1;
});

allTweens.push(tween);
} else if (obj.geometry instanceof BufferGeometry) {
let count = obj.geometry?.attributes?.position?.count ?? 0;
if (obj.geometry instanceof TubeGeometry) count *= 6;
if (count === 0) return;

const savedCount = obj.geometry.drawRange.count;
obj.geometry.setDrawRange(0, 0);

const tween = new Tween(obj.geometry.drawRange, this.tweenGroup)
.to({ count }, duration)
.easing(Easing.Quartic.Out);

tween.onComplete(() => {
obj.geometry.drawRange.count = savedCount;
});

allTweens.push(tween);
}
});

if (allTweens.length === 0) {
onEnd?.();
return;
}

// Correct async completion handling
let completed = 0;
const total = allTweens.length;

for (const tween of allTweens) {
tween.onComplete(() => {
completed++;
if (completed === total) {
onEnd?.();
}
});
tween.start();
}
}
}
17 changes: 17 additions & 0 deletions packages/phoenix-event-display/src/managers/three-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { XRManager, XRSessionType } from './xr/xr-manager';
import { VRManager } from './xr/vr-manager';
import { ARManager } from './xr/ar-manager';
import type { GeometryUIParameters } from '../../lib/types/geometry-ui-parameters';
import type { PileupEvent } from '../../lib/types/event-data';

(function () {
const _updateMatrixWorld = Object3D.prototype.updateMatrixWorld;
Expand Down Expand Up @@ -1400,6 +1401,22 @@ export class ThreeManager {
this.animationsManager.animateClippingWithCollision(tweenDuration, onEnd);
}

/**
* Animate multiple pp interactions (pileup) from a single bunch crossing,
* sequenced in time order using the timestamp on each interaction.
* Delegates to AnimationsManager.animatePileup().
* @param pileupEvent Object containing all pileup interactions with timestamps.
* @param totalDuration Total duration of the full animation in ms. Default 6000.
* @param onEnd Callback fired when all interactions have finished animating.
*/
public animatePileup(
pileupEvent: PileupEvent,
totalDuration: number = 6000,
onEnd?: () => void,
): void {
this.animationsManager.animatePileup(pileupEvent, totalDuration, onEnd);
}

/** Saves a blob */
private saveBlob(blob: Blob | MediaSource, fileName: string) {
const a = document.createElement('a');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@
(click)="toggleAnimateEvent()"
>
</app-menu-toggle>

<app-menu-toggle
tooltip="Animate pileup"
icon="event-folder"
[active]="isPileupAnimating"
(click)="toggleAnimatePileup()"
>
</app-menu-toggle>
Loading
Loading