Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/firebird-ng/src/app/services/three.service.ts is written in an unsupported language. File is not indexed.

0001 import { Injectable, NgZone, OnDestroy} from '@angular/core';
0002 import * as THREE from 'three';
0003 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
0004 import {
0005   HemisphereLight,
0006   DirectionalLight,
0007   AmbientLight,
0008   PointLight,
0009   SpotLight,
0010   Frustum,
0011   Matrix4,
0012   Camera,
0013   Scene, Mesh
0014 } from 'three';
0015 import {PerfService} from "./perf.service";
0016 import {BehaviorSubject, Subject} from "rxjs";
0017 
0018 
0019 import {
0020   acceleratedRaycast,
0021   computeBoundsTree,
0022   disposeBoundsTree, MeshBVH, MeshBVHHelper
0023 } from 'three-mesh-bvh';
0024 
0025 
0026 
0027 @Injectable({
0028   providedIn: 'root',
0029 })
0030 export class ThreeService implements OnDestroy {
0031 
0032 
0033   /** Three.js core components */
0034   public scene!: THREE.Scene;
0035   public sceneGeometry!: THREE.Group;
0036   public sceneEvent!: THREE.Group;
0037   public sceneHelpers!: THREE.Group;
0038   public renderer!: THREE.WebGLRenderer;
0039   public controls!: OrbitControls;
0040 
0041 
0042   /** Cameras */
0043   public perspectiveCamera!: THREE.PerspectiveCamera;
0044   public orthographicCamera!: THREE.OrthographicCamera;
0045 
0046   /** Camera that is actually used */
0047   public camera!: THREE.PerspectiveCamera | THREE.OrthographicCamera;
0048   public cameraMode$ = new BehaviorSubject<boolean>(true);
0049 
0050   /** Optional clipping planes and logic. */
0051   public clipPlanes = [
0052     new THREE.Plane(new THREE.Vector3(0, -1, 0), 0),
0053     new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),
0054   ];
0055 
0056   /** Functions callbacks that help organize performance */
0057   public profileBeginFunc: (() => void) | null = null;
0058   public profileEndFunc: (() => void) | null = null;
0059 
0060 
0061 
0062 
0063   /** Animation loop control */
0064   private animationFrameId: number | null = null;
0065   private shouldRender = false;
0066 
0067   /** Callbacks to run each frame before rendering. */
0068   private frameCallbacks: Array<() => void> = [];
0069 
0070   private clipIntersection: boolean = false;
0071 
0072   /** Initialization flag */
0073   private initialized: boolean = false;
0074 
0075   /** Reference to the container element used for rendering */
0076   private containerElement!: HTMLElement;
0077 
0078   /** Lights */
0079   private ambientLight!: AmbientLight;
0080   private hemisphereLight!: HemisphereLight;
0081   private directionalLight!: DirectionalLight;
0082   private pointLight!: PointLight; // Optional
0083   private spotLight!: SpotLight; // Optional
0084 
0085   /** BVH wizard */
0086   private boundsViz!: MeshBVHHelper;
0087 
0088    // Raycasting properties
0089   private raycaster = new THREE.Raycaster();
0090   private pointer = new THREE.Vector2();
0091   private isRaycastEnabled = false;
0092 
0093 
0094   // Track hover indicator
0095   // private hoverPoint: THREE.Mesh | null = null;
0096 
0097   // Track highlighted object for raycast feedback
0098   private highlightedObject: THREE.Object3D | null = null;
0099   private originalMaterials = new Map<THREE.Object3D, THREE.Material | THREE.Material[]>();
0100 
0101   // Events
0102   public trackHovered = new Subject<{track: THREE.Object3D, point: THREE.Vector3}>();
0103   public trackClicked = new Subject<{track: THREE.Object3D, point: THREE.Vector3}>();
0104 
0105   // Raw hit point every frame (hover)
0106   public pointHovered = new Subject<THREE.Vector3>();
0107 
0108   // Distance ready after second point
0109   public distanceReady = new Subject<{ p1: THREE.Vector3; p2: THREE.Vector3; dist: number }>();
0110 
0111   // Toggle by UI when “3‑D Distance” checkbox is on
0112   public measureMode = false;
0113 
0114   // temp storage for first measure point
0115   private firstMeasurePoint: THREE.Vector3 | null = null;
0116 
0117   //  Add properties for event handlers and measurement state
0118   private pointerMoveHandler?: (event: PointerEvent) => void;
0119   private pointerDownHandler?: (event: PointerEvent) => void;
0120   private doubleClickHandler?: (event: MouseEvent) => void;
0121   private hoverTimeout: number | null = null;
0122   private measurementPoints: THREE.Mesh[] = [];
0123 
0124   private frustumCuller = {
0125     frustum: new Frustum(),
0126     projScreenMatrix: new Matrix4(),
0127 
0128     updateFrustum(camera: Camera): void {
0129       this.projScreenMatrix.multiplyMatrices(
0130         camera.projectionMatrix,
0131         camera.matrixWorldInverse
0132       );
0133       this.frustum.setFromProjectionMatrix(this.projScreenMatrix);
0134     },
0135 
0136     cullMeshes(scene: Scene, camera: Camera): void {
0137       this.updateFrustum(camera);
0138 
0139       scene.traverse((object:any) => {
0140         if (object!=null && (object as any).isMesh) {
0141           // First check if object has bounds
0142           if (!object.geometry.boundingBox) {
0143             object.geometry.computeBoundingBox();
0144           }
0145 
0146           // For objects with BVH
0147           if (object.geometry.boundsTree) {
0148             // Use BVH for efficient culling
0149             const visible = object.geometry.boundsTree.shapecast({
0150               intersectsBounds: (box: THREE.Box3) => {
0151                 return this.frustum.intersectsBox(box);
0152               }
0153             });
0154             console.log("Shapecast!");
0155             object.visible = visible ?? true;
0156           } else {
0157             // Fallback to standard bounding box check
0158             const box = new THREE.Box3().setFromObject(object);
0159             object.visible = this.frustum.intersectsBox(box);
0160           }
0161         }
0162       });
0163     }
0164   };
0165 
0166 
0167   constructor(
0168     private ngZone: NgZone,
0169     private perfService: PerfService) {
0170     // Empty constructor – initialization happens in init()
0171 
0172      // Apply mesh-bvh acceleration to improve raycasting performance
0173     THREE.Mesh.prototype.raycast = acceleratedRaycast;
0174     THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
0175     THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
0176   }
0177 
0178   /**
0179    * Initializes the Three.js scene, camera, renderer, controls, and lights.
0180    * Must be called before any other method.
0181    * @param container A string representing the ID of the HTML element,
0182    *                  or the actual HTMLElement where the renderer will attach.
0183    */
0184   init(container: string | HTMLElement): void {
0185 
0186     let containerElement: HTMLElement;
0187 
0188     // Figure out the container
0189     if (typeof container === 'string') {
0190       const el = document.getElementById(container);
0191       if (!el) {
0192         throw new Error(`ThreeService Initialization Error: Container element #${container} not found.`);
0193       }
0194       containerElement = el;
0195     } else {
0196       containerElement = container;
0197     }
0198 
0199     // If already initialized once, warn but still re-attach the canvas.
0200     if (this.initialized) {
0201       console.warn('ThreeService has already been initialized. Re-attaching renderer...');
0202       this.attachRenderer(containerElement);
0203       return;
0204     }
0205 
0206     this.containerElement = containerElement;
0207 
0208     // 1) Create scene
0209     this.scene = new THREE.Scene();
0210     this.scene.background = new THREE.Color(0x3f3f3f); // Dark grey background
0211 
0212     // Geometry scene tree
0213     this.sceneGeometry = new THREE.Group();
0214     this.sceneGeometry.name = 'Geometry';
0215     this.scene.add(this.sceneGeometry);
0216 
0217     // Event scene tree
0218     this.sceneEvent = new THREE.Group();
0219     this.sceneEvent.name = 'Event';
0220     this.scene.add(this.sceneEvent);
0221 
0222     // Lights scene tree
0223     this.sceneHelpers = new THREE.Group();
0224     this.sceneHelpers.name = 'Helpers';
0225     this.scene.add(this.sceneHelpers);
0226 
0227     // BVH helper
0228     // this.boundsViz = new MeshBVHHelper( knots[ 0 ] );
0229     // containerObj.add( boundsViz );
0230 
0231     // Create cameras
0232     this.perspectiveCamera = new THREE.PerspectiveCamera(60, 1, 10, 40000);
0233     this.perspectiveCamera.position.set(-7000, 0 , 0);
0234 
0235 
0236 
0237     // Better orthographic camera initialization
0238     const orthoSize = 1000; // Start with a large enough size to see the detector
0239     this.orthographicCamera = new THREE.OrthographicCamera(
0240       -orthoSize, orthoSize,
0241       orthoSize, -orthoSize,
0242       -10000, 40000 // Critical change: Allow negative near plane to see objects behind camera position
0243     );
0244     this.orthographicCamera.position.copy(this.perspectiveCamera.position);
0245     this.orthographicCamera.lookAt(this.scene.position);
0246 
0247     // Default camera is perspective
0248     this.camera = this.perspectiveCamera;
0249 
0250     // Create renderer
0251     this.renderer = new THREE.WebGLRenderer({ antialias: true });
0252     this.renderer.setPixelRatio(window.devicePixelRatio);
0253     this.renderer.localClippingEnabled = false;
0254     this.renderer.shadowMap.enabled = true;
0255     this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
0256 
0257     // Append renderer to the container
0258     this.containerElement.appendChild(this.renderer.domElement);
0259 
0260     // Create OrbitControls
0261     this.controls = new OrbitControls(this.camera, this.renderer.domElement);
0262     this.controls.target.set(0, 0, 0);
0263     this.controls.enableDamping = false;
0264     this.controls.dampingFactor = 0.05;
0265 
0266     // Perspective camera distance limits
0267     const sceneRadius = 15000;
0268     this.controls.minDistance = sceneRadius * 0.05;
0269     this.controls.maxDistance = sceneRadius * 5;
0270     this.camera.far = this.controls.maxDistance * 1.1;
0271     this.camera.updateProjectionMatrix();
0272 
0273     this.controls.update();
0274 
0275     // Setup lights
0276     this.setupLights();
0277 
0278     // Add default objects
0279     this.addDefaultObjects();
0280 
0281     // (!) We set initialized here, as at this point all main objects are created and configured
0282     // It is important not to set this flag at the function end as functions, such as setSize will check the flag
0283     this.initialized = true;
0284 
0285     // ----------- POST INIT ------------------
0286 
0287     // Set initial size
0288     const width = this.containerElement.clientWidth;
0289     const height = this.containerElement.clientHeight;
0290     this.setSize(width, height);
0291 
0292 
0293     // Initialize the hover point
0294     // this.initHoverPoint();
0295 
0296     // Set up new raycasting handlers
0297     this.setupRaycasting();
0298 
0299     // Compute BVH for all existing meshes for fast raycasting
0300     this.setupBVH();
0301 
0302     // Start rendering
0303     this.startRendering();
0304   }
0305 
0306   /**
0307    * If the service is already initialized (scene, camera, renderer exist),
0308    * you can re-attach the <canvas> to a container if it was removed or changed.
0309    */
0310   private attachRenderer(elem: HTMLElement): void {
0311     this.containerElement = elem;
0312 
0313     // If the canvas is not already in the DOM, re-append it.
0314     if (this.renderer?.domElement) {
0315       this.containerElement.appendChild(this.renderer.domElement);
0316     }
0317   }
0318 
0319   /**
0320    * When You Do Want to Recreate the Entire Scene
0321    * If sometimes you genuinely need to start fresh (e.g. user changed geometry drastically)
0322    */
0323   public reset(): void {
0324     this.stopRendering();
0325     // remove old scene from memory
0326     // e.g. dispose geometries, empty the scene, etc.
0327     this.renderer.domElement.parentNode?.removeChild(this.renderer.domElement);
0328 
0329     this.initialized = false;
0330     // next time `init` is called, it will do full creation again.
0331   }
0332 
0333 
0334   /**
0335    * Sets up the lighting for the scene.
0336    */
0337   private setupLights(): void {
0338     this.ambientLight = new AmbientLight(0xffffff, 0.4);
0339     this.ambientLight.name = "Light-Ambient";
0340     this.sceneHelpers.add(this.ambientLight);
0341 
0342     this.hemisphereLight = new HemisphereLight(0xffffff, 0x444444, 0.6);
0343     this.hemisphereLight.position.set(0, 200, 0);
0344     this.hemisphereLight.name = "Light-Hemisphere";
0345     this.sceneHelpers.add(this.hemisphereLight);
0346 
0347     this.directionalLight = new DirectionalLight(0xffffff, 0.8);
0348     this.directionalLight.position.set(100, 200, 100);
0349     this.directionalLight.name = "Light-Directional";
0350     this.directionalLight.castShadow = true;
0351     this.directionalLight.shadow.mapSize.width = 512;
0352     this.directionalLight.shadow.mapSize.height = 512;
0353     this.directionalLight.shadow.camera.near = 0.5;
0354     this.directionalLight.shadow.camera.far = 1000;
0355     this.sceneHelpers.add(this.directionalLight);
0356 
0357     this.pointLight = new PointLight(0xffffff, 0.5, 500);
0358     this.pointLight.position.set(-100, 100, -100);
0359     this.pointLight.castShadow = true;
0360     this.pointLight.name = "Light-Point";
0361     this.sceneHelpers.add(this.pointLight);
0362 
0363     this.spotLight = new SpotLight(0xffffff, 0.5);
0364     this.spotLight.position.set(0, 300, 0);
0365     this.spotLight.angle = Math.PI / 6;
0366     this.spotLight.penumbra = 0.2;
0367     this.spotLight.decay = 2;
0368     this.spotLight.distance = 1000;
0369     this.spotLight.castShadow = true;
0370     this.spotLight.name = "Light-Spot";
0371     this.sceneHelpers.add(this.spotLight);
0372   }
0373 
0374   /**
0375    * Adds default objects to the scene.
0376    */
0377   private addDefaultObjects(): void {
0378     // const gridHelper = new THREE.GridHelper(1000, 100);
0379     // gridHelper.name = "Grid";
0380     // this.sceneHelpers.add(gridHelper);
0381 
0382     const axesHelper = new THREE.AxesHelper(1500);
0383     axesHelper.name = "Axes";
0384     this.sceneHelpers.add(axesHelper);
0385 
0386     // const geometry = new THREE.BoxGeometry(100, 100, 100);
0387     // const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
0388     // const cube = new THREE.Mesh(geometry, material);
0389     // cube.name = "TestCube"
0390     // cube.castShadow = true;
0391     // cube.receiveShadow = true;
0392     // this.sceneGeometry.add(cube);
0393   }
0394 
0395   /**
0396    * Starts the rendering loop.
0397    */
0398   startRendering(): void {
0399     this.ensureInitialized('startRendering');
0400 
0401     if (this.animationFrameId !== null) {
0402       console.warn('[ThreeService]: Rendering loop is already running.');
0403       return;
0404     }
0405 
0406     this.shouldRender = true;
0407     this.ngZone.runOutsideAngular(() => {
0408       this.renderLoop();
0409     });
0410   }
0411 
0412   /**
0413    * Stops the rendering loop.
0414    */
0415   stopRendering(): void {
0416     this.shouldRender = false;
0417     if (this.animationFrameId !== null) {
0418       cancelAnimationFrame(this.animationFrameId);
0419       this.animationFrameId = null;
0420     }
0421   }
0422 
0423   /**
0424    * The render loop: updates controls, executes frame callbacks, renders the scene, and schedules the next frame.
0425    */
0426   private renderLoop(): void {
0427     if (!this.shouldRender) {
0428       return;
0429     }
0430 
0431     this.animationFrameId = requestAnimationFrame(() => this.renderLoop());
0432 
0433     try {
0434       const frameStartTime = performance.now();  // Add this
0435       // Profiling start
0436       this.profileBeginFunc?.();
0437 
0438 
0439       // Add frustum culling before rendering
0440       // this.frustumCuller.cullMeshes(this.scene, this.camera);
0441 
0442       // Update three components
0443       this.controls.update();
0444       this.renderer.render(this.scene, this.camera);
0445       // Profiling end
0446       this.perfService.updateStats(this.renderer, frameStartTime);
0447 
0448 
0449       // Run all custom/users callbacks
0450       for (const cb of this.frameCallbacks) {
0451         cb();
0452       }
0453 
0454       this.profileEndFunc?.();
0455     } catch (error) {
0456       console.error('(!!!) ThreeService Render Loop Error:', error);
0457       this.stopRendering();
0458     }
0459   }
0460 
0461   /**
0462    * Adds a callback to be executed each frame before rendering.
0463    * Prevents duplicate callbacks.
0464    * @param callback Function to execute each frame.
0465    */
0466   addFrameCallback(callback: () => void): void {
0467     if (!this.frameCallbacks.includes(callback)) {
0468       this.frameCallbacks.push(callback);
0469     } else {
0470       console.warn('ThreeService: Attempted to add a duplicate frame callback.');
0471     }
0472   }
0473 
0474   /**
0475    * Removes a previously added frame callback.
0476    * @param callback The callback function to remove.
0477    */
0478   removeFrameCallback(callback: () => void): void {
0479     const index = this.frameCallbacks.indexOf(callback);
0480     if (index !== -1) {
0481       this.frameCallbacks.splice(index, 1);
0482     } else {
0483       console.warn('ThreeService: Attempted to remove a non-existent frame callback.');
0484     }
0485   }
0486 
0487   /**
0488    * Sets the size of the renderer and updates the camera projections.
0489    * @param width The new width in pixels.
0490    * @param height The new height in pixels.
0491    */
0492   setSize(width: number, height: number): void {
0493     if (!this.initialized) {
0494       console.error('ThreeService: setSize called before initialization.');
0495       return;
0496     }
0497     this.renderer.setSize(width, height);
0498 
0499     this.perspectiveCamera.aspect = width / height;
0500     this.perspectiveCamera.updateProjectionMatrix();
0501 
0502     this.orthographicCamera.left = width / -2;
0503     this.orthographicCamera.right = width / 2;
0504     this.orthographicCamera.top = height / 2;
0505     this.orthographicCamera.bottom = height / -2;
0506     this.orthographicCamera.updateProjectionMatrix();
0507 
0508     this.controls.update();
0509   }
0510 
0511   /**
0512    * Enables or disables local clipping.
0513    * @param enable Whether clipping should be enabled.
0514    */
0515   enableClipping(enable: boolean): void {
0516     this.renderer.localClippingEnabled = enable;
0517 
0518     // Update all materials to use clipping planes when enabled
0519     if (enable) {
0520       this.updateMaterialClipping();
0521     }
0522   }
0523 
0524   /**
0525    * Sets two-plane clipping by rotating the clipping planes.
0526    * @param startAngleDeg The starting angle in degrees.
0527    * @param openingAngleDeg The opening angle in degrees.
0528    */
0529   setClippingAngle(startAngleDeg: number, openingAngleDeg: number): void {
0530     const planeA = this.clipPlanes[0];
0531     const planeB = this.clipPlanes[1];
0532 
0533     this.clipIntersection = openingAngleDeg < 180;
0534     const startAngle = (startAngleDeg * Math.PI) / 180;
0535     const openingAngle = (openingAngleDeg * Math.PI) / 180;
0536 
0537     const quatA = new THREE.Quaternion();
0538     quatA.setFromAxisAngle(new THREE.Vector3(0, 0, 1), startAngle);
0539     planeA.normal.set(0, -1, 0).applyQuaternion(quatA);
0540 
0541     const quatB = new THREE.Quaternion();
0542     quatB.setFromAxisAngle(new THREE.Vector3(0, 0, 1), startAngle + openingAngle);
0543     planeB.normal.set(0, 1, 0).applyQuaternion(quatB);
0544 
0545     // Enable clipping and update materials
0546     this.renderer.localClippingEnabled = true;
0547     this.updateMaterialClipping();
0548   }
0549 
0550   /**
0551    * Update all materials to use current clipping planes
0552    */
0553   private updateMaterialClipping(): void {
0554     const updateObjectClipping = (object: THREE.Object3D) => {
0555       if (object instanceof THREE.Mesh && object.material) {
0556         const materials = Array.isArray(object.material) ? object.material : [object.material];
0557 
0558         materials.forEach((material: THREE.Material) => {
0559           if (this.renderer.localClippingEnabled) {
0560             material.clippingPlanes = this.clipPlanes;
0561             material.clipIntersection = this.clipIntersection;
0562           } else {
0563             material.clippingPlanes = null;
0564             material.clipIntersection = false;
0565           }
0566 
0567           // Prevent z-fighting
0568           if (material instanceof THREE.MeshBasicMaterial ||
0569             material instanceof THREE.MeshLambertMaterial ||
0570             material instanceof THREE.MeshPhongMaterial ||
0571             material instanceof THREE.MeshStandardMaterial) {
0572             material.polygonOffset = true;
0573             material.polygonOffsetFactor = 1;
0574             material.polygonOffsetUnits = 1;
0575           }
0576 
0577           material.needsUpdate = true;
0578         });
0579       }
0580     };
0581 
0582     this.sceneGeometry.traverse(updateObjectClipping);
0583     this.sceneEvent.traverse(updateObjectClipping);
0584   }
0585 
0586   /**
0587    * Toggles between perspective and orthographic cameras.
0588    * @param useOrtho Whether to use the orthographic camera.
0589    */
0590   toggleOrthographicView(useOrtho: boolean): void {
0591 
0592     if (useOrtho) {
0593       // When switching to orthographic, sync position and target from perspective
0594       this.orthographicCamera.position.copy(this.perspectiveCamera.position);
0595 
0596       // Get the current target from OrbitControls
0597       const target = this.controls.target.clone();
0598 
0599 
0600       // Update orthographic camera to look in the same direction
0601       this.orthographicCamera.lookAt(target);
0602 
0603       // Calculate suitable frustum size based on distance to target
0604       const distance = this.orthographicCamera.position.distanceTo(target);
0605       const orthoSize = distance * Math.tan(THREE.MathUtils.degToRad(this.perspectiveCamera.fov / 2));
0606 
0607       // Update orthographic frustum based on aspect ratio
0608       const aspect = this.renderer.domElement.width / this.renderer.domElement.height;
0609       this.orthographicCamera.left = -orthoSize * aspect;
0610       this.orthographicCamera.right = orthoSize * aspect;
0611       this.orthographicCamera.top = orthoSize;
0612       this.orthographicCamera.bottom = -orthoSize;
0613 
0614       // Set a generous near/far plane range to ensure all geometry is visible
0615       this.orthographicCamera.near = -10000;
0616       this.orthographicCamera.far = 40000;
0617 
0618       this.orthographicCamera.updateProjectionMatrix();
0619       this.camera = this.orthographicCamera;
0620     } else {
0621       // Switch back to perspective camera
0622       this.camera = this.perspectiveCamera;
0623     }
0624 
0625     // Update the controls to use the current camera
0626     this.controls.object = this.camera;
0627     this.controls.update();
0628 
0629     this.cameraMode$.next(!useOrtho);
0630   }
0631 
0632   /**
0633    * Ensures the service has been initialized before performing operations.
0634    * @param methodName The name of the method performing the check.
0635    */
0636   private ensureInitialized(methodName: string): void {
0637     if (!this.initialized) {
0638       const errorMsg = `ThreeService Error: Method '${methodName}' called before initialization. Call 'init(container)' first.`;
0639       console.error(errorMsg);
0640       throw new Error(errorMsg);
0641     }
0642   }
0643 
0644   /**
0645    * Cleans up resources when the service is destroyed.
0646    */
0647   ngOnDestroy(): void {
0648     this.clearHighlight();
0649     this.stopRendering();
0650     this.cleanupEventListeners();
0651     if(this.sceneGeometry) this.cleanupBVH(this.sceneGeometry);
0652     if(this.sceneEvent) this.cleanupBVH(this.sceneEvent);
0653   }
0654 
0655 
0656   logRendererInfo() {
0657     // Access the THREE.WebGLRenderer from threeService
0658     const renderer = this.renderer;
0659     const info = renderer.info;
0660     console.log('Draw calls:', info.render.calls);
0661     console.log('Triangles:', info.render.triangles);
0662     console.log('Points:', info.render.points);
0663     console.log('Lines:', info.render.lines);
0664     console.log('Geometries in memory:', info.memory.geometries);
0665     console.log('Textures in memory:', info.memory.textures);
0666     console.log('Programs:', info.programs?.length);
0667     console.log(info.programs);
0668   }
0669   // /**
0670   //  * Initialize the hover point indicator
0671   //  */
0672   // private initHoverPoint(): void {
0673   //   const sphereGeom = new THREE.SphereGeometry(6, 16, 16);
0674   //   const sphereMat = new THREE.MeshBasicMaterial({
0675   //     color: 0xff0000,
0676   //     transparent: true,
0677   //     opacity: 0.8,
0678   //     depthTest: false,
0679   //     depthWrite: false
0680   //   });
0681   //
0682   //   this.hoverPoint = new THREE.Mesh(sphereGeom, sphereMat);
0683   //   this.hoverPoint.visible = false;
0684   //   this.hoverPoint.name = "HoverPoint";
0685   //   this.hoverPoint.renderOrder = 999;
0686   //   this.sceneHelpers.add(this.hoverPoint);
0687   // }
0688 
0689   /**
0690    * Highlight an object by making its material brighter
0691    */
0692   private highlightObject(object: THREE.Object3D): void {
0693     if (this.highlightedObject === object) return;
0694 
0695     // Clear previous highlight
0696     this.clearHighlight();
0697 
0698     if (object instanceof THREE.Mesh && object.material) {
0699       this.highlightedObject = object;
0700 
0701       // Store original material(s)
0702       this.originalMaterials.set(object, object.material);
0703 
0704       // Create highlighted version
0705       const materials = Array.isArray(object.material) ? object.material : [object.material];
0706       const highlightedMaterials = materials.map(mat => {
0707         const highlightMat = mat.clone();
0708 
0709         // Make material brighter by increasing emissive
0710         if ('emissive' in highlightMat) {
0711           highlightMat.emissive.setHex(0x444444); // Add subtle glow
0712         }
0713 
0714         // Increase overall brightness for materials that support it
0715         if ('color' in highlightMat && highlightMat.color) {
0716           highlightMat.color.multiplyScalar(1.5); // Make 50% brighter
0717         }
0718 
0719         highlightMat.needsUpdate = true;
0720         return highlightMat;
0721       });
0722 
0723       object.material = Array.isArray(object.material) ? highlightedMaterials : highlightedMaterials[0];
0724     }
0725   }
0726 
0727   /**
0728    * Clear the current highlight
0729    */
0730   private clearHighlight(): void {
0731     if (this.highlightedObject && this.originalMaterials.has(this.highlightedObject)) {
0732       const original = this.originalMaterials.get(this.highlightedObject);
0733       if (this.highlightedObject instanceof THREE.Mesh && original) {
0734         this.highlightedObject.material = original;
0735       }
0736       this.originalMaterials.delete(this.highlightedObject);
0737       this.highlightedObject = null;
0738     }
0739   }
0740 
0741   /**
0742    * Helper to check if a point is clipped by active clipping planes
0743    */
0744   private isPointClipped(point: THREE.Vector3): boolean {
0745     if (!this.renderer.localClippingEnabled || this.clipPlanes.length === 0) {
0746       return false;
0747     }
0748 
0749     for (const plane of this.clipPlanes) {
0750       const distance = plane.distanceToPoint(point);
0751       if (distance < 0) {
0752         return true;
0753       }
0754     }
0755     return false;
0756   }
0757 
0758   /**
0759    * Filter intersections based on clipping planes
0760    */
0761   private filterClippedIntersections(intersections: THREE.Intersection[]): THREE.Intersection[] {
0762     if (!this.renderer.localClippingEnabled || this.clipPlanes.length === 0) {
0763       return intersections;
0764     }
0765 
0766     return intersections.filter(intersection => !this.isPointClipped(intersection.point));
0767   }
0768 
0769 
0770   /**
0771    * Sets up the raycasting functionality with proper clipping support
0772    */
0773   private setupRaycasting(): void {
0774     const buildBVHIfNeeded = (obj: THREE.Object3D) => {
0775       if (obj instanceof THREE.Mesh && obj.geometry && !obj.geometry.boundsTree) {
0776         // @ts-ignore
0777         obj.geometry.computeBoundsTree?.();
0778       }
0779     };
0780 
0781     //  Throttled hover handling to improve performance
0782     const onPointerMove = (event: PointerEvent) => {
0783       if (!this.isRaycastEnabled || this.measureMode) {
0784         this.clearHighlight();
0785         return;
0786       }
0787 
0788       //  Throttle hover events
0789       if (this.hoverTimeout) return;
0790 
0791       this.hoverTimeout = window.setTimeout(() => {
0792         this.hoverTimeout = null;
0793 
0794         // Use proper canvas dimensions for coordinate calculation
0795         const canvas = this.renderer.domElement;
0796         const rect = canvas.getBoundingClientRect();
0797 
0798         this.pointer.x = ((event.clientX - rect.left) / canvas.clientWidth) * 2 - 1;
0799         this.pointer.y = -((event.clientY - rect.top) / canvas.clientHeight) * 2 + 1;
0800 
0801         this.raycaster.setFromCamera(this.pointer, this.camera);
0802         this.raycaster.firstHitOnly = false;
0803         this.raycaster.near = this.camera.near;
0804         this.raycaster.far = this.camera.far;
0805 
0806         this.sceneEvent.traverse(buildBVHIfNeeded);
0807         this.sceneGeometry.traverse(buildBVHIfNeeded);
0808 
0809         let intersection: THREE.Intersection | null = null;
0810 
0811         // Try sceneEvent first
0812         const hitsEvt = this.raycaster.intersectObjects(this.sceneEvent.children, true);
0813         const filteredEvtHits = this.filterClippedIntersections(hitsEvt);
0814 
0815         if (filteredEvtHits.length > 0) {
0816           intersection = filteredEvtHits[0];
0817         } else {
0818           // Fall back to geometry
0819           const hitsGeo = this.raycaster.intersectObjects(this.sceneGeometry.children, true);
0820           const filteredGeoHits = this.filterClippedIntersections(hitsGeo);
0821 
0822           if (filteredGeoHits.length > 0) {
0823             intersection = filteredGeoHits[0];
0824           }
0825         }
0826 
0827         if (intersection && intersection.object.name &&
0828           !intersection.object.name.includes('Helper') &&
0829           !intersection.object.name.startsWith('MeasurePoint_') &&
0830           intersection.object.visible) {
0831 
0832           this.highlightObject(intersection.object);
0833           this.trackHovered.next({ track: intersection.object, point: intersection.point.clone() });
0834           console.log('[raycast] HOVER', intersection.object.name, intersection.point);
0835 
0836           this.ngZone.run(() => {
0837             this.pointHovered.next(intersection.point.clone());
0838           });
0839         } else {
0840           this.clearHighlight();
0841         }
0842       }, 16); // ~60fps throttling
0843     };
0844 
0845     //  Single click for selection only (no measurement, no preventDefault in selection mode)
0846     const onPointerDown = (event: PointerEvent) => {
0847       if (!this.isRaycastEnabled || this.measureMode) return;
0848 
0849       // Only handle left mouse button
0850       if (event.button !== 0) return;
0851 
0852       //  Don't prevent default for selection to allow OrbitControls
0853       // event.preventDefault(); // REMOVED
0854 
0855       const canvas = this.renderer.domElement;
0856       const rect = canvas.getBoundingClientRect();
0857 
0858       this.pointer.x = ((event.clientX - rect.left) / canvas.clientWidth) * 2 - 1;
0859       this.pointer.y = -((event.clientY - rect.top) / canvas.clientHeight) * 2 + 1;
0860 
0861       this.raycaster.setFromCamera(this.pointer, this.camera);
0862       this.raycaster.firstHitOnly = false;
0863 
0864       // Selection mode
0865       const hitsEvt = this.raycaster.intersectObjects(this.sceneEvent.children, true);
0866       const hitsGeo = this.raycaster.intersectObjects(this.sceneGeometry.children, true);
0867 
0868       const filteredEvtHits = this.filterClippedIntersections(hitsEvt);
0869       const filteredGeoHits = this.filterClippedIntersections(hitsGeo);
0870 
0871       const selected = filteredEvtHits[0] ?? filteredGeoHits[0];
0872 
0873       if (selected && selected.object.visible && selected.object.name &&
0874         selected.object.name !== 'HoverPoint' &&
0875         !selected.object.name.startsWith('MeasurePoint_')) {
0876         this.trackClicked.next({ track: selected.object, point: selected.point.clone() });
0877         console.log('[raycast] SELECTED', selected.object.name, selected.point);
0878       }
0879     };
0880 
0881     //  Double-click handler for distance measurement
0882     const onDoubleClick = (event: MouseEvent) => {
0883       if (!this.isRaycastEnabled || !this.measureMode) return;
0884 
0885       event.preventDefault();
0886       event.stopPropagation();
0887 
0888       const canvas = this.renderer.domElement;
0889       const rect = canvas.getBoundingClientRect();
0890 
0891       this.pointer.x = ((event.clientX - rect.left) / canvas.clientWidth) * 2 - 1;
0892       this.pointer.y = -((event.clientY - rect.top) / canvas.clientHeight) * 2 + 1;
0893 
0894       this.raycaster.setFromCamera(this.pointer, this.camera);
0895       this.raycaster.firstHitOnly = false;
0896 
0897       // Measure mode - double click
0898       const hitsEvt = this.raycaster.intersectObjects(this.sceneEvent.children, true);
0899       const hitsGeo = this.raycaster.intersectObjects(this.sceneGeometry.children, true);
0900 
0901       const filteredEvtHits = this.filterClippedIntersections(hitsEvt);
0902       const filteredGeoHits = this.filterClippedIntersections(hitsGeo);
0903 
0904       const picked = filteredEvtHits[0] ?? filteredGeoHits[0];
0905 
0906       if (picked && picked.object.visible &&
0907         picked.object.name !== 'HoverPoint' &&
0908         !picked.object.name.startsWith('MeasurePoint_')) {
0909         const pt = picked.point.clone();
0910 
0911         if (!this.firstMeasurePoint) {
0912           this.firstMeasurePoint = pt;
0913           this.showMeasurePoint(pt, 'first');
0914           console.log('[raycast] DIST: first point from', picked.object.name, pt);
0915         } else {
0916           const p1 = this.firstMeasurePoint.clone();
0917           const p2 = pt;
0918           const dist = p1.distanceTo(p2);
0919 
0920           this.showMeasurePoint(pt, 'second');
0921 
0922           this.ngZone.run(() => {
0923             this.distanceReady.next({ p1, p2, dist });
0924           });
0925 
0926           console.log('[raycast] DIST: second point from', picked.object.name, '→', dist.toFixed(2));
0927 
0928           //  Reset after measurement
0929           setTimeout(() => this.resetMeasurement(), 2000); // Clear after 2 seconds
0930         }
0931       }
0932     };
0933 
0934     //  Clean up existing listeners before adding new ones
0935     this.cleanupEventListeners();
0936 
0937     // Store references and attach listeners
0938     this.pointerMoveHandler = onPointerMove;
0939     this.pointerDownHandler = onPointerDown;
0940     this.doubleClickHandler = onDoubleClick;
0941 
0942     const canvas = this.renderer.domElement;
0943     canvas.addEventListener('pointermove', this.pointerMoveHandler, false);
0944     canvas.addEventListener('pointerdown', this.pointerDownHandler, false);
0945     canvas.addEventListener('dblclick', this.doubleClickHandler, false);
0946   }
0947 
0948 //  Visual feedback for measurement points
0949   private showMeasurePoint(point: THREE.Vector3, type: 'first' | 'second'): void {
0950     const geometry = new THREE.SphereGeometry(8, 16, 16);
0951     const material = new THREE.MeshBasicMaterial({
0952       color: type === 'first' ? 0x00ff00 : 0x0000ff, // Green for first, blue for second
0953       transparent: true,
0954       opacity: 0.9,
0955       depthTest: false,
0956       depthWrite: false
0957     });
0958 
0959     const sphere = new THREE.Mesh(geometry, material);
0960     sphere.position.copy(point);
0961     sphere.name = `MeasurePoint_${type}`;
0962     sphere.renderOrder = 1000;
0963 
0964     // Add to helpers scene
0965     this.sceneHelpers.add(sphere);
0966     this.measurementPoints.push(sphere);
0967   }
0968 
0969 //  Reset measurement state and clear visual indicators
0970   private resetMeasurement(): void {
0971     this.firstMeasurePoint = null;
0972     this.clearMeasurePoints();
0973   }
0974 
0975   private clearMeasurePoints(): void {
0976     this.measurementPoints.forEach(point => {
0977       this.sceneHelpers.remove(point);
0978       point.geometry.dispose();
0979       if (point.material instanceof THREE.Material) {
0980         point.material.dispose();
0981       }
0982     });
0983     this.measurementPoints = [];
0984   }
0985 
0986   /**
0987    * Clean up event listeners
0988    */
0989   private cleanupEventListeners(): void {
0990 
0991     if(!this.renderer?.domElement) {
0992       return;
0993     }
0994     const canvas = this.renderer.domElement;
0995 
0996     if (this.pointerMoveHandler) {
0997       canvas.removeEventListener('pointermove', this.pointerMoveHandler);
0998       this.pointerMoveHandler = undefined;
0999     }
1000     if (this.pointerDownHandler) {
1001       canvas.removeEventListener('pointerdown', this.pointerDownHandler);
1002       this.pointerDownHandler = undefined;
1003     }
1004     if (this.doubleClickHandler) {
1005       canvas.removeEventListener('dblclick', this.doubleClickHandler);
1006       this.doubleClickHandler = undefined;
1007     }
1008 
1009     //  Clear timeout if active
1010     if (this.hoverTimeout) {
1011       clearTimeout(this.hoverTimeout);
1012       this.hoverTimeout = null;
1013     }
1014   }
1015 
1016   setupBVH(): void {
1017     const processMesh = (mesh: THREE.Mesh) => {
1018       if (mesh.geometry && !mesh.geometry.boundsTree) {
1019         // @ts-ignore
1020         mesh.geometry.computeBoundsTree({
1021           maxLeafTris: 10,
1022           strategy: 0
1023         });
1024       }
1025     };
1026 
1027     this.sceneGeometry.traverse((object) => {
1028       if ((object as any).isMesh) {
1029         processMesh(object as Mesh);
1030       }
1031     });
1032 
1033     this.sceneEvent.traverse((object) => {
1034       if ((object as any).isMesh) {
1035         processMesh(object as Mesh);
1036       }
1037     });
1038   }
1039 
1040   cleanupBVH(object: THREE.Object3D): void {
1041 
1042     if (object instanceof THREE.Mesh && object.geometry && object.geometry.boundsTree) {
1043       // @ts-ignore
1044       object.geometry.disposeBoundsTree();
1045     }
1046     if(object.children != null) {
1047       object.children.forEach(child => this.cleanupBVH(child));
1048     }
1049   }
1050 
1051   //  Enhanced toggle methods
1052   toggleRaycast(): void {
1053     this.isRaycastEnabled = !this.isRaycastEnabled;
1054     console.log(`Raycast is now ${this.isRaycastEnabled ? 'ENABLED' : 'DISABLED'}`);
1055 
1056     if (!this.isRaycastEnabled) {
1057       this.clearHighlight();
1058       //  Reset measurement when disabling raycast
1059       this.resetMeasurement();
1060     }
1061   }
1062 
1063   isRaycastEnabledState(): boolean {
1064     return this.isRaycastEnabled;
1065   }
1066 
1067 }