Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/firebird-ng/src/app/painters/trajectory.painter.ts is written in an unsupported language. File is not indexed.

0001 import {EventGroupPainter} from "./event-group-painter";
0002 import {EventGroup} from "../model/event-group";
0003 import {
0004   PointTrajectoryGroup,
0005   PointTrajectory
0006 } from "../model/point-trajectory.group";
0007 
0008 import {Color, Object3D} from "three";
0009 import {LineMaterial} from "three/examples/jsm/lines/LineMaterial.js";
0010 import {LineGeometry} from "three/examples/jsm/lines/LineGeometry.js";
0011 import {Line2} from "three/examples/jsm/lines/Line2.js";
0012 
0013 /** Example color set. Feel free to refine or expand. */
0014 export enum NeonTrackColors {
0015   Red = 0xFF0007,
0016   Pink = 0xCF00FF,
0017   Violet = 0x5400FF,
0018   Blue = 0x0097FF,
0019   DeepBlue = 0x003BFF,
0020   Teal = 0x00FFD1,
0021   Green = 0x13FF00,
0022   Salad = 0x8CFF00,
0023   Yellow = 0xFFEE00,
0024   Orange = 0xFF3500,
0025   Gray = 0xAAAAAA,
0026 }
0027 
0028 /**
0029  * We'll keep each line's full data in a small structure so we can rebuild partial geometry.
0030  */
0031 interface TrajectoryRenderContext {
0032   collectionIndex: number;           // Index in the array
0033   lineObj: Line2;                    // the Line2 object in the scene
0034   points: number[][];                // the raw array of [x, y, z, t, dx, dy, dz, dt]
0035   lineMaterial: LineMaterial;        // the material used
0036   startTime: number;                 // The time of the first point
0037   endTime: number;                   // The end of the last point
0038   params: Record<string, any>;       // Track parameters
0039   lastPaintIndex: number;            // This is needed for partial track draw optimization
0040 }
0041 
0042 /**
0043  * Painter that draws lines for a "PointTrajectoryComponent",
0044  * supporting partial display based on time.
0045  */
0046 export class TrajectoryPainter extends EventGroupPainter {
0047   /** A small array to store each line's data and references. */
0048   public trajectories: TrajectoryRenderContext[] = [];
0049   private timeColumnIndex = 3;         // TODO check that line has time column
0050 
0051   /** Base materials that we clone for each line. */
0052   private baseSolidMaterial: LineMaterial;
0053   private baseDashedMaterial: LineMaterial;
0054 
0055   public readonly trackColorHighlight = 0xff4081; // vivid pink for highlight
0056   public readonly trackWidthFactor = 2;          // how many times thicker when highlighted
0057 
0058   constructor(parentNode: Object3D, component: EventGroup) {
0059     super(parentNode, component);
0060 
0061     if (component.type !== PointTrajectoryGroup.type) {
0062       throw new Error("Wrong component type given to PointTrajectoryPainter.");
0063     }
0064 
0065     // Create base materials
0066     this.baseSolidMaterial = new LineMaterial({
0067       color: 0xffffff,
0068       linewidth: 10,   // in world units
0069       worldUnits: true,
0070       dashed: false,
0071       alphaToCoverage: true
0072     });
0073 
0074     this.baseDashedMaterial = new LineMaterial({
0075       color: 0xffffff,
0076       linewidth: 10,
0077       worldUnits: true,
0078       dashed: true,
0079       dashSize: 100,
0080       gapSize: 100,
0081       alphaToCoverage: true
0082     });
0083 
0084     // Build lines at construction
0085     this.initLines();
0086   }
0087 
0088   /**
0089    * Builds the Line2 objects for each line in the data.
0090    * Initially, we set them fully visible (or we could set them invisible).
0091    */
0092   private initLines() {
0093 
0094     const component = this.component as PointTrajectoryGroup;
0095 
0096     // Let us see if paramColumns includes "pdg" or "charge" or something.
0097     const pdgIndex = component.paramColumns.indexOf("pdg");
0098     const chargeIndex = component.paramColumns.indexOf("charge");
0099     let paramsToColumnsMismatchWarned = false;
0100     let noPointsWarned = 0;
0101 
0102 
0103     for (let trajIndex = 0; trajIndex < component.trajectories.length; trajIndex++) {
0104       const trajectory = component.trajectories[trajIndex];
0105 
0106       // Copy params
0107       const paramColumns = component.paramColumns;
0108       const params = trajectory.params;
0109       if (params.length != paramColumns.length && !paramsToColumnsMismatchWarned) {
0110         // We do the warning only once!
0111         console.error(`params.length(${params.length})  != paramColumns.length(${paramColumns.length}) at '${component.name}'. This should never happen!`);
0112         paramsToColumnsMismatchWarned = true;
0113       }
0114 
0115       // We intentionally use the very dumb method, but this method allows us to do at least something if they mismatch
0116       const paramArrLen = Math.min(paramColumns.length, params.length);
0117       const paramsDict: Record<string, any> = {};
0118       for (let i = 0; i < paramArrLen; i++) {
0119         paramsDict[paramColumns[i]] = params[i];
0120       }
0121 
0122       // Check we have enough points to build at least something!
0123       if (trajectory.points.length <= 1) {
0124         if (noPointsWarned < 10) {
0125           const result = Object.entries(paramsDict)
0126             .map(([key, value]) => `${key}:${value}`)
0127             .join(", ");
0128           console.warn(`Trajectory has ${trajectory.points.length} points. This can't be. Track parameters: ${result}`);
0129           noPointsWarned++;
0130         }
0131         continue;   // Skip this line!
0132       }
0133 
0134       // Create proper material
0135       const lineMaterial = this.createLineMaterial(trajectory, pdgIndex, chargeIndex);
0136 
0137       // We'll start by building a geometry with *all* points, and rely on paint() to do partial logic.
0138       // We'll store the full set of points in linesData, then paint() can rebuild partial geometry.
0139       const geometry = new LineGeometry();
0140       const fullPositions = this.generateFlatXYZ(trajectory.points);
0141       geometry.setPositions(fullPositions);
0142 
0143       const line2 = new Line2(geometry, lineMaterial);
0144       line2.computeLineDistances();
0145 
0146       // Add to the scene
0147       this.parentNode.add(line2);
0148 
0149       let startTime = 0;
0150       let endTime = 0;
0151       if (trajectory.points[0].length > this.timeColumnIndex) {
0152         startTime = trajectory.points[0][this.timeColumnIndex];
0153         endTime = trajectory.points[trajectory.points.length - 1][this.timeColumnIndex]
0154       }
0155 
0156       const trajData: TrajectoryRenderContext = {
0157         collectionIndex: trajIndex,
0158         lineObj: line2,
0159         lineMaterial: lineMaterial,
0160         points: trajectory.points,
0161         startTime: startTime,
0162         endTime: endTime,
0163         params: paramsDict,
0164         lastPaintIndex: 0,
0165       }
0166 
0167       trajData.lineObj.name = this.getNodeName(trajData, component.trajectories.length);
0168       trajData.lineObj.userData["track_params"] = trajData.params;
0169 
0170       // Store the original material properties for highlighting
0171       const origColor = lineMaterial.color.getHex();
0172       const origWidth = lineMaterial.linewidth;
0173 
0174       // Define proper highlight and unhighlight functions
0175       trajData.lineObj.userData["highlightFunction"] = () => {
0176         // Store original values if not already stored
0177         if (!trajData.lineObj.userData["origColor"]) {
0178           trajData.lineObj.userData["origColor"] = origColor;
0179           trajData.lineObj.userData["origWidth"] = origWidth;
0180         }
0181 
0182         // Apply highlight
0183         const mat = trajData.lineObj.material as LineMaterial;
0184         mat.color.setHex(this.trackColorHighlight);
0185         mat.linewidth = origWidth * this.trackWidthFactor;
0186         mat.needsUpdate = true;
0187       };
0188 
0189       trajData.lineObj.userData["unhighlightFunction"] = () => {
0190         // Restore original properties
0191         if (trajData.lineObj.userData["origColor"] !== undefined) {
0192           const mat = trajData.lineObj.material as LineMaterial;
0193           mat.color.setHex(trajData.lineObj.userData["origColor"]);
0194           mat.linewidth = trajData.lineObj.userData["origWidth"];
0195           mat.needsUpdate = true;
0196         }
0197       };
0198 
0199       // Keep the data
0200       this.trajectories.push(trajData);
0201     }
0202   }
0203 
0204   /**
0205    * Creates or picks a line material based on PDG or charge, etc.
0206    */
0207   private createLineMaterial(line: PointTrajectory, pdgIndex: number, chargeIndex: number) {
0208 
0209 
0210     // Try to read PDG and/or charge from line.params
0211     // This assumes line.params matches paramColumns.
0212     let pdg = 0, charge = 0;
0213     if (pdgIndex >= 0 && pdgIndex < line.params.length) {
0214       pdg = Math.floor(line.params[pdgIndex]);
0215     }
0216     if (chargeIndex >= 0 && chargeIndex < line.params.length) {
0217       charge = line.params[chargeIndex];
0218     }
0219 
0220     // Minimal PDG-based color logic
0221     // ---------- PDG‑specific cases ----------
0222     switch (pdg) {
0223       case  22: {                             // γ
0224         const mat = this.baseDashedMaterial.clone();
0225         mat.color = new Color(NeonTrackColors.Yellow);
0226         return mat;
0227       }
0228       case -22: {                            // optical photon
0229         const mat = this.baseSolidMaterial.clone();
0230         mat.color = new Color(NeonTrackColors.Salad);
0231         mat.linewidth = 2;
0232         return mat;
0233       }
0234       case  11: {                            // e⁻
0235         const mat = this.baseSolidMaterial.clone();
0236         mat.color = new Color(NeonTrackColors.Blue);
0237         return mat;
0238       }
0239       case -11: {                            // e⁺
0240         const mat = this.baseSolidMaterial.clone();
0241         mat.color = new Color(NeonTrackColors.Orange);
0242         return mat;
0243       }
0244       case  211: {                           // π⁺
0245         const mat = this.baseSolidMaterial.clone();
0246         mat.color = new Color(NeonTrackColors.Pink);
0247         return mat;
0248       }
0249       case -211: {                           // π⁻
0250         const mat = this.baseSolidMaterial.clone();
0251         mat.color = new Color(NeonTrackColors.Teal);
0252         return mat;
0253       }
0254       case  2212: {                          // proton
0255         const mat = this.baseSolidMaterial.clone();
0256         mat.color = new Color(NeonTrackColors.Violet);
0257         return mat;
0258       }
0259       case  2112: {                          // neutron
0260         const mat = this.baseDashedMaterial.clone();
0261         mat.color = new Color(NeonTrackColors.Green);
0262         return mat;
0263       }
0264     }
0265 
0266     // ---------- Fallback by charge ----------
0267     if (charge > 0) {
0268       const mat = this.baseSolidMaterial.clone();
0269       mat.color = new Color(NeonTrackColors.Red);
0270       return mat;
0271     }
0272 
0273     if (charge < 0) {
0274       const mat = this.baseSolidMaterial.clone();
0275       mat.color = new Color(NeonTrackColors.DeepBlue);
0276       return mat;
0277     }
0278 
0279     // Neutral fallback
0280     const mat = this.baseSolidMaterial.clone();
0281     mat.color = new Color(NeonTrackColors.Gray);
0282     return mat;
0283   }
0284 
0285   /**
0286    * Helper to flatten the [x, y, z, t, ...] points into [x0, y0, z0, x1, y1, z1, ...].
0287    * We skip anything beyond the first 3 indices in each point array, because
0288    * x=0,y=1,z=2 are the first three.
0289    */
0290   private generateFlatXYZ(points: number[][]): number[] {
0291     const flat: number[] = [];
0292     for (let i = 0; i < points.length; i++) {
0293       flat.push(points[i][0], points[i][1], points[i][2]); // x,y,z
0294     }
0295     return flat;
0296   }
0297 
0298   /**
0299    * The main Paint method, called each time the user updates "time."
0300    * If time is null - timeless mode, we show the entire tracks. Otherwise, we show partial up to that time.
0301    */
0302   public override paint(time: number | null): void {
0303 
0304     if (time === null) {
0305       this.paintNoTime();
0306     } else {
0307       this.paintAtTime(time);
0308     }
0309   }
0310 
0311   private paintNoTime() {
0312     for (const track of this.trajectories) {
0313       // Rebuild geometry with *all* points
0314       track.lineObj.visible = true;
0315       track.lineObj.geometry.instanceCount = Infinity;
0316     }
0317   }
0318 
0319 
0320   /**
0321    * Improved fastPaint function with proper boundary checking between time points
0322    * @param time Current simulation time
0323    */
0324   public paintAtTime(time: number): void {
0325     // First pass: categorize tracks as fully visible, partial, or hidden
0326     const partialTracks: TrajectoryRenderContext[] = [];
0327 
0328     for (const track of this.trajectories) {
0329       // Hide tracks that haven't started yet
0330       if (track.startTime > time) {
0331         track.lineObj.visible = false;
0332         track.lastPaintIndex = -1;       // if time moves forward, and we start showing track the next time
0333         continue;
0334       }
0335 
0336       // Show track
0337       track.lineObj.visible = true;
0338 
0339       // If track has already ended, show it completely
0340       if (track.endTime <= time) {
0341         track.lineObj.geometry.instanceCount = Infinity;
0342 
0343         // if next paint the time moves backward, and we start hiding track parts,
0344         // we want lastPaintIndex to correspond to fully rendered track
0345         track.lastPaintIndex = this.trajectories.length - 1;
0346         continue;
0347       }
0348 
0349       // This track is only partially visible and will be treated the next
0350       partialTracks.push(track);
0351     }
0352 
0353     // Second pass: handle partially visible tracks
0354     for (const track of partialTracks) {
0355       // Validate lastPaintIndex
0356       if (track.lastPaintIndex < 0 || track.lastPaintIndex >= track.points.length) {
0357         track.lastPaintIndex = 0;
0358       }
0359 
0360       // Find the correct interval where the current time falls
0361       // This is the key improvement: check if we need to move to next/previous point
0362       // rather than just searching forward or backward arbitrarily
0363 
0364       let needToUpdate = true;
0365       while (needToUpdate) {
0366         needToUpdate = false;
0367 
0368         // Check if we should move forward to next point
0369         if (track.lastPaintIndex < track.points.length - 1 &&
0370           time >= track.points[track.lastPaintIndex + 1][this.timeColumnIndex]) {
0371           track.lastPaintIndex++;
0372           needToUpdate = true;
0373         }
0374         // Check if we should move backward to previous point
0375         else if (track.lastPaintIndex > 0 &&
0376           time < track.points[track.lastPaintIndex][this.timeColumnIndex]) {
0377           track.lastPaintIndex--;
0378           needToUpdate = true;
0379         }
0380       }
0381 
0382       // At this point, we've found the correct index where:
0383       // time is between points[lastPaintIndex] and points[lastPaintIndex+1]
0384       // Show points up to and including lastPaintIndex
0385       track.lineObj.geometry.instanceCount = track.lastPaintIndex + 1;
0386     }
0387   }
0388 
0389   /**
0390    * Dispose all line objects, geometry, materials
0391    */
0392   public override dispose(): void {
0393     for (const ld of this.trajectories) {
0394       const geom = ld.lineObj.geometry as LineGeometry;
0395       geom.dispose();
0396       ld.lineMaterial.dispose();
0397 
0398       if (this.parentNode) {
0399         this.parentNode.remove(ld.lineObj);
0400       }
0401     }
0402     this.trajectories = [];
0403     super.dispose();
0404   }
0405 
0406   private getNodeName(trajData: TrajectoryRenderContext, trajCount: number) {
0407 
0408     // Calculate the number of digits needed (order of magnitude + 1)
0409     const padLength = Math.floor(Math.log10(trajCount)) + 1;
0410 
0411     // Use padStart to pad the string representation with leading zeros
0412     const indexStr = String(trajData.collectionIndex).padStart(padLength, ' ');
0413 
0414 
0415     let name = "track"
0416     if ("type" in trajData.params) {
0417       name = trajData.params["type"]
0418     } else if ("pdg" in trajData.params) {
0419       name = trajData.params["pdg"]
0420     } else if ("charge" in trajData.params) {
0421       const charge = parseFloat(trajData.params["cahrge"]);
0422       if (Math.abs(charge) < 0.00001) {
0423         name = "NeuTrk";
0424       }
0425       if (charge > 0) {
0426         name = "PosTrk";
0427       } else if (charge < 0) {
0428         name = "NegTrk";
0429       }
0430     }
0431     name = "[" + name + "]"
0432 
0433     let time = "no-t"
0434     if (Math.abs(trajData.startTime) > 0.000001 || Math.abs(trajData.endTime) > 0.000001) {
0435       time = `t:${trajData.startTime.toFixed(1)}-${trajData.endTime.toFixed(1)}`;
0436     }
0437 
0438     let momentum = "no-p"
0439     if ("px" in trajData.params && "py" in trajData.params && "pz" in trajData.params) {
0440       let px = parseFloat(trajData.params["px"]);
0441       let py = parseFloat(trajData.params["py"]);
0442       let pz = parseFloat(trajData.params["pz"]);
0443       momentum = "p:" + (Math.sqrt(px * px + py * py + pz * pz) / 1000.0).toFixed(3);
0444     }
0445 
0446     return `${indexStr} ${trajData.collectionIndex} ${name} ${momentum} ${time}`;
0447   }
0448 }