Warning, /firebird/packages/firebird-ng/display/src/lib/services/scene-helpers.service.ts is written in an unsupported language. File is not indexed.
0001 import { Injectable } from '@angular/core';
0002 import * as THREE from 'three';
0003 import { ThreeService } from './three.service';
0004
0005 /**
0006 * Keeps references to "helper" objects and toggles or moves them:
0007 * - Axis lines
0008 * - EtaPhi grids
0009 * - Cartesian grids
0010 * - 3D points picking
0011 * Every setter that changes what the scene shows schedules a render
0012 * (`ThreeService.invalidate()`): the on-demand render loop draws nothing
0013 * new without one.
0014 */
0015 @Injectable({ providedIn: 'root' })
0016 export class SceneHelpersService {
0017 private cartesianGridGroup: THREE.Group | null = null;
0018 private etaPhiGroup: THREE.Group | null = null;
0019 private labelsGroup: THREE.Group | null = null;
0020
0021 // For "show distance" or "show 3D coords" logic:
0022 private onPointerMoveFn?: (event: MouseEvent) => void;
0023 private onPointerClickFn?: (event: MouseEvent) => void;
0024
0025 constructor(private threeService: ThreeService) {}
0026
0027 /**
0028 * Toggles the existing axes helper created in ThreeService.
0029 */
0030 setShowAxis(show: boolean) {
0031 if (this.threeService.axesHelper) {
0032 this.threeService.axesHelper.visible = show;
0033 this.threeService.invalidate();
0034 }
0035 }
0036
0037 /**
0038 * Show/hide a cartesian grid on the XZ plane at Y = -4000.
0039 * Grid squares are 1 meter (1000 units) on a side.
0040 */
0041 setShowCartesianGrid(show: boolean) {
0042 if (!this.cartesianGridGroup) {
0043 const gridSize = 10000; // total extent in units (10 m)
0044 const divisions = gridSize / 1000; // 1 m per square
0045 this.cartesianGridGroup = new THREE.Group();
0046 this.cartesianGridGroup.name = 'CartesianGrid';
0047 // GridHelper creates a grid on the XZ plane by default
0048 const grid = new THREE.GridHelper(gridSize, divisions, 0xffffff, 0x888888);
0049 grid.material = new THREE.LineBasicMaterial({ color: 0x888888, transparent: true, opacity: 0.4 });
0050 this.cartesianGridGroup.add(grid);
0051 // Position at Y = -4000
0052 this.cartesianGridGroup.position.set(0, -4000, 0);
0053 this.threeService.sceneHelpers.add(this.cartesianGridGroup);
0054 }
0055 this.cartesianGridGroup.visible = show;
0056 this.threeService.invalidate();
0057 }
0058
0059 /**
0060 * Show/hide eta lines with labels for common HEP pseudorapidity values.
0061 * Eta lines are drawn as cones emanating from the origin in the XZ plane (beam axis = Z).
0062 */
0063 setShowEtaPhiGrid(show: boolean) {
0064 if (!this.etaPhiGroup) {
0065 this.etaPhiGroup = new THREE.Group();
0066 this.etaPhiGroup.name = 'EtaPhi';
0067 this.buildEtaLines(this.etaPhiGroup);
0068 this.threeService.sceneHelpers.add(this.etaPhiGroup);
0069 }
0070 this.etaPhiGroup.visible = show;
0071 this.threeService.invalidate();
0072 }
0073
0074 /**
0075 * Build lines for common eta values.
0076 * Pseudorapidity eta = -ln(tan(theta/2)), so theta = 2*atan(exp(-eta)).
0077 * In HEP convention: beam axis = Z, so a particle at angle theta from Z
0078 * travels in direction (sin(theta), 0, cos(theta)).
0079 */
0080 private buildEtaLines(group: THREE.Group) {
0081 // Common eta values for HEP collider detectors
0082 const etaValues = [-4, -3, -2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2, 3, 4];
0083 const lineLength = 4000; // mm, extent of lines
0084
0085 const material = new THREE.LineBasicMaterial({
0086 color: 0xffcc00,
0087 transparent: true,
0088 opacity: 0.6,
0089 });
0090
0091 for (const eta of etaValues) {
0092 const theta = 2 * Math.atan(Math.exp(-eta));
0093 // Direction in (x, y, z) with beam along Z:
0094 // x = sin(theta), y = 0, z = cos(theta)
0095 const sinTheta = Math.sin(theta);
0096 const cosTheta = Math.cos(theta);
0097
0098 const points = [
0099 new THREE.Vector3(0, 0, 0),
0100 new THREE.Vector3(sinTheta * lineLength, 0, cosTheta * lineLength),
0101 ];
0102 const geometry = new THREE.BufferGeometry().setFromPoints(points);
0103 const line = new THREE.Line(geometry, material);
0104 line.name = `eta_${eta}`;
0105 group.add(line);
0106
0107 // Add label sprite at the end of the line
0108 const label = this.createTextSprite(`η=${eta}`, 0xffcc00);
0109 label.position.set(
0110 sinTheta * (lineLength + 200),
0111 0,
0112 cosTheta * (lineLength + 200)
0113 );
0114 label.name = `eta_label_${eta}`;
0115 group.add(label);
0116 }
0117 }
0118
0119 /**
0120 * Show/hide X, Y, Z labels at the end of the axes helper.
0121 */
0122 showLabels(show: boolean) {
0123 if (!this.labelsGroup) {
0124 this.labelsGroup = new THREE.Group();
0125 this.labelsGroup.name = 'AxisLabels';
0126
0127 const axisLength = 1500; // matches AxesHelper size in three.service
0128 const offset = 150; // offset past the axis end
0129
0130 const xLabel = this.createTextSprite('X', 0xff0000);
0131 xLabel.position.set(axisLength + offset, 0, 0);
0132 this.labelsGroup.add(xLabel);
0133
0134 const yLabel = this.createTextSprite('Y', 0x00ff00);
0135 yLabel.position.set(0, axisLength + offset, 0);
0136 this.labelsGroup.add(yLabel);
0137
0138 const zLabel = this.createTextSprite('Z', 0x0000ff);
0139 zLabel.position.set(0, 0, axisLength + offset);
0140 this.labelsGroup.add(zLabel);
0141
0142 this.threeService.sceneHelpers.add(this.labelsGroup);
0143 }
0144 this.labelsGroup.visible = show;
0145 this.threeService.invalidate();
0146 }
0147
0148 /**
0149 * Creates a sprite with text rendered on a canvas texture.
0150 */
0151 private createTextSprite(text: string, color: number): THREE.Sprite {
0152 const canvas = document.createElement('canvas');
0153 const size = 256;
0154 canvas.width = size;
0155 canvas.height = size;
0156 const ctx = canvas.getContext('2d')!;
0157
0158 ctx.clearRect(0, 0, size, size);
0159
0160 // Convert hex color to CSS string
0161 const cssColor = `#${color.toString(16).padStart(6, '0')}`;
0162
0163 ctx.font = 'Bold 120px Arial';
0164 ctx.textAlign = 'center';
0165 ctx.textBaseline = 'middle';
0166 ctx.fillStyle = cssColor;
0167 ctx.fillText(text, size / 2, size / 2);
0168
0169 const texture = new THREE.CanvasTexture(canvas);
0170 texture.needsUpdate = true;
0171
0172 const spriteMaterial = new THREE.SpriteMaterial({
0173 map: texture,
0174 transparent: true,
0175 depthTest: false,
0176 });
0177 const sprite = new THREE.Sprite(spriteMaterial);
0178 sprite.scale.set(200, 200, 1);
0179 sprite.name = `label_${text}`;
0180 return sprite;
0181 }
0182
0183 /**
0184 * Show/hide 3D mouse coordinates by hooking pointer events, running a Raycaster, etc.
0185 */
0186 show3DMousePoints(show: boolean) {
0187 if (show) {
0188 this.onPointerClickFn = (evt) => this.handle3DPointClick(evt);
0189 window.addEventListener('click', this.onPointerClickFn);
0190 } else {
0191 if (this.onPointerClickFn) {
0192 window.removeEventListener('click', this.onPointerClickFn);
0193 this.onPointerClickFn = undefined;
0194 }
0195 }
0196 }
0197
0198 private handle3DPointClick(evt: MouseEvent) {
0199 const rect = this.threeService.renderer.domElement.getBoundingClientRect();
0200 const x = ((evt.clientX - rect.left) / rect.width) * 2 - 1;
0201 const y = -((evt.clientY - rect.top) / rect.height) * 2 + 1;
0202 const raycaster = new THREE.Raycaster();
0203 raycaster.setFromCamera(new THREE.Vector2(x, y), this.threeService.camera);
0204
0205 const intersects = raycaster.intersectObjects(this.threeService.scene.children, true);
0206 if (intersects.length > 0) {
0207 const point = intersects[0].point;
0208 console.log('Clicked 3D coords:', point);
0209 }
0210 }
0211
0212 /**
0213 * Toggle "show 3D distance" by hooking pointer events for measuring two points, etc.
0214 */
0215 show3DDistance(show: boolean) {
0216 console.warn('show3DDistance not implemented yet.');
0217 }
0218
0219 /**
0220 * SHIFT cartesian grid by pointer or by values.
0221 */
0222 shiftCartesianGridByPointer() {
0223 console.warn('shiftCartesianGridByPointer not implemented.');
0224 }
0225
0226 translateCartesianGrid(shift: THREE.Vector3) {
0227 if (this.cartesianGridGroup) {
0228 this.cartesianGridGroup.position.add(shift);
0229 this.threeService.invalidate();
0230 }
0231 }
0232
0233 translateCartesianLabels(shift: THREE.Vector3) {
0234 // If you keep labels in a separate group or do 2D overlay, handle that here
0235 }
0236
0237 setCameraView(targetPos: THREE.Vector3, cameraPos: THREE.Vector3) {
0238 this.threeService.camera.position.copy(cameraPos);
0239 this.threeService.controls.target.copy(targetPos);
0240 this.threeService.controls.update();
0241 this.threeService.invalidate();
0242 }
0243 }