Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-core/src/painters/batched-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 import {NeonTrackColors, pidColorFor, pidIsDashed} from "./trajectory.painter";
0005 
0006 import {Color, Object3D, Intersection} from "three";
0007 // Import from three/webgpu, NEVER from three/src/... — see trajectory.painter.ts
0008 import {Line2NodeMaterial} from "three/webgpu";
0009 import {LineSegmentsGeometry} from "three/examples/jsm/lines/LineSegmentsGeometry.js";
0010 import {LineSegments2} from "three/examples/jsm/lines/webgpu/LineSegments2.js";
0011 
0012 /**
0013  * One draw call worth of segments: every trajectory of the piece that shares
0014  * a dash style lives in a single LineSegments2. Segments are sorted by their
0015  * START time, so revealing the event up to time t is one instanceCount write
0016  * (the same segment granularity the per-track painter reveals with).
0017  */
0018 interface SegmentBatch {
0019   object: LineSegments2;
0020   geometry: LineSegmentsGeometry;
0021   material: Line2NodeMaterial;
0022   /** Total segments in the batch. */
0023   count: number;
0024   /** Per segment, ascending: the segment's start-point time. */
0025   startTimes: Float32Array;
0026   /** Per segment (sorted order): the trajectory id it belongs to. */
0027   trackIds: Uint32Array;
0028   /** Per segment (sorted order): 6 floats, start rgb + end rgb. */
0029   colors: Float32Array;
0030   dashed: boolean;
0031 }
0032 
0033 /**
0034  * Alternative painter for "PointTrajectory" pieces that batches ALL
0035  * trajectories into two objects (solid + dashed) instead of one Line2 per
0036  * trajectory.
0037  *
0038  * Why: the per-track painter costs one scene object + one material per
0039  * trajectory. Measured on a 53k-trajectory background frame that is ~0.8 s of
0040  * construction and, far worse, >10 s PER FRAME of draw submission — the
0041  * renderer, not the build, is what freezes the browser. Batched, the same
0042  * piece renders in 2 draw calls with 2 materials.
0043  *
0044  * Select it per piece through the normal painter selection config:
0045  * `painters.byPiece.<pieceName> = trajectory-lines-batched` (panel dropdown,
0046  * yaml, or URL `?config.painters.byPiece.MCParticles=trajectory-lines-batched`)
0047  * — the per-track painter stays registered, so the two are switchable live
0048  * for comparison.
0049  *
0050  * Batching trade-offs vs the per-track painter:
0051  * - per-track coloring is kept (per-segment color attribute; material color
0052  *   stays white because vertex colors MULTIPLY it);
0053  * - line width and dash pattern are per MATERIAL, so all tracks of a batch
0054  *   share them (optical photons lose their thinner-line special case);
0055  * - highlight recolors the track's segments (no width change);
0056  * - 3D picking cannot use one-object-per-entity stamps — the batch object
0057  *   carries an `entityIndexResolver` that maps the picked segment
0058  *   (intersection.faceIndex) back to the trajectory id.
0059  */
0060 export class BatchedTrajectoryPainter extends EventPiecePainter {
0061 
0062   static meta: PainterMeta = {
0063     id: 'trajectory-lines-batched',
0064     forPieceTypes: [PointTrajectoryPiece.type],
0065     label: 'Trajectory lines (batched)',
0066     configs: [
0067       { key: 'colorMode', default: 'pid', options: ['pid', 'momentum', 'solid'], label: 'Coloring' },
0068       { key: 'lineWidth', default: 30, min: 1, max: 300, step: 1, label: 'Line width [mm]' },
0069       { key: 'color', default: '#00b7ff', label: 'Solid color' },
0070     ],
0071   };
0072 
0073   public readonly trackColorHighlight = 0xff4081;
0074 
0075   private batches: SegmentBatch[] = [];
0076   private timeIndex = -1;
0077   /** Momentum ceiling of the piece, for the momentum color scale. */
0078   private maxMomentum = 0;
0079 
0080   constructor(parentNode: Object3D, piece: EventPiece, config?: PainterConfigView) {
0081     super(parentNode, piece, config);
0082     if (piece.type !== PointTrajectoryPiece.type) {
0083       throw new Error("Wrong piece type given to BatchedTrajectoryPainter.");
0084     }
0085     this.timeIndex = (piece as PointTrajectoryPiece).pointColumns.indexOf('t');
0086     this.buildBatches();
0087     this.applyStyles();
0088   }
0089 
0090   private get trajectoryPiece(): PointTrajectoryPiece {
0091     return this.piece as PointTrajectoryPiece;
0092   }
0093 
0094   /** Whether this track renders in the dashed batch (fixed at build, like the per-track painter). */
0095   private trackIsDashed(trackIndex: number): boolean {
0096     const pdg = this.trackParamNumber('pdg', trackIndex);
0097     return pidIsDashed(pdg === null ? 0 : Math.floor(pdg));
0098   }
0099 
0100   private trackParamNumber(column: string, trackIndex: number): number | null {
0101     const value = this.trajectoryPiece.param(column, trackIndex);
0102     return typeof value === 'number' ? value : null;
0103   }
0104 
0105   private buildBatches(): void {
0106     const piece = this.trajectoryPiece;
0107     const buildStart = performance.now();
0108 
0109     // Pass 1: count segments per dash style
0110     const counts = [0, 0]; // [solid, dashed]
0111     const dashedOf = new Uint8Array(piece.count);
0112     for (let trackIndex = 0; trackIndex < piece.count; trackIndex++) {
0113       const points = piece.points[trackIndex];
0114       if (points.length <= 1) continue;
0115       const dashed = this.trackIsDashed(trackIndex) ? 1 : 0;
0116       dashedOf[trackIndex] = dashed;
0117       counts[dashed] += points.length - 1;
0118     }
0119 
0120     for (const dashed of [0, 1]) {
0121       const count = counts[dashed];
0122       if (count === 0) continue;
0123 
0124       // Pass 2: collect (time, trackId, pointIndex) per segment, unsorted
0125       const times = new Float32Array(count);
0126       const segTrack = new Uint32Array(count);
0127       const segPoint = new Uint32Array(count);
0128       let fill = 0;
0129       for (let trackIndex = 0; trackIndex < piece.count; trackIndex++) {
0130         if (dashedOf[trackIndex] !== dashed) continue;
0131         const points = piece.points[trackIndex];
0132         if (points.length <= 1) continue;
0133         for (let j = 0; j < points.length - 1; j++) {
0134           times[fill] = this.timeIndex >= 0 ? points[j][this.timeIndex] : 0;
0135           segTrack[fill] = trackIndex;
0136           segPoint[fill] = j;
0137           fill++;
0138         }
0139       }
0140 
0141       // Sort segments by start time so a time cut is one instanceCount write
0142       const order = new Uint32Array(count);
0143       for (let i = 0; i < count; i++) order[i] = i;
0144       if (this.timeIndex >= 0) {
0145         // Typed-array sort with a comparator; ~O(n log n) on segment count
0146         order.sort((a, b) => times[a] - times[b]);
0147       }
0148 
0149       const positions = new Float32Array(count * 6);
0150       const startTimes = new Float32Array(count);
0151       const trackIds = new Uint32Array(count);
0152       for (let i = 0; i < count; i++) {
0153         const source = order[i];
0154         const trackIndex = segTrack[source];
0155         const points = piece.points[trackIndex];
0156         const start = points[segPoint[source]];
0157         const end = points[segPoint[source] + 1];
0158         const base = i * 6;
0159         positions[base] = start[0];
0160         positions[base + 1] = start[1];
0161         positions[base + 2] = start[2];
0162         positions[base + 3] = end[0];
0163         positions[base + 4] = end[1];
0164         positions[base + 5] = end[2];
0165         startTimes[i] = times[source];
0166         trackIds[i] = trackIndex;
0167       }
0168 
0169       const geometry = new LineSegmentsGeometry();
0170       geometry.setPositions(positions);
0171       const colors = new Float32Array(count * 6);
0172       geometry.setColors(colors); // filled by applyStyles
0173 
0174       // Vertex colors MULTIPLY the material color — it must stay white
0175       const material = new Line2NodeMaterial({
0176         color: 0xffffff,
0177         vertexColors: true,
0178         linewidth: this.config.value<number>('lineWidth'),
0179         worldUnits: true,
0180         dashed: dashed === 1,
0181         dashSize: 100,
0182         gapSize: 100,
0183         alphaToCoverage: true,
0184       });
0185 
0186       const object = new LineSegments2(geometry, material as any);
0187       if (dashed) object.computeLineDistances();
0188       object.name = `${this.pieceName}_batched_${dashed ? 'dashed' : 'solid'}`;
0189 
0190       const batch: SegmentBatch = {
0191         object,
0192         geometry,
0193         material,
0194         count,
0195         startTimes,
0196         trackIds,
0197         colors,
0198         dashed: dashed === 1,
0199       };
0200 
0201       // 3D picking: one object holds every track, so the entity is resolved
0202       // from the picked segment index, not from the object itself
0203       object.userData['pieceName'] = this.pieceName;
0204       object.userData['entityIndexResolver'] = (intersection: Intersection) => {
0205         const segment = intersection.faceIndex;
0206         return typeof segment === 'number' && segment < batch.count ? batch.trackIds[segment] : null;
0207       };
0208 
0209       this.parentNode.add(object);
0210       this.batches.push(batch);
0211     }
0212 
0213     const buildMs = performance.now() - buildStart;
0214     if (buildMs > 10) {
0215       const total = counts[0] + counts[1];
0216       console.log(`[load-timing] BatchedTrajectoryPainter '${this.pieceName}' build ` +
0217         `${buildMs.toFixed(1)} ms (${total} segments in ${this.batches.length} batches)`);
0218     }
0219   }
0220 
0221   /** The rgb of one track under the current color mode. */
0222   private trackColor(trackIndex: number, colorMode: string, solidColor: string, scratch: Color): Color {
0223     if (colorMode === 'solid') {
0224       return scratch.set(solidColor);
0225     }
0226     if (colorMode === 'momentum') {
0227       const px = this.trackParamNumber('px', trackIndex);
0228       const py = this.trackParamNumber('py', trackIndex);
0229       const pz = this.trackParamNumber('pz', trackIndex);
0230       if (px === null || py === null || pz === null || this.maxMomentum <= 0) {
0231         return scratch.setHex(NeonTrackColors.Gray);
0232       }
0233       const fraction = Math.min(1, Math.sqrt(px * px + py * py + pz * pz) / this.maxMomentum);
0234       // Blue (low) -> red (high), linear in |p|, same scale as the per-track painter
0235       return scratch.setHSL(0.66 * (1 - fraction), 1, 0.5);
0236     }
0237     const pdg = this.trackParamNumber('pdg', trackIndex);
0238     const charge = this.trackParamNumber('charge', trackIndex) ?? 0;
0239     return scratch.setHex(pidColorFor(pdg === null ? 0 : Math.floor(pdg), charge));
0240   }
0241 
0242   /** Writes one track's color into every one of its segments in one batch. */
0243   private colorTrackSegments(batch: SegmentBatch, trackIndex: number, color: Color): void {
0244     const {trackIds, colors} = batch;
0245     for (let i = 0; i < batch.count; i++) {
0246       if (trackIds[i] !== trackIndex) continue;
0247       const base = i * 6;
0248       colors[base] = colors[base + 3] = color.r;
0249       colors[base + 1] = colors[base + 4] = color.g;
0250       colors[base + 2] = colors[base + 5] = color.b;
0251     }
0252   }
0253 
0254   /** Pushes a batch's `colors` array into its geometry attribute. */
0255   private uploadColors(batch: SegmentBatch): void {
0256     const attribute = batch.geometry.attributes['instanceColorStart'];
0257     if (attribute && 'data' in attribute) {
0258       (attribute as unknown as { data: { array: Float32Array; needsUpdate: boolean } })
0259         .data.array.set(batch.colors);
0260       (attribute as unknown as { data: { needsUpdate: boolean } }).data.needsUpdate = true;
0261     } else {
0262       batch.geometry.setColors(batch.colors);
0263     }
0264   }
0265 
0266   override onConfigChanged(): void {
0267     this.applyStyles();
0268   }
0269 
0270   /** Recolors every segment and applies the shared width; drops any highlight. */
0271   private applyStyles(): void {
0272     const colorMode = this.config.value<string>('colorMode');
0273     const lineWidth = this.config.value<number>('lineWidth');
0274     const solidColor = this.config.value<string>('color');
0275     const piece = this.trajectoryPiece;
0276 
0277     if (colorMode === 'momentum' && this.maxMomentum === 0) {
0278       for (let trackIndex = 0; trackIndex < piece.count; trackIndex++) {
0279         const px = this.trackParamNumber('px', trackIndex) ?? 0;
0280         const py = this.trackParamNumber('py', trackIndex) ?? 0;
0281         const pz = this.trackParamNumber('pz', trackIndex) ?? 0;
0282         this.maxMomentum = Math.max(this.maxMomentum, Math.sqrt(px * px + py * py + pz * pz));
0283       }
0284     }
0285 
0286     // Per-track colors once, then per-segment fill through the sorted order
0287     const scratch = new Color();
0288     const trackRgb = new Float32Array(piece.count * 3);
0289     for (let trackIndex = 0; trackIndex < piece.count; trackIndex++) {
0290       const color = this.trackColor(trackIndex, colorMode, solidColor, scratch);
0291       trackRgb[trackIndex * 3] = color.r;
0292       trackRgb[trackIndex * 3 + 1] = color.g;
0293       trackRgb[trackIndex * 3 + 2] = color.b;
0294     }
0295 
0296     for (const batch of this.batches) {
0297       for (let i = 0; i < batch.count; i++) {
0298         const track = batch.trackIds[i] * 3;
0299         const base = i * 6;
0300         batch.colors[base] = batch.colors[base + 3] = trackRgb[track];
0301         batch.colors[base + 1] = batch.colors[base + 4] = trackRgb[track + 1];
0302         batch.colors[base + 2] = batch.colors[base + 5] = trackRgb[track + 2];
0303       }
0304       this.uploadColors(batch);
0305       batch.material.linewidth = lineWidth;
0306       batch.material.needsUpdate = true;
0307     }
0308     // A restyle repaints highlighted tracks in their normal color, same as
0309     // the per-track painter.
0310   }
0311 
0312   /**
0313    * Draws one track in the highlight color. Highlights are independent:
0314    * other highlighted tracks stay highlighted (the selection and the
0315    * hovered track show together).
0316    */
0317   override highlightEntity(entityIndex: number): void {
0318     const scratch = new Color().setHex(this.trackColorHighlight);
0319     for (const batch of this.batches) {
0320       this.colorTrackSegments(batch, entityIndex, scratch);
0321       this.uploadColors(batch);
0322     }
0323   }
0324 
0325   override unhighlightEntity(entityIndex: number): void {
0326     const scratch = new Color();
0327     const color = this.trackColor(
0328       entityIndex,
0329       this.config.value<string>('colorMode'),
0330       this.config.value<string>('color'),
0331       scratch,
0332     );
0333     for (const batch of this.batches) {
0334       this.colorTrackSegments(batch, entityIndex, color);
0335       this.uploadColors(batch);
0336     }
0337   }
0338 
0339   /**
0340    * Time cut: segments are pre-sorted by start time, so painting at time t is
0341    * a binary search + one instanceCount write per batch — the same segment
0342    * granularity the per-track painter reveals with (a segment shows once its
0343    * start point's time has passed).
0344    */
0345   public override paint(time: number | null): void {
0346     for (const batch of this.batches) {
0347       if (time === null || this.timeIndex < 0) {
0348         batch.geometry.instanceCount = batch.count;
0349         continue;
0350       }
0351       // Upper bound: first index with startTime > time
0352       let low = 0, high = batch.count;
0353       while (low < high) {
0354         const mid = (low + high) >> 1;
0355         if (batch.startTimes[mid] <= time) low = mid + 1;
0356         else high = mid;
0357       }
0358       batch.geometry.instanceCount = low;
0359     }
0360   }
0361 
0362   public override dispose(): void {
0363     for (const batch of this.batches) {
0364       batch.geometry.dispose();
0365       batch.material.dispose();
0366       this.parentNode?.remove(batch.object);
0367     }
0368     this.batches = [];
0369     super.dispose();
0370   }
0371 }