Warning, /firebird/firebird-ng/src/app/painters/point-trajectory.painter.ts is written in an unsupported language. File is not indexed.
0001 import { ComponentPainter } from "./component-painter";
0002 import { EntryComponent } from "../model/entry-component";
0003 import {
0004 PointTrajectoryComponent,
0005 TrackerLineSegment
0006 } from "../model/point-trajectory.event-component";
0007
0008 import { Color, Object3D } from "three";
0009 import { LineMaterial } from "three/examples/jsm/lines/LineMaterial";
0010 import { LineGeometry } from "three/examples/jsm/lines/LineGeometry";
0011 import { Line2 } from "three/examples/jsm/lines/Line2";
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 InternalLineData {
0032 lineObj: Line2; // the Line2 object in the scene
0033 points: number[][]; // the raw array of [x, y, z, t, dx, dy, dz, dt]
0034 lineMaterial: LineMaterial; // the material used
0035 startTime: number; // The time of the first point
0036 endTime: number; // The end of the last point
0037 params: Record<string, any>; // Track parameters
0038 lastPaintIndex: number; // This is needed for partial track draw optimization
0039 }
0040
0041 /**
0042 * Painter that draws lines for a "TrackerLinePointTrajectoryComponent",
0043 * supporting partial display based on time.
0044 */
0045 export class PointTrajectoryPainter extends ComponentPainter {
0046 /** A small array to store each line's data and references. */
0047 private trajectories: InternalLineData[] = [];
0048 private timeColumnIndex = 3; // TODO check that line has time column
0049
0050 /** Base materials that we clone for each line. */
0051 private baseSolidMaterial: LineMaterial;
0052 private baseDashedMaterial: LineMaterial;
0053
0054 constructor(parentNode: Object3D, component: EntryComponent) {
0055 super(parentNode, component);
0056
0057 if (component.type !== PointTrajectoryComponent.type) {
0058 throw new Error("Wrong component type given to TrackerLinePointTrajectoryPainter.");
0059 }
0060
0061 // Create base materials
0062 this.baseSolidMaterial = new LineMaterial({
0063 color: 0xffffff,
0064 linewidth: 10, // in world units
0065 worldUnits: true,
0066 dashed: false,
0067 alphaToCoverage: true
0068 });
0069
0070 this.baseDashedMaterial = new LineMaterial({
0071 color: 0xffffff,
0072 linewidth: 10,
0073 worldUnits: true,
0074 dashed: true,
0075 dashSize: 100,
0076 gapSize: 100,
0077 alphaToCoverage: true
0078 });
0079
0080 // Build lines at construction
0081 this.initLines();
0082 }
0083
0084 /**
0085 * Builds the Line2 objects for each line in the data.
0086 * Initially, we set them fully visible (or we could set them invisible).
0087 */
0088 private initLines() {
0089 const component = this.component as PointTrajectoryComponent;
0090
0091 // Let us see if paramColumns includes "pdg" or "charge" or something.
0092 const pdgIndex = component.paramColumns.indexOf("pdg");
0093 const chargeIndex = component.paramColumns.indexOf("charge");
0094 let paramsToColumnsMismatchWarned = false;
0095 let noPointsWarned = 0;
0096
0097 for (const lineSegment of component.lines) {
0098
0099 // Copy params
0100 const paramColumns = component.paramColumns;
0101 const params = lineSegment.params;
0102 if(params.length != paramColumns.length && !paramsToColumnsMismatchWarned) {
0103 // We do the warning only once!
0104 console.error(`params.length(${params.length}) != paramColumns.length(${paramColumns.length}) at '${component.name}'. This should never happen!`);
0105 paramsToColumnsMismatchWarned = true;
0106 }
0107
0108 // We intentionally use the very dumb method but this method allows us do at least something if they mismatch
0109 const paramArrLen = Math.min(paramColumns.length, params.length);
0110 const paramsDict: Record<string, any> = {};
0111 for (let i = 0; i < paramArrLen; i++) {
0112 paramsDict[paramColumns[i]] = params[i];
0113 }
0114
0115 // Check we have enough points to build at least something!
0116 if(lineSegment.points.length <= 1) {
0117 if(noPointsWarned < 10) {
0118 const result = Object.entries(paramsDict)
0119 .map(([key, value]) => `${key}:${value}`)
0120 .join(", ");
0121 console.warn(`Line has ${lineSegment.points.length} points. This can't be. Track parameters: ${result}`);
0122 noPointsWarned++;
0123 }
0124 continue; // Skip this line!
0125 }
0126
0127 // Create proper material
0128 const { lineMaterial } = this.createLineMaterial(lineSegment, pdgIndex, chargeIndex);
0129
0130 // We'll start by building a geometry with *all* points, and rely on paint() to do partial logic.
0131 // We'll store the full set of points in linesData, then paint() can rebuild partial geometry.
0132 const geometry = new LineGeometry();
0133 const fullPositions = this.generateFlatXYZ(lineSegment.points);
0134 geometry.setPositions(fullPositions);
0135
0136 const line2 = new Line2(geometry, lineMaterial);
0137 line2.computeLineDistances();
0138
0139 // Add to the scene
0140 this.parentNode.add(line2);
0141
0142 let startTime = 0;
0143 let endTime = 0;
0144 if(lineSegment.points[0].length > this.timeColumnIndex) {
0145 startTime = lineSegment.points[0][this.timeColumnIndex];
0146 endTime = lineSegment.points[lineSegment.points.length-1][this.timeColumnIndex]
0147 }
0148
0149 const trajData: InternalLineData = {
0150 lineObj: line2,
0151 lineMaterial: lineMaterial,
0152 points: lineSegment.points,
0153 startTime: startTime,
0154 endTime: endTime,
0155 params: paramsDict,
0156 lastPaintIndex: 0,
0157 }
0158
0159 trajData.lineObj.name = this.getNodeName(trajData);
0160 trajData.lineObj.userData["track_params"] = trajData.params;
0161
0162 // Keep the data
0163 this.trajectories.push(trajData);
0164
0165 }
0166 }
0167
0168 /**
0169 * Creates or picks a line material based on PDG or charge, etc.
0170 */
0171 private createLineMaterial(line: TrackerLineSegment, pdgIndex: number, chargeIndex: number) {
0172 let colorVal = NeonTrackColors.Gray;
0173 let dashed = false;
0174
0175 // Try to read PDG and/or charge from line.params
0176 // This assumes line.params matches paramColumns.
0177 let pdg = 0, charge = 0;
0178 if (pdgIndex >= 0 && pdgIndex < line.params.length) {
0179 pdg = Math.floor(line.params[pdgIndex]);
0180 }
0181 if (chargeIndex >= 0 && chargeIndex < line.params.length) {
0182 charge = line.params[chargeIndex];
0183 }
0184
0185 // Minimal PDG-based color logic
0186 switch (pdg) {
0187 case 22: // gamma
0188 colorVal = NeonTrackColors.Yellow;
0189 dashed = true;
0190 break;
0191 case 11: // e-
0192 colorVal = NeonTrackColors.Blue;
0193 dashed = false;
0194 break;
0195 case -11: // e+
0196 colorVal = NeonTrackColors.Red;
0197 dashed = false;
0198 break;
0199 case 211: // pi+
0200 colorVal = NeonTrackColors.Pink;
0201 dashed = false;
0202 break;
0203 case -211: // pi-
0204 colorVal = NeonTrackColors.Teal;
0205 dashed = false;
0206 break;
0207 case 2212: // proton
0208 colorVal = NeonTrackColors.Violet;
0209 dashed = false;
0210 break;
0211 case 2112: // neutron
0212 colorVal = NeonTrackColors.Green;
0213 dashed = true;
0214 break;
0215 default:
0216 // fallback by charge
0217 if (charge > 0) colorVal = NeonTrackColors.Red;
0218 else if (charge < 0) colorVal = NeonTrackColors.DeepBlue;
0219 else colorVal = NeonTrackColors.Gray;
0220 break;
0221 }
0222
0223 // clone base material
0224 const mat = dashed ? this.baseDashedMaterial.clone() : this.baseSolidMaterial.clone();
0225 mat.color = new Color(colorVal);
0226
0227 return { lineMaterial: mat, dashed };
0228 }
0229
0230 /**
0231 * Helper to flatten the [x, y, z, t, ...] points into [x0, y0, z0, x1, y1, z1, ...].
0232 * We skip anything beyond the first 3 indices in each point array, because
0233 * x=0,y=1,z=2 are the first three.
0234 */
0235 private generateFlatXYZ(points: number[][]): number[] {
0236 const flat: number[] = [];
0237 for (let i = 0; i < points.length; i++) {
0238 flat.push(points[i][0], points[i][1], points[i][2]); // x,y,z
0239 }
0240 return flat;
0241 }
0242
0243 /**
0244 * Rebuild partial geometry for a line up to time `t`.
0245 * If the user wants interpolation, we do that for the "one extra" point beyond t.
0246 * Otherwise, we just up to the last point with time <= t.
0247 */
0248 private buildPartialXYZ(points: number[][], t: number): number[] {
0249 const flat: number[] = [];
0250
0251 // We assume each "points[i]" = [x, y, z, time, dx, dy, dz, dt].
0252 // The time is at index 3 if it exists.
0253 const TIME_INDEX = 3;
0254 if (!points.length) return flat;
0255
0256 let lastGoodIndex = -1;
0257 for (let i = 0; i < points.length; i++) {
0258 const p = points[i];
0259 if (p.length > TIME_INDEX) {
0260 if (p[TIME_INDEX] <= t) {
0261 // This entire point is included
0262 flat.push(p[0], p[1], p[2]);
0263 lastGoodIndex = i;
0264 } else {
0265 // we've found a point beyond t, let's see if we want to interpolate
0266 if (lastGoodIndex >= 0) {
0267 // Interpolate between points[lastGoodIndex] and points[i]
0268 const p0 = points[lastGoodIndex];
0269 const t0 = p0[TIME_INDEX];
0270 const t1 = p[TIME_INDEX];
0271 if (Math.abs(t1 - t0) > 1e-9) {
0272 const frac = (t - t0) / (t1 - t0);
0273 const xInterp = p0[0] + frac * (p[0] - p0[0]);
0274 const yInterp = p0[1] + frac * (p[1] - p0[1]);
0275 const zInterp = p0[2] + frac * (p[2] - p0[2]);
0276 flat.push(xInterp, yInterp, zInterp);
0277 } else {
0278 // times are effectively the same
0279 flat.push(p0[0], p0[1], p0[2]);
0280 }
0281 }
0282 break; // stop scanning
0283 }
0284 } else {
0285 // If there's no time column for that point, let's assume 0 or treat as instant
0286 // For simplicity, let's treat time as 0. So if t>0, we include it.
0287 flat.push(p[0], p[1], p[2]);
0288 lastGoodIndex = i;
0289 }
0290 }
0291 return flat;
0292 }
0293
0294 /**
0295 * The main painting method, called each time the user updates "time."
0296 * If time is null, we show the entire track. Otherwise, we show partial up to that time.
0297 */
0298 public override paint(time: number | null): void {
0299 // If time===null => show all lines fully
0300 if (time === null) {
0301 this.paintNoTime();
0302 return;
0303 }
0304
0305 // Otherwise, partial or none
0306 this.fastPaint(time);
0307 }
0308
0309 private paintNoTime() {
0310 for (const track of this.trajectories) {
0311 // Rebuild geometry with *all* points
0312 track.lineObj.visible = true;
0313 track.lineObj.geometry.instanceCount=Infinity;
0314 }
0315 }
0316
0317 public paintAtTime(time:number) {
0318 for (const ld of this.trajectories) {
0319 // Rebuild geometry up to time
0320 const partialPositions = this.buildPartialXYZ(ld.points, time);
0321 if (partialPositions.length < 2 * 3) {
0322 // fewer than 2 points => hide
0323 ld.lineObj.visible = false;
0324 continue;
0325 }
0326 ld.lineObj.visible = true;
0327
0328 // Dispose old geometry
0329 const geom = ld.lineObj.geometry as LineGeometry;
0330 geom.dispose();
0331
0332 // Set new geometry
0333 geom.setPositions(partialPositions);
0334 ld.lineObj.computeLineDistances();
0335 }
0336 }
0337
0338 public fastPaint(time: number) {
0339
0340 // pass1 select fully visible, partial and fully hidden tracks
0341
0342 let partialTracks: InternalLineData[] = []; // Replace 'any' with the actual type
0343
0344 for (const track of this.trajectories) {
0345 if (track.startTime > time) {
0346 track.lineObj.visible = false;
0347 } else {
0348 track.lineObj.visible = true;
0349 track.lineObj.geometry.instanceCount = track.points.length;
0350
0351 if (track.endTime > time) {
0352 partialTracks.push(track);
0353 } else {
0354 // track should be visible fully
0355 track.lineObj.geometry.instanceCount = Infinity;
0356 }
0357 }
0358 }
0359
0360 if (partialTracks.length > 0) {
0361 for (let track of partialTracks) {
0362 let geometryPosCount = track.points.length;
0363
0364 //if (!geometryPosCount || geometryPosCount < 10) continue;
0365 //let trackProgress = (time - track.startTime) / (track.endTime - track.startTime);
0366 //let roundedProgress = Math.round(geometryPosCount * trackProgress * 2) / 2; // *2/2 to stick to 0.5 rounding
0367
0368 if(track.lastPaintIndex<0 || track.lastPaintIndex>=track.points.length) {
0369 // In case of emergency set lastPointIndex to the center of array
0370 track.lastPaintIndex=track.points.length/2;
0371 }
0372
0373 if(track.points[track.lastPaintIndex][this.timeColumnIndex] < time) {
0374 // Seek the correct point of time forward
0375 while(track.points[track.lastPaintIndex][this.timeColumnIndex] < time && track.lastPaintIndex<track.points.length) {
0376 track.lastPaintIndex++;
0377 }
0378 }
0379 else {
0380 // Seek the correct point of time backward
0381 while(track.points[track.lastPaintIndex][this.timeColumnIndex] > time && track.lastPaintIndex>=0) {
0382 track.lastPaintIndex--;
0383 }
0384 }
0385
0386 track.lineObj.geometry.instanceCount = track.lastPaintIndex;
0387 }
0388 }
0389
0390 }
0391
0392 /**
0393 * Dispose all line objects, geometry, materials
0394 */
0395 public override dispose(): void {
0396 for (const ld of this.trajectories) {
0397 const geom = ld.lineObj.geometry as LineGeometry;
0398 geom.dispose();
0399 ld.lineMaterial.dispose();
0400
0401 if (this.parentNode) {
0402 this.parentNode.remove(ld.lineObj);
0403 }
0404 }
0405 this.trajectories = [];
0406 super.dispose();
0407 }
0408
0409 private getNodeName(trajData: InternalLineData) {
0410
0411 let name = "track"
0412 if("type" in trajData.params) {
0413 name = trajData.params["type"]
0414 } else if ("pdg" in trajData.params) {
0415 name = trajData.params["pdg"]
0416 } else if ("charge" in trajData.params) {
0417 const charge = parseFloat(trajData.params["cahrge"]);
0418 if(Math.abs(charge) < 0.00001) {
0419 name = "NeuTrk";
0420 }
0421 if(charge>0) {
0422 name = "PosTrk";
0423 } else if (charge < 0) {
0424 name = "NegTrk";
0425 }
0426 }
0427 name="["+name+"]"
0428
0429 let time = "no-t"
0430 if(Math.abs(trajData.startTime) > 0.000001 || Math.abs(trajData.endTime) > 0.000001) {
0431 time = `t:${trajData.startTime.toFixed(1)}-${trajData.endTime.toFixed(1)}`;
0432 }
0433
0434 let momentum = "no-p"
0435 if("px" in trajData.params && "py" in trajData.params && "pz" in trajData.params) {
0436 let px = parseFloat(trajData.params["px"]);
0437 let py = parseFloat(trajData.params["py"]);
0438 let pz = parseFloat(trajData.params["pz"]);
0439 momentum = "p:"+(Math.sqrt(px*px+py*py+pz*pz)/1000.0).toFixed(3);
0440 }
0441
0442 return `${name} ${momentum} ${time}`;
0443 }
0444 }