Warning, /firebird/packages/firebird-ng/display/src/lib/components/object-raycast/object-raycast.component.ts is written in an unsupported language. File is not indexed.
0001 import {Component, ElementRef, OnDestroy, TemplateRef, ViewChild, ViewContainerRef, ChangeDetectionStrategy, signal} from '@angular/core';
0002 import {MatDialog, MatDialogClose, MatDialogRef} from "@angular/material/dialog";
0003 import {MatMenuItem} from "@angular/material/menu";
0004 import {MatCheckbox, MatCheckboxChange} from "@angular/material/checkbox";
0005 import {MatIcon} from "@angular/material/icon";
0006 import {MatIconButton} from "@angular/material/button";
0007 import {Subscription} from "rxjs";
0008 import {ThreeService} from "../../services/three.service";
0009 import {SelectionService} from "../../services/selection.service";
0010 import * as THREE from 'three';
0011 import {MatTooltip} from "@angular/material/tooltip";
0012
0013 @Component({
0014 selector: 'app-object-raycast',
0015 imports: [
0016 MatIcon,
0017 MatDialogClose,
0018 MatMenuItem,
0019 MatCheckbox,
0020 MatTooltip,
0021 MatIconButton,
0022 ],
0023 templateUrl: './object-raycast.component.html',
0024 changeDetection: ChangeDetectionStrategy.OnPush,
0025 styleUrl: './object-raycast.component.scss'
0026 })
0027 export class ObjectRaycastComponent implements OnDestroy {
0028
0029 @ViewChild('openRayBtn', { read: ElementRef }) openRayBtn!: ElementRef;
0030 @ViewChild('raycastDialogTmpl') raycastDialogTmpl!: TemplateRef<any>;
0031 dialogRef: MatDialogRef<any> | null = null;
0032
0033 // UI state. Signals: the checkboxes live in the dialog, which renders this
0034 // component's template in another view; a plain field changed there would
0035 // never reach the overlay below in this component's own view.
0036 readonly coordsEnabled = signal(false);
0037 readonly distanceEnabled = signal(false);
0038
0039 // Signals as well: updates arrive from native canvas listeners via
0040 // ThreeService, which schedule no change detection under zoneless.
0041 readonly coordsText = signal('');
0042 readonly distanceText = signal('');
0043
0044 private coordsSub?: Subscription;
0045 private distSub?: Subscription;
0046 private distLine?: THREE.Line;
0047
0048 /** True while this component holds ThreeService's measure mode on. */
0049 private measuring = false;
0050
0051 constructor(
0052 private dialog: MatDialog,
0053 private three: ThreeService,
0054 private viewContainerRef: ViewContainerRef,
0055 /** Selection is resolved by SelectionService (3D pick → entity), not by
0056 * this component's own picking; the overlay shows the shared selection. */
0057 public selection: SelectionService,
0058 ) {}
0059
0060 /* ------------ UI ------------- */
0061 openRaycastDialog() {
0062 if (this.dialogRef) {
0063 this.dialogRef.close();
0064 return;
0065 }
0066
0067 const rect = this.openRayBtn.nativeElement.getBoundingClientRect();
0068 const dialogWidth = 320;
0069
0070 const left = Math.max(rect.right - dialogWidth, 8);
0071 const top = rect.bottom + 12;
0072
0073 this.dialogRef = this.dialog.open(this.raycastDialogTmpl, {
0074 position: {
0075 top: `${top}px`,
0076 left: `${left}px`
0077 },
0078 hasBackdrop: false,
0079 panelClass: 'custom-position-dialog',
0080 autoFocus: false,
0081 viewContainerRef: this.viewContainerRef
0082 });
0083
0084 this.dialogRef.afterClosed().subscribe(() => {
0085 this.dialogRef = null;
0086 });
0087 }
0088
0089 /* ---------- checkbox handlers ---------- */
0090
0091
0092 toggleShowCoords(e: MatCheckboxChange): void {
0093 this.coordsEnabled.set(e.checked);
0094 this.updateSubscriptions();
0095 this.updateRaycastActivation();
0096 }
0097
0098 toggleShowDistance(e: MatCheckboxChange): void {
0099 this.distanceEnabled.set(e.checked);
0100 this.setMeasureMode(e.checked);
0101 this.updateSubscriptions();
0102 this.updateRaycastActivation();
0103 }
0104
0105 /** Measure mode turns clicks into distance points, so click-to-select pauses while it is on. */
0106 private setMeasureMode(on: boolean): void {
0107 this.measuring = on;
0108 this.three.measureMode = on;
0109 }
0110
0111 /* ---------- central switch ---------- */
0112 /** Ensures ThreeService raycast state matches UI needs */
0113 private updateRaycastActivation(): void {
0114 const needRaycast = this.coordsEnabled() || this.distanceEnabled();
0115 const isOn = this.three.isRaycastEnabledState();
0116 if (needRaycast && !isOn) this.three.toggleRaycast();
0117 if (!needRaycast && isOn) this.three.toggleRaycast();
0118 }
0119
0120 /* ---------- RxJS subscriptions ---------- */
0121 private updateSubscriptions(): void {
0122
0123 /* XYZ overlay */
0124 if (this.coordsEnabled() && !this.coordsSub) {
0125 this.coordsSub = this.three.pointHovered.subscribe(pt => {
0126 this.coordsText.set(`X:${pt.x.toFixed(2)} Y:${pt.y.toFixed(2)} Z:${pt.z.toFixed(2)}`);
0127 });
0128 } else if (!this.coordsEnabled() && this.coordsSub) {
0129 this.coordsSub.unsubscribe();
0130 this.coordsSub = undefined;
0131 this.coordsText.set('');
0132 }
0133
0134 /* distance overlay */
0135 if (this.distanceEnabled() && !this.distSub) {
0136 this.distSub = this.three.distanceReady.subscribe(({ p1, p2, dist }) => {
0137 this.distanceText.set(`${dist.toFixed(2)} units`);
0138
0139 // draw / update line helper
0140 if (!this.distLine) {
0141 const g = new THREE.BufferGeometry().setFromPoints([p1, p2]);
0142 const m = new THREE.LineBasicMaterial({ color: 0xffff00 });
0143 this.distLine = new THREE.Line(g, m);
0144 this.three.sceneHelpers.add(this.distLine);
0145 } else {
0146 (this.distLine.geometry as THREE.BufferGeometry).setFromPoints([p1, p2]);
0147 }
0148 this.three.invalidate();
0149 });
0150 } else if (!this.distanceEnabled() && this.distSub) {
0151 this.distSub.unsubscribe();
0152 this.distSub = undefined;
0153 this.distanceText.set('');
0154 this.removeDistanceLine();
0155 }
0156
0157 }
0158
0159 private removeDistanceLine(): void {
0160 if (!this.distLine) return;
0161 this.three.sceneHelpers.remove(this.distLine);
0162 this.distLine.geometry.dispose();
0163 (this.distLine.material as THREE.Material).dispose();
0164 this.distLine = undefined;
0165 this.three.invalidate();
0166 }
0167
0168 /* ---------- cleanup ---------- */
0169 /**
0170 * ThreeService outlives display pages: leaving the page must hand back
0171 * everything this component switched on there. Otherwise measure mode
0172 * stays on (click-to-select stops working on the next visit), hover
0173 * picking keeps running, and the distance line stays in the scene.
0174 */
0175 ngOnDestroy(): void {
0176 this.dialogRef?.close();
0177 // The page holds two instances (desktop and mobile toolbars): only the
0178 // one that switched hover picking on switches it off.
0179 const holdsRaycast = this.coordsEnabled() || this.distanceEnabled();
0180 this.coordsEnabled.set(false);
0181 this.distanceEnabled.set(false);
0182 if (this.measuring) {
0183 this.setMeasureMode(false);
0184 }
0185 this.updateSubscriptions();
0186 if (holdsRaycast) {
0187 this.updateRaycastActivation();
0188 }
0189 this.removeDistanceLine();
0190 }
0191 }