Warning, /firebird/packages/firebird-ng/display/src/lib/services/selection.service.ts is written in an unsupported language. File is not indexed.
0001 import { Injectable, effect, inject, signal } from '@angular/core';
0002 import { EntityRef, entityRefOf } from '@dexvis/firebird-core';
0003 import { ThreeService } from './three.service';
0004 import { EventDisplayService } from './event-display.service';
0005 import { DataModelService } from './data-model.service';
0006
0007 /**
0008 * The one selection of the event display: (piece name, entity index) — the
0009 * physics identity of the thing the user picked, not a scene object.
0010 *
0011 * Both directions run through here:
0012 * - a 3D click resolves the picked Object3D to its entity (painters stamp
0013 * their objects; `entityRefOf` walks up to the stamp) and sets the signal;
0014 * - panels (model tree, inspectors) call `select()` and the owning painter
0015 * highlights its scene objects.
0016 *
0017 * Hover over event data (opt-in hover picking) routes the same way: the
0018 * hovered entity is highlighted through its painter, and the selected
0019 * entity keeps its highlight while the pointer passes over it or others.
0020 *
0021 * Painters own the entity↔object mapping; this service only routes.
0022 */
0023 @Injectable({
0024 providedIn: 'root',
0025 })
0026 export class SelectionService {
0027 private three = inject(ThreeService);
0028 private eventDisplay = inject(EventDisplayService);
0029 private dataService = inject(DataModelService);
0030
0031 private selectionSignal = signal<EntityRef | null>(null);
0032
0033 /** The current selection, null when nothing is selected. */
0034 readonly selection = this.selectionSignal.asReadonly();
0035
0036 private selectedPieceSignal = signal<string | null>(null);
0037
0038 /** The entity under the pointer (hover picking), highlighted through its painter. */
0039 private hovered: EntityRef | null = null;
0040
0041 /**
0042 * The piece the UI is focused on — set by entity selections (their piece)
0043 * and by piece-level clicks in the model tree. The painter-config panel
0044 * shows this piece's painter and knobs.
0045 */
0046 readonly selectedPiece = this.selectedPieceSignal.asReadonly();
0047
0048 constructor() {
0049 // 3D pick → selection. trackClicked fires for any picked scene object;
0050 // only objects painters stamped resolve to an entity. The intersection
0051 // rides along for batched painters, which resolve the entity from the
0052 // picked segment instead of the object.
0053 this.three.trackClicked.subscribe(({ track, intersection }) => {
0054 const ref = entityRefOf(track, intersection);
0055 if (ref) {
0056 this.select(ref);
0057 }
0058 });
0059
0060 // 3D hover → painter highlight (null: the pointer left event data).
0061 this.three.trackHovered.subscribe(picked => {
0062 this.hover(picked ? entityRefOf(picked.track, picked.intersection) : null);
0063 });
0064
0065 // Event switches rebuild all painters — the old selection points into
0066 // disposed objects, so it resets.
0067 effect(() => {
0068 this.dataService.currentEntry();
0069 this.hovered = null;
0070 this.selectionSignal.set(null);
0071 this.selectedPieceSignal.set(null);
0072 });
0073 }
0074
0075 /**
0076 * Moves the hover highlight to `ref` (null clears it). The selected entity
0077 * is never unhighlighted by hover, and is re-highlighted when the hover
0078 * leaves it: a painter may hold a single highlight slot.
0079 */
0080 private hover(ref: EntityRef | null): void {
0081 const previous = this.hovered;
0082 if (sameEntity(previous, ref)) return;
0083 this.hovered = ref;
0084 const selected = this.selectionSignal();
0085 if (previous && !sameEntity(previous, selected)) {
0086 this.eventDisplay.painterFor(previous.pieceName)?.unhighlightEntity(previous.entityIndex);
0087 }
0088 if (ref && !sameEntity(ref, selected)) {
0089 this.eventDisplay.painterFor(ref.pieceName)?.highlightEntity(ref.entityIndex);
0090 }
0091 if (selected && previous && !ref) {
0092 this.eventDisplay.painterFor(selected.pieceName)?.highlightEntity(selected.entityIndex);
0093 }
0094 // Highlights mutate materials directly — schedule a render.
0095 this.three.invalidate();
0096 }
0097
0098 /** Focuses a piece without selecting an entity (model-tree piece click). */
0099 selectPiece(pieceName: string | null): void {
0100 this.selectedPieceSignal.set(pieceName);
0101 }
0102
0103 /**
0104 * Selects one entity (or clears with null). The previously selected
0105 * entity is unhighlighted and the new one highlighted through its painter.
0106 */
0107 select(ref: EntityRef | null): void {
0108 const previous = this.selectionSignal();
0109 // The previous selection keeps its highlight while it is still hovered.
0110 if (previous && !sameEntity(previous, ref) && !sameEntity(previous, this.hovered)) {
0111 this.eventDisplay.painterFor(previous.pieceName)?.unhighlightEntity(previous.entityIndex);
0112 }
0113 if (ref) {
0114 this.eventDisplay.painterFor(ref.pieceName)?.highlightEntity(ref.entityIndex);
0115 this.selectedPieceSignal.set(ref.pieceName);
0116 }
0117 this.selectionSignal.set(ref);
0118 // Highlights mutate materials directly — schedule a render.
0119 this.three.invalidate();
0120 }
0121
0122 clear(): void {
0123 this.select(null);
0124 }
0125
0126 /** True when the given entity is the current selection. */
0127 isSelected(pieceName: string, entityIndex: number): boolean {
0128 const current = this.selectionSignal();
0129 return current !== null && current.pieceName === pieceName && current.entityIndex === entityIndex;
0130 }
0131 }
0132
0133 /** True when both refs name the same entity (two nulls count as the same). */
0134 function sameEntity(a: EntityRef | null, b: EntityRef | null | undefined): boolean {
0135 if (!a || !b) return !a && !b;
0136 return a.pieceName === b.pieceName && a.entityIndex === b.entityIndex;
0137 }