Back to home page

EIC code displayed by LXR

 
 

    


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

0001 import {EventPiecePainter, PainterConfigView, PainterMeta} from "./event-piece-painter";
0002 import {EventPiece} from "../model/event-piece";
0003 import {PointTrajectoryPiece} from "../model/point-trajectory.piece";
0004 
0005 import {Color, Object3D} from "three";
0006 // Import from three/webgpu, NEVER from three/src/... : a deep src import can
0007 // be served as a second copy of the whole three node system (seen with the
0008 // vite dep optimizer), whose module-level TSL stack state is separate from
0009 // the renderer's — the material build then throws "No stack defined for
0010 // assign operation" and tracks render as bare hairlines.
0011 import {Line2NodeMaterial} from "three/webgpu";
0012 import {LineGeometry} from "three/examples/jsm/lines/LineGeometry.js";
0013 import {Line2} from "three/examples/jsm/lines/webgpu/Line2.js";
0014 
0015 
0016 
0017 /** Example color set. Feel free to refine or expand. */
0018 export enum NeonTrackColors {
0019   Red = 0xFF0007,
0020   Pink = 0xCF00FF,
0021   Violet = 0x5400FF,
0022   Blue = 0x0097FF,
0023   DeepBlue = 0x003BFF,
0024   Teal = 0x00FFD1,
0025   Green = 0x13FF00,
0026   Salad = 0x8CFF00,
0027   Yellow = 0xFFEE00,
0028   Orange = 0xFF3500,
0029   Gray = 0xAAAAAA,
0030 }
0031 
0032 /**
0033  * The pid-mode track color, shared by the per-track and batched trajectory
0034  * painters so switching between them keeps the palette.
0035  */
0036 export function pidColorFor(pdg: number, charge: number): number {
0037   switch (pdg) {
0038     case 22: return NeonTrackColors.Yellow;      // γ
0039     case -22: return NeonTrackColors.Salad;      // optical photon
0040     case 11: return NeonTrackColors.Blue;        // e⁻
0041     case -11: return NeonTrackColors.Orange;     // e⁺
0042     case 211: return NeonTrackColors.Pink;       // π⁺
0043     case -211: return NeonTrackColors.Teal;      // π⁻
0044     case 2212: return NeonTrackColors.Violet;    // proton
0045     case 2112: return NeonTrackColors.Green;     // neutron
0046   }
0047   if (charge > 0) return NeonTrackColors.Red;
0048   if (charge < 0) return NeonTrackColors.DeepBlue;
0049   return NeonTrackColors.Gray;
0050 }
0051 
0052 /** Whether pid-mode draws this particle dashed (photons and neutrons). */
0053 export function pidIsDashed(pdg: number): boolean {
0054   return pdg === 22 || pdg === 2112;
0055 }
0056 
0057 /**
0058  * We'll keep each line's full data in a small structure so we can rebuild partial geometry.
0059  */
0060 interface TrajectoryRenderContext {
0061   collectionIndex: number;           // Trajectory id == index in the piece columns
0062   lineObj: Line2;                    // the Line2 object in the scene
0063   points: number[][];                // the raw array of [x, y, z, t, dx, dy, dz, dt]
0064   lineMaterial: Line2NodeMaterial;        // the material used
0065   startTime: number;                 // The time of the first point
0066   endTime: number;                   // The end of the last point
0067   params: Record<string, any>;       // Track parameters (column name -> value at this index)
0068   lastPaintIndex: number;            // This is needed for partial track draw optimization
0069 }
0070 
0071 /**
0072  * Painter that draws lines for a "PointTrajectory" piece,
0073  * supporting partial display based on time.
0074  */
0075 export class TrajectoryPainter extends EventPiecePainter {
0076 
0077   /**
0078    * Knobs: coloring mode (particle id, momentum magnitude, or one solid
0079    * color) and the line width in world units (mm) — 30 reads as a clear but
0080    * thin ribbon at detector scale; values in the hundreds cover whole
0081    * subdetectors. Live: changes restyle existing lines without a rebuild.
0082    */
0083   static meta: PainterMeta = {
0084     id: 'trajectory-lines',
0085     forPieceTypes: [PointTrajectoryPiece.type],
0086     label: 'Trajectory lines',
0087     configs: [
0088       { key: 'colorMode', default: 'pid', options: ['pid', 'momentum', 'solid'], label: 'Coloring' },
0089       { key: 'lineWidth', default: 30, min: 1, max: 300, step: 1, label: 'Line width [mm]' },
0090       { key: 'color', default: '#00b7ff', label: 'Solid color' },
0091     ],
0092   };
0093 
0094   /** A small array to store each line's data and references. */
0095   public trajectories: TrajectoryRenderContext[] = [];
0096   private timeColumnIndex = 3;         // TODO check that line has time column
0097 
0098   public readonly trackColorHighlight = 0xff4081; // vivid pink for highlight
0099   public readonly trackWidthFactor = 2;          // how many times thicker when highlighted
0100 
0101   /** Momentum ceiling [MeV] of this piece, for the momentum color scale. */
0102   private maxMomentum = 0;
0103 
0104   constructor(parentNode: Object3D, piece: EventPiece, config?: PainterConfigView) {
0105     super(parentNode, piece, config);
0106 
0107     if (piece.type !== PointTrajectoryPiece.type) {
0108       throw new Error("Wrong piece type given to TrajectoryPainter.");
0109     }
0110 
0111     // Build lines at construction
0112     this.initLines();
0113     this.applyStyles();
0114   }
0115 
0116   /**
0117    * Builds the Line2 objects for each trajectory in the piece.
0118    * Initially, we set them fully visible (or we could set them invisible).
0119    */
0120   private initLines() {
0121 
0122     const piece = this.piece as PointTrajectoryPiece;
0123     const columnNames = Object.keys(piece.columns);
0124     let noPointsWarned = 0;
0125 
0126     // [load-timing] accumulators: where construction time goes across ALL
0127     // lines of this piece (materials vs geometry vs distances vs the rest)
0128     const buildStart = performance.now();
0129     let materialMs = 0, geometryMs = 0, distancesMs = 0;
0130     let builtLines = 0;
0131     let stepStart = 0;
0132 
0133     for (let trajIndex = 0; trajIndex < piece.count; trajIndex++) {
0134 
0135       // Collect this trajectory's parameters from the columns (id == index)
0136       const paramsDict: Record<string, any> = {};
0137       for (const columnName of columnNames) {
0138         paramsDict[columnName] = piece.columns[columnName][trajIndex];
0139       }
0140 
0141       const points = piece.points[trajIndex];
0142 
0143       // Check we have enough points to build at least something!
0144       if (points.length <= 1) {
0145         if (noPointsWarned < 10) {
0146           const result = Object.entries(paramsDict)
0147             .map(([key, value]) => `${key}:${value}`)
0148             .join(", ");
0149           console.warn(`Trajectory has ${points.length} points. This can't be. Track parameters: ${result}`);
0150           noPointsWarned++;
0151         }
0152         continue;   // Skip this line!
0153       }
0154 
0155       // Create proper material
0156       stepStart = performance.now();
0157       const lineMaterial = this.createLine2NodeMaterial(paramsDict);
0158       materialMs += performance.now() - stepStart;
0159 
0160 
0161       // We'll start by building a geometry with *all* points, and rely on paint() to do partial logic.
0162       stepStart = performance.now();
0163       const geometry = new LineGeometry();
0164       const fullPositions = this.generateFlatXYZ(points);
0165       geometry.setPositions(fullPositions);
0166       geometryMs += performance.now() - stepStart;
0167 
0168 
0169       const line2 = new Line2(geometry, lineMaterial as any);
0170       stepStart = performance.now();
0171       line2.computeLineDistances();
0172       distancesMs += performance.now() - stepStart;
0173       builtLines++;
0174 
0175       // Add to the scene
0176       this.parentNode.add(line2);
0177 
0178       let startTime = 0;
0179       let endTime = 0;
0180       if (points[0].length > this.timeColumnIndex) {
0181         startTime = points[0][this.timeColumnIndex];
0182         endTime = points[points.length - 1][this.timeColumnIndex]
0183       }
0184 
0185       const trajData: TrajectoryRenderContext = {
0186         collectionIndex: trajIndex,
0187         lineObj: line2,
0188         lineMaterial: lineMaterial,
0189         points: points,
0190         startTime: startTime,
0191         endTime: endTime,
0192         params: paramsDict,
0193         lastPaintIndex: 0,
0194       }
0195 
0196       trajData.lineObj.name = this.getNodeName(trajData, piece.count);
0197       trajData.lineObj.userData["track_params"] = trajData.params;
0198 
0199       // Selection mapping: trajectory id ≡ index; the Line2 is the entity object
0200       this.registerEntityObject(trajIndex, trajData.lineObj);
0201 
0202 
0203       // Highlight captures the CURRENT material state at highlight time (not
0204       // at build time): the color/width knobs restyle materials live, and a
0205       // build-time snapshot would restore stale values.
0206       trajData.lineObj.userData["highlightFunction"] = () => {
0207         const mat = trajData.lineObj.material as unknown as Line2NodeMaterial;
0208         if (trajData.lineObj.userData["origColor"] === undefined) {
0209           trajData.lineObj.userData["origColor"] = mat.color.getHex();
0210           trajData.lineObj.userData["origWidth"] = mat.linewidth;
0211         }
0212         mat.color.setHex(this.trackColorHighlight);
0213         mat.linewidth = (trajData.lineObj.userData["origWidth"] as number) * this.trackWidthFactor;
0214         mat.needsUpdate = true;
0215       };
0216 
0217       trajData.lineObj.userData["unhighlightFunction"] = () => {
0218         // Restore original properties
0219         if (trajData.lineObj.userData["origColor"] !== undefined) {
0220           const mat = trajData.lineObj.material as unknown as Line2NodeMaterial;
0221           mat.color.setHex(trajData.lineObj.userData["origColor"]);
0222           mat.linewidth = trajData.lineObj.userData["origWidth"];
0223           mat.needsUpdate = true;
0224           delete trajData.lineObj.userData["origColor"];
0225           delete trajData.lineObj.userData["origWidth"];
0226         }
0227       };
0228 
0229       // Keep the data
0230       this.trajectories.push(trajData);
0231     }
0232 
0233     const totalMs = performance.now() - buildStart;
0234     if (totalMs > 10) {
0235       const otherMs = totalMs - materialMs - geometryMs - distancesMs;
0236       console.log(`[load-timing] TrajectoryPainter '${this.pieceName}' build ${totalMs.toFixed(1)} ms ` +
0237         `(${builtLines} lines): materials ${materialMs.toFixed(1)}, geometry ${geometryMs.toFixed(1)}, ` +
0238         `lineDistances ${distancesMs.toFixed(1)}, other ${otherMs.toFixed(1)}`);
0239     }
0240   }
0241 
0242   /**
0243    * Creates a new solid Line2NodeMaterial.
0244    * NOTE: We must create fresh materials instead of using .clone() because
0245    * Line2NodeMaterial.clone() in three.js v0.183 does NOT copy _useWorldUnits,
0246    * _useDash, or linewidth, causing all cloned materials to have 1px lines.
0247    */
0248   private newSolidMaterial(color: NeonTrackColors, linewidth?: number): Line2NodeMaterial {
0249     return new Line2NodeMaterial({
0250       color: color,
0251       linewidth: linewidth ?? this.config.value<number>('lineWidth'),
0252       worldUnits: true,
0253       dashed: false,
0254       alphaToCoverage: true,
0255     });
0256   }
0257 
0258   private newDashedMaterial(color: NeonTrackColors): Line2NodeMaterial {
0259     return new Line2NodeMaterial({
0260       color: color,
0261       linewidth: this.config.value<number>('lineWidth'),
0262       worldUnits: true,
0263       dashed: true,
0264       dashSize: 100,
0265       gapSize: 100,
0266       alphaToCoverage: true,
0267     });
0268   }
0269 
0270   /**
0271    * Creates or picks a line material based on PDG or charge, etc.
0272    * Reads the "pdg" and "charge" columns when the writer declared them.
0273    */
0274   private createLine2NodeMaterial(params: Record<string, any>) {
0275 
0276     let pdg = 0, charge = 0;
0277     if (typeof params["pdg"] === "number") {
0278       pdg = Math.floor(params["pdg"]);
0279     }
0280     if (typeof params["charge"] === "number") {
0281       charge = params["charge"];
0282     }
0283 
0284     // Minimal PDG-based color logic
0285     // ---------- PDG‑specific cases ----------
0286     switch (pdg) {
0287       case  22:   return this.newDashedMaterial(NeonTrackColors.Yellow);    // γ
0288       case -22:   return this.newSolidMaterial(NeonTrackColors.Salad, 3);  // optical photon
0289       case  11:   return this.newSolidMaterial(NeonTrackColors.Blue);      // e⁻
0290       case -11:   return this.newSolidMaterial(NeonTrackColors.Orange);    // e⁺
0291       case  211:  return this.newSolidMaterial(NeonTrackColors.Pink);      // π⁺
0292       case -211:  return this.newSolidMaterial(NeonTrackColors.Teal);      // π⁻
0293       case  2212: return this.newSolidMaterial(NeonTrackColors.Violet);    // proton
0294       case  2112: return this.newDashedMaterial(NeonTrackColors.Green);    // neutron
0295     }
0296 
0297     // ---------- Fallback by charge ----------
0298     if (charge > 0) return this.newSolidMaterial(NeonTrackColors.Red);
0299     if (charge < 0) return this.newSolidMaterial(NeonTrackColors.DeepBlue);
0300 
0301     // Neutral fallback
0302     return this.newSolidMaterial(NeonTrackColors.Gray);
0303   }
0304 
0305   /** |p| [MeV] from the px/py/pz columns, or null when the writer omitted them. */
0306   private momentumOf(params: Record<string, any>): number | null {
0307     const px = params['px'], py = params['py'], pz = params['pz'];
0308     if (typeof px !== 'number' || typeof py !== 'number' || typeof pz !== 'number') return null;
0309     return Math.sqrt(px * px + py * py + pz * pz);
0310   }
0311 
0312   /** The pid-mode color, matching the material choice in createLine2NodeMaterial. */
0313   private pidColorOf(params: Record<string, any>): number {
0314     const pdg = typeof params['pdg'] === 'number' ? Math.floor(params['pdg']) : 0;
0315     const charge = typeof params['charge'] === 'number' ? params['charge'] : 0;
0316     return pidColorFor(pdg, charge);
0317   }
0318 
0319   /** Restyle existing lines from the config knobs. Called live on knob changes. */
0320   override onConfigChanged(): void {
0321     this.applyStyles();
0322   }
0323 
0324   /**
0325    * Applies colorMode/lineWidth/color to all line materials in place —
0326    * no geometry rebuild, so knob changes are instant even for large events.
0327    */
0328   private applyStyles(): void {
0329     const colorMode = this.config.value<string>('colorMode');
0330     const lineWidth = this.config.value<number>('lineWidth');
0331     const solidColor = this.config.value<string>('color');
0332 
0333     if (colorMode === 'momentum' && this.maxMomentum === 0) {
0334       for (const track of this.trajectories) {
0335         this.maxMomentum = Math.max(this.maxMomentum, this.momentumOf(track.params) ?? 0);
0336       }
0337     }
0338 
0339     const scratch = new Color();
0340     for (const track of this.trajectories) {
0341       const material = track.lineObj.material as unknown as Line2NodeMaterial;
0342       const params = track.params;
0343 
0344       // Optical photons keep their deliberately thin lines
0345       const isOpticalPhoton = typeof params['pdg'] === 'number' && Math.floor(params['pdg']) === -22;
0346       material.linewidth = isOpticalPhoton ? Math.min(3, lineWidth) : lineWidth;
0347 
0348       if (colorMode === 'solid') {
0349         material.color.set(solidColor);
0350       } else if (colorMode === 'momentum') {
0351         const momentum = this.momentumOf(params);
0352         if (momentum === null || this.maxMomentum <= 0) {
0353           material.color.setHex(NeonTrackColors.Gray);
0354         } else {
0355           // Blue (low) -> red (high), linear in |p|
0356           const fraction = Math.min(1, momentum / this.maxMomentum);
0357           material.color.copy(scratch.setHSL(0.66 * (1 - fraction), 1, 0.5));
0358         }
0359       } else {
0360         material.color.setHex(this.pidColorOf(params));
0361       }
0362       material.needsUpdate = true;
0363 
0364       // A restyle invalidates any pending highlight-restore snapshot
0365       delete track.lineObj.userData['origColor'];
0366       delete track.lineObj.userData['origWidth'];
0367     }
0368   }
0369 
0370   /**
0371    * Helper to flatten the [x, y, z, t, ...] points into [x0, y0, z0, x1, y1, z1, ...].
0372    * We skip anything beyond the first 3 indices in each point array, because
0373    * x=0,y=1,z=2 are the first three.
0374    */
0375   private generateFlatXYZ(points: number[][]): number[] {
0376     const flat: number[] = [];
0377     for (let i = 0; i < points.length; i++) {
0378       flat.push(points[i][0], points[i][1], points[i][2]); // x,y,z
0379     }
0380     return flat;
0381   }
0382 
0383   /**
0384    * The main Paint method, called each time the user updates "time."
0385    * If time is null - timeless mode, we show the entire tracks. Otherwise, we show partial up to that time.
0386    */
0387   public override paint(time: number | null): void {
0388 
0389     if (time === null) {
0390       this.paintNoTime();
0391     } else {
0392       this.paintAtTime(time);
0393     }
0394   }
0395 
0396   private paintNoTime() {
0397     for (const track of this.trajectories) {
0398       // Show all points
0399       track.lineObj.visible = true;
0400       track.lineObj.geometry.instanceCount = track.points.length - 1;
0401     }
0402   }
0403 
0404 
0405   /**
0406    * Improved fastPaint function with proper boundary checking between time points
0407    * @param time Current simulation time
0408    */
0409   public paintAtTime(time: number): void {
0410     // First pass: categorize tracks as fully visible, partial, or hidden
0411     const partialTracks: TrajectoryRenderContext[] = [];
0412 
0413     for (const track of this.trajectories) {
0414       // Hide tracks that haven't started yet
0415       if (track.startTime > time) {
0416         track.lineObj.visible = false;
0417         track.lastPaintIndex = -1;       // if time moves forward, and we start showing track the next time
0418         continue;
0419       }
0420 
0421       // Show track
0422       track.lineObj.visible = true;
0423 
0424       // If track has already ended, show it completely
0425       if (track.endTime <= time) {
0426         track.lineObj.geometry.instanceCount = track.points.length - 1;
0427 
0428         // if next paint the time moves backward, and we start hiding track parts,
0429         // we want lastPaintIndex to correspond to fully rendered track
0430         track.lastPaintIndex = this.trajectories.length - 1;
0431         continue;
0432       }
0433 
0434       // This track is only partially visible and will be treated the next
0435       partialTracks.push(track);
0436     }
0437 
0438     // Second pass: handle partially visible tracks
0439     for (const track of partialTracks) {
0440       // Validate lastPaintIndex
0441       if (track.lastPaintIndex < 0 || track.lastPaintIndex >= track.points.length) {
0442         track.lastPaintIndex = 0;
0443       }
0444 
0445       // Find the correct interval where the current time falls
0446       // This is the key improvement: check if we need to move to next/previous point
0447       // rather than just searching forward or backward arbitrarily
0448 
0449       let needToUpdate = true;
0450       while (needToUpdate) {
0451         needToUpdate = false;
0452 
0453         // Check if we should move forward to next point
0454         if (track.lastPaintIndex < track.points.length - 1 &&
0455           time >= track.points[track.lastPaintIndex + 1][this.timeColumnIndex]) {
0456           track.lastPaintIndex++;
0457           needToUpdate = true;
0458         }
0459         // Check if we should move backward to previous point
0460         else if (track.lastPaintIndex > 0 &&
0461           time < track.points[track.lastPaintIndex][this.timeColumnIndex]) {
0462           track.lastPaintIndex--;
0463           needToUpdate = true;
0464         }
0465       }
0466 
0467       // At this point, we've found the correct index where:
0468       // time is between points[lastPaintIndex] and points[lastPaintIndex+1]
0469       // Show points up to and including lastPaintIndex
0470       track.lineObj.geometry.instanceCount = track.lastPaintIndex + 1;
0471     }
0472   }
0473 
0474   /**
0475    * Dispose all line objects, geometry, materials
0476    */
0477   public override dispose(): void {
0478     for (const ld of this.trajectories) {
0479       const geom = ld.lineObj.geometry as LineGeometry;
0480       geom.dispose();
0481       ld.lineMaterial.dispose();
0482 
0483       if (this.parentNode) {
0484         this.parentNode.remove(ld.lineObj);
0485       }
0486     }
0487     this.trajectories = [];
0488     super.dispose();
0489   }
0490 
0491   private getNodeName(trajData: TrajectoryRenderContext, trajCount: number) {
0492 
0493     // Calculate the number of digits needed (order of magnitude + 1)
0494     const padLength = Math.floor(Math.log10(trajCount)) + 1;
0495 
0496     // Use padStart to pad the string representation with leading zeros
0497     const indexStr = String(trajData.collectionIndex).padStart(padLength, ' ');
0498 
0499 
0500     let name = "track"
0501     if ("type" in trajData.params) {
0502       name = trajData.params["type"]
0503     } else if ("pdg" in trajData.params) {
0504       name = trajData.params["pdg"]
0505     } else if ("charge" in trajData.params) {
0506       const charge = parseFloat(trajData.params["charge"]);
0507       if (Math.abs(charge) < 0.00001) {
0508         name = "NeuTrk";
0509       }
0510       if (charge > 0) {
0511         name = "PosTrk";
0512       } else if (charge < 0) {
0513         name = "NegTrk";
0514       }
0515     }
0516     name = "[" + name + "]"
0517 
0518     let time = "no-t"
0519     if (Math.abs(trajData.startTime) > 0.000001 || Math.abs(trajData.endTime) > 0.000001) {
0520       time = `t:${trajData.startTime.toFixed(1)}-${trajData.endTime.toFixed(1)}`;
0521     }
0522 
0523     let momentum = "no-p"
0524     if ("px" in trajData.params && "py" in trajData.params && "pz" in trajData.params) {
0525       let px = parseFloat(trajData.params["px"]);
0526       let py = parseFloat(trajData.params["py"]);
0527       let pz = parseFloat(trajData.params["pz"]);
0528       momentum = "p:" + (Math.sqrt(px * px + py * py + pz * pz) / 1000.0).toFixed(3);
0529     }
0530 
0531     return `${indexStr} ${trajData.collectionIndex} ${name} ${momentum} ${time}`;
0532   }
0533 }