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 }