Warning, /firebird/packages/firebird-ng/display/src/lib/components/scene-tree/scene-tree.component.ts is written in an unsupported language. File is not indexed.
0001 import {
0002 ChangeDetectorRef,
0003 Component,
0004 EventEmitter,
0005 OnInit,
0006 Output,
0007 ChangeDetectionStrategy
0008 } from '@angular/core';
0009
0010 import {
0011 MatTree,
0012 MatTreeNode,
0013 MatTreeNodeDef,
0014 MatTreeNodePadding,
0015 MatTreeNodeToggle,
0016 } from '@angular/material/tree';
0017 import { MatIcon } from '@angular/material/icon';
0018 import { MatIconButton } from '@angular/material/button';
0019 import { MatTooltip } from '@angular/material/tooltip';
0020
0021 import * as THREE from 'three';
0022 import { Mesh, Object3D} from 'three';
0023
0024 import { Line, LineSegments } from 'three';
0025 import { Line2 } from 'three/examples/jsm/lines/Line2.js';
0026 import { ThreeService } from '../../services/three.service';
0027
0028
0029
0030 @Component({
0031 selector: 'app-scene-tree',
0032 standalone: true,
0033 imports: [
0034 MatTree,
0035 MatTreeNode,
0036 MatTreeNodeToggle,
0037 MatTreeNodeDef,
0038 MatTreeNodePadding,
0039 MatIcon,
0040 MatTooltip,
0041 MatIconButton,
0042 ],
0043 templateUrl: './scene-tree.component.html',
0044 changeDetection: ChangeDetectionStrategy.OnPush,
0045 styleUrls: ['./scene-tree.component.scss'],
0046 })
0047 export class SceneTreeComponent implements OnInit {
0048
0049 @Output() configureItem = new EventEmitter<string>();
0050
0051 /** Enable/disable generic geometry highlighting. */
0052 public isHighlightingEnabled = false;
0053
0054 /** Enable/disable event-track highlighting. */
0055 public isTrackHighlightingEnabled = false;
0056
0057
0058 /* ---------------- Highlight materials for regular geometry ---------------- */
0059
0060 private readonly geometryHighlightMaterial = new THREE.MeshBasicMaterial({
0061 color: 0xffff00,
0062 wireframe: true,
0063 });
0064
0065 /* ---------------- Tree helpers ---------------- */
0066
0067 public childrenAccessor = (node: Object3D): Object3D[] => node.children ?? [];
0068
0069 public isExpandable = (node: Object3D): boolean =>
0070 !!node.children && node.children.length > 0;
0071
0072 public dataSource: Object3D[] = [];
0073
0074 public hasChild = (_: number, node: Object3D) =>
0075 !!node.children && node.children.length > 0;
0076
0077 /* ---------------- Constructor / init ---------------- */
0078
0079 constructor(
0080 private threeService: ThreeService,
0081 private cdr: ChangeDetectorRef,
0082 ) {}
0083
0084 ngOnInit(): void {
0085 this.refreshSceneTree();
0086 }
0087
0088 /* ---------------- UI callbacks ---------------- */
0089
0090
0091 public get isAnyHighlightingEnabled(): boolean {
0092 return this.isHighlightingEnabled || this.isTrackHighlightingEnabled;
0093 }
0094
0095
0096 public toggleHighlighting(): void {
0097 const newState = !this.isAnyHighlightingEnabled;
0098
0099 this.isHighlightingEnabled = newState;
0100 this.isTrackHighlightingEnabled = newState;
0101 }
0102
0103 /* ---------------- Tree population ---------------- */
0104
0105 public refreshSceneTree(): void {
0106 const scene = this.threeService.scene;
0107 if (!scene) {
0108 console.warn('No scene present in ThreeService.');
0109 return;
0110 }
0111 // Force tree to disconnect old data source, then reconnect with new data
0112 this.dataSource = [];
0113 this.cdr.detectChanges();
0114 this.dataSource = [...scene.children];
0115 this.cdr.detectChanges();
0116 }
0117
0118 /* ---------------- Visibility toggle ---------------- */
0119
0120 public isEffectivelyVisible(obj: Object3D): boolean {
0121 let cur: Object3D | null = obj;
0122 while (cur) {
0123 if (!cur.visible) return false;
0124 cur = cur.parent!;
0125 }
0126 return true;
0127 }
0128
0129
0130 private revealPath(target: Object3D, full = false): void {
0131 // enabling each invisible parent and hiding its other children
0132 let branch: Object3D = target;
0133 let parent: Object3D | null = branch.parent;
0134 while (parent) {
0135 if (!parent.visible) {
0136 parent.visible = true;
0137 parent.children.forEach(child => {
0138 if (child !== branch) child.visible = false;
0139 });
0140 }
0141 branch = parent;
0142 parent = parent.parent;
0143 }
0144
0145 // show every descendant of the original target
0146 if (full) target.traverse(child => (child.visible = true));
0147 }
0148
0149
0150 public toggleVisibility(node: Object3D): void {
0151 if (node.visible) {
0152 node.visible = false;
0153 } else {
0154 const hiddenAncestor = (() => {
0155 for (let p = node.parent; p; p = p.parent) if (!p.visible) return true;
0156 return false;
0157 })();
0158
0159 if (hiddenAncestor) {
0160 this.revealPath(node, /* full = */ false);
0161 node.visible = true;
0162 } else {
0163 this.revealPath(node, /* full = */ true);
0164 node.visible = true;
0165 }
0166 }
0167 this.threeService.invalidate();
0168 }
0169
0170
0171 /* ---------------- Mouse-over handlers ---------------- */
0172
0173 public onMouseEnterNode(node: Object3D): void {
0174 if (this.isHighlightingEnabled && !this.isTrackNode(node)) {
0175 this.highlightNode(node);
0176 }
0177
0178 if (this.isTrackHighlightingEnabled && this.isTrackNode(node)) {
0179 this.highlightTrack(node);
0180 }
0181 }
0182
0183 public onMouseLeaveNode(node: Object3D): void {
0184 if (this.isHighlightingEnabled && !this.isTrackNode(node)) {
0185 this.unhighlightNode(node);
0186 }
0187
0188 if (this.isTrackHighlightingEnabled && this.isTrackNode(node)) {
0189 this.unhighlightTrack(node);
0190 }
0191 }
0192
0193 /* ---------------- Geometry highlight (meshes) ---------------- */
0194
0195 public highlightNode(node: Object3D): void {
0196 node.traverse(child => {
0197 if (child instanceof Mesh) {
0198 if (!child.userData['origMaterial']) {
0199 child.userData['origMaterial'] = child.material;
0200 }
0201 child.material = this.geometryHighlightMaterial;
0202 }
0203 });
0204 this.threeService.invalidate();
0205 }
0206
0207 public unhighlightNode(node: Object3D): void {
0208 node.traverse(child => {
0209 if (child instanceof Mesh && child.userData['origMaterial']) {
0210 child.material = child.userData['origMaterial'];
0211 delete child.userData['origMaterial'];
0212 }
0213 });
0214 this.threeService.invalidate();
0215 }
0216
0217
0218 /** Returns true if node belongs to the "event track" hierarchy. */
0219 public isTrackNode(node: Object3D): boolean {
0220 return (
0221 this.isUnderEventParent(node) &&
0222 (node.name.toLowerCase().includes('track') ||
0223 this.hasLineGeometry(node) ||
0224 node.userData?.['isTrack'] === true)
0225 );
0226 }
0227
0228 /** Detect "Event" parent upwards in scene graph. */
0229 private isUnderEventParent(node: Object3D): boolean {
0230 if (
0231 node.name.toLowerCase() === 'event' ||
0232 node.userData?.['isEvent'] === true
0233 ) {
0234 return true;
0235 }
0236 let cur: Object3D | null = node;
0237 while (cur && cur.parent) {
0238 if (
0239 cur.parent.name.toLowerCase() === 'event' ||
0240 cur.parent.userData?.['isEvent'] === true
0241 ) {
0242 return true;
0243 }
0244 cur = cur.parent;
0245 }
0246 return false;
0247 }
0248
0249 /** Quick geometry test for lines. */
0250 private hasLineGeometry(object: Object3D): boolean {
0251 let found = false;
0252 object.traverse(o => {
0253 if (
0254 o instanceof Line ||
0255 o instanceof LineSegments ||
0256 o instanceof Line2 ||
0257 (o instanceof Mesh &&
0258 o.geometry &&
0259 o.geometry.type.includes('Line'))
0260 ) {
0261 found = true;
0262 }
0263 });
0264 return found;
0265 }
0266
0267
0268 public highlightTrack(node: Object3D): void {
0269 node.traverse(obj3d => {
0270 if (obj3d.userData['highlightFunction']) {
0271 obj3d.userData['highlightFunction']();
0272 }
0273 });
0274 this.threeService.invalidate();
0275 }
0276
0277 public unhighlightTrack(node: Object3D): void {
0278 node.traverse(obj3d => {
0279 if (obj3d.userData['unhighlightFunction']) {
0280 obj3d.userData['unhighlightFunction']();
0281 }
0282 });
0283 this.threeService.invalidate();
0284 }
0285 }