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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

**Note:** Version bump only for package root

## Unreleased

- Add VR/AR controls in UI to start/end WebXR sessions and toggle AR DOM overlay (#558)
- dat.GUI: new `XR` folder with `Enter VR`, `Enter AR`, `AR DOM overlay`, `Exit XR`
- Phoenix menu: new `XR` section exposing the same actions
- Angular UI: `VR` and `AR` toggles wired to `EventDisplay` XR handlers

- Add per-collection "Extend to radius" option for tracks (#177)
- New helper: `RKHelper.extrapolateFromLastPosition(track, radius)`
- UI: dat.GUI and Phoenix menu controls to toggle extension and set radius
- Scene update: `SceneManager.extendCollectionTracks(collectionName, radius, enable)`




Expand Down
1 change: 1 addition & 0 deletions guides/developers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* [Phoenix event display](./event-display.md)
* [Event data format](./event_data_format.md)
* [Event data loader](./event-data-loader.md)
* [Track extension to radius](./track-extension.md)
* [Using JSROOT](./using-jsroot.md)
* [Running with XR (AR/VR) support](./running-with-xr-support.md)
* [Convert GDML/ROOT Geometry to GLTF](./convert-gdml-to-gltf.md)
Expand Down
12 changes: 12 additions & 0 deletions guides/developers/running-with-xr-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,15 @@ Now navigate to `https://localhost:4200` (notice the **https**) in your browser
## Debugging XR in the browser

Check out the [WebXR API Emulator extension](https://github.com/MozillaReality/WebXR-emulator-extension#how-to-use) to test and debug XR directly in the browser.

## Using the new XR UI

You can now start XR sessions directly from the Phoenix UI:

- In dat.GUI: open the `XR` folder and click `Enter VR` or `Enter AR`. Use `AR DOM overlay` to show Phoenix overlays on top of the AR scene, and `Exit XR` to leave.
- In the Phoenix menu: open the `XR` section and use the same actions (`Enter VR`, `Enter AR`, `AR DOM overlay`, `Exit XR`).
- In the Angular app UI: the top UI menu includes `VR` and `AR` toggles that call into the same XR session handlers.

Notes:
- WebXR requires a compatible browser/device and HTTPS. When developing locally, use the SSL start script above.
- AR scales the scene down and reduces camera near to suit real-world scale. Exiting AR restores previous values automatically.
96 changes: 96 additions & 0 deletions guides/developers/track-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Track Extension to Radius

## Overview

Phoenix supports optionally extending tracks with measured hits out to a specified transverse radius using Runge-Kutta propagation. This feature addresses issue #177 and allows tracks that already have measurements (e.g., `CombinedInDetTracks`) to be extended so they reach the calorimeter region, similar to tracks without measurements that are automatically propagated.

## User Interface

The extension feature is available on a **per-collection basis** through both the dat.GUI menu and Phoenix menu.

### dat.GUI

For each track collection, you'll find:
- **Extend to radius** (checkbox): Toggle to enable/disable track extension
- **Radius** (slider, 100-5000 mm): Set the target transverse radius

### Phoenix Menu

Under each track collection's "Draw Options":
- **Extend to radius** (checkbox): Toggle extension on/off
- **Extend radius** (slider, 100-5000 mm): Configure target radius

Changes take effect immediately — toggling or adjusting the radius rebuilds the track geometries in the scene.

## Implementation Details

### RKHelper

A new method `RKHelper.extrapolateFromLastPosition(track, radius)` extrapolates from the last measured hit outward until the track reaches the specified transverse radius (or propagation limits are hit).

**Parameters:**
- `track`: Track object with `pos` (measured hits) and `dparams` (track parameters)
- `radius`: Target transverse radius in mm

**Returns:** Array of additional position points `[x, y, z][]` (does not include the last measured point)

### SceneManager

The `SceneManager.extendCollectionTracks(collectionName, radius, enable)` method applies extension to all tracks in a collection:

1. Retrieves the collection group from EventData
2. For each track:
- Calls `RKHelper.extrapolateFromLastPosition` if enabled
- Rebuilds the tube and line geometries using measured + extrapolated points
- Persists extension state in `userData`:
- `extendedToRadius`: boolean (enabled/disabled)
- `extendRadius`: number (radius in mm)
- `extendedPos`: number[][] (array of extrapolated points)

**Note:** The original `track.pos` array is never modified — extrapolated points are stored separately.

### Performance Considerations

For collections with thousands of tracks:
- **Throttling**: Consider debouncing UI slider changes (e.g., only apply on `onFinishChange`)
- **Worker threads**: For very large datasets, compute extrapolation in a Web Worker to avoid blocking the main thread
- **Current implementation**: Uses synchronous RK propagation; suitable for typical event sizes (< 1000 tracks per collection)

## Example Usage

```typescript
// Enable extension for "CombinedInDetTracks" collection to 1500 mm radius
sceneManager.extendCollectionTracks('CombinedInDetTracks', 1500, true);

// Disable extension (revert to measured-only)
sceneManager.extendCollectionTracks('CombinedInDetTracks', 1500, false);

// Access extension state
const trackGroup = collection.children[0];
const params = trackGroup.userData;
if (params.extendedToRadius) {
console.log(`Extended to ${params.extendRadius} mm`);
console.log(`Extrapolated points:`, params.extendedPos);
}
```

## Testing

Unit test for `RKHelper.extrapolateFromLastPosition`:
- `packages/phoenix-event-display/src/tests/helpers/rk-helper.test.ts`

Integration test for `SceneManager.extendCollectionTracks`:
- `packages/phoenix-event-display/src/tests/managers/three-manager/scene-manager.test.ts`

## Related Files

- `packages/phoenix-event-display/src/helpers/rk-helper.ts`
- `packages/phoenix-event-display/src/managers/three-manager/scene-manager.ts`
- `packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts`
- `packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts`

## See Also

- [Event display guide](./event-display.md)
- [Event data format](./event_data_format.md)
- Issue #177: "Optionally extend all tracks to a radius"
76 changes: 76 additions & 0 deletions packages/phoenix-event-display/src/helpers/rk-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,80 @@ export class RKHelper {

return positions.concat(extrapolatedPos);
}

/**
* Extrapolate track from its last measured position out to a given transverse radius.
* Returns only the appended positions (does not include the last measured point).
* @param track Track which is to be extrapolated (should have `pos` and `dparams`)
* @param radius transverse radius in mm to extrapolate to
*/
public static extrapolateFromLastPosition(
track: { pos?: number[][]; dparams?: any },
radius: number,
): number[][] {
if (!track?.dparams) return [];

const lastPosArr =
track.pos && track.pos.length ? track.pos[track.pos.length - 1] : null;
if (!lastPosArr) return [];

const lastPos = new Vector3(lastPosArr[0], lastPosArr[1], lastPosArr[2]);

// Infer start direction using last two measured points if available
let startDir: Vector3 | null = null;
if (track.pos && track.pos.length > 1) {
const prev = track.pos[track.pos.length - 2];
const prevV = new Vector3(prev[0], prev[1], prev[2]);
startDir = lastPos.clone().sub(prevV).normalize();
}

const dparams = track.dparams;
const d0 = dparams[0];
const z0 = dparams[1];
const phi = dparams[2];
let theta = dparams[3];
const qop = dparams[4];

if (theta < 0) theta += Math.PI;

let p: number;
if (qop !== 0) p = Math.abs(1 / qop);
else p = Number.MAX_VALUE;
const q = Math.round(p * qop);

if (!startDir)
startDir = CoordinateHelper.sphericalToCartesian(
p,
theta,
phi,
).normalize();

const inbounds = (pos: Vector3) =>
Math.sqrt(pos.x * pos.x + pos.y * pos.y) <= radius;

const traj = RungeKutta.propagate(
lastPos,
startDir,
p,
q,
5,
1500,
inbounds,
);

const extrapolatedPos = traj.map((val) => [
val.pos.x,
val.pos.y,
val.pos.z,
]);

// Remove any point equal to lastPos (first point of traj may be identical)
const eps = 1e-6;
return extrapolatedPos.filter((pArr) => {
const dx = pArr[0] - lastPos.x;
const dy = pArr[1] - lastPos.y;
const dz = pArr[2] - lastPos.z;
return Math.abs(dx) > eps || Math.abs(dy) > eps || Math.abs(dz) > eps;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ import {
Quaternion,
DoubleSide,
BoxGeometry,
CatmullRomCurve3,
TubeGeometry,
MeshToonMaterial,
Line,
type Object3DEventMap,
} from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';
import { Font } from 'three/examples/jsm/loaders/FontLoader.js';
import { Cut } from '../../lib/models/cut.model';
import { CoordinateHelper } from '../../helpers/coordinate-helper';
import { RKHelper } from '../../helpers/rk-helper';
import HelvetikerFont from './fonts/helvetiker_regular.typeface.json';

/**
Expand Down Expand Up @@ -356,6 +361,93 @@ export class SceneManager {
return this.getObjectsGroup(SceneManager.EVENT_DATA_ID);
}

/**
* Optionally extend all tracks in a named collection out to a transverse radius.
* Rebuilds the per-track geometries (tube + line) by appending RK extrapolated points.
* NOTE: For large collections, consider throttling this method or computing on a worker thread.
* @param collectionName name of the collection group under EventData
* @param radius transverse radius in mm to extend to
* @param enable whether to enable (append extrapolated points) or disable (revert to measured only)
*/
public extendCollectionTracks(
collectionName: string,
radius: number,
enable: boolean,
) {
const eventData = this.getEventData();
if (!eventData) return;
const collection = eventData.getObjectByName(collectionName) as Group;
if (!collection) return;

for (const child of Object.values(collection.children)) {
const trackGroup = child as Group;
const trackParams = (trackGroup as any).userData;
if (!trackParams) continue;
if (!trackParams.pos || !trackParams.dparams) continue;

const basePos: number[][] = trackParams.pos;
let positions: number[][] = basePos;
let extendedPos: number[][] = [];

if (enable) {
const extra = RKHelper.extrapolateFromLastPosition(trackParams, radius);
if (extra && extra.length) {
positions = basePos.concat(extra);
extendedPos = extra;
}
}

// Persist extension state in userData for downstream code/export
trackParams.extendedToRadius = enable;
trackParams.extendRadius = radius;
trackParams.extendedPos = extendedPos;

// Build new geometries from positions
const points: Vector3[] = positions.map(
(p) => new Vector3(p[0], p[1], p[2]),
);
const curve = new CatmullRomCurve3(points);
const vertices = curve.getPoints(50);

// Find tube (Mesh) and line (Line) children
let tubeObject: Mesh | undefined;
let lineObject: Line | undefined;
for (const obj of trackGroup.children) {
if (
(obj as any).type === 'Mesh' &&
(obj as any).material &&
(obj as any).name !== 'Track'
) {
tubeObject = obj as Mesh;
}
if ((obj as any).type === 'Line') {
lineObject = obj as Line;
}
}

const linewidth = trackParams.linewidth ? trackParams.linewidth : 2;

if (tubeObject) {
try {
const newGeo: any = new TubeGeometry(curve, undefined, linewidth);
if (tubeObject.geometry) tubeObject.geometry.dispose?.();
tubeObject.geometry = newGeo;
} catch (e) {
// Fall back silently if TubeGeometry not available
}
}

if (lineObject) {
const newLineGeom = new BufferGeometry().setFromPoints(vertices);
if (lineObject.geometry) lineObject.geometry.dispose?.();
lineObject.geometry = newLineGeom;
}

// Update trackGroup userData to reflect current state
(trackGroup as any).userData = trackParams;
}
}

/**
* Get geometries inside the scene.
* @returns A group of objects with geometries.
Expand Down
Loading