xref: /aosp_15_r20/development/tools/winscope/src/viewers/components/rects/rects_component.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {
18  Component,
19  ElementRef,
20  EventEmitter,
21  HostListener,
22  Inject,
23  Input,
24  OnDestroy,
25  OnInit,
26  Output,
27  SimpleChange,
28  SimpleChanges,
29} from '@angular/core';
30import {CanColor} from '@angular/material/core';
31import {MatIconRegistry} from '@angular/material/icon';
32import {MatSelectChange} from '@angular/material/select';
33import {DomSanitizer} from '@angular/platform-browser';
34import {assertDefined} from 'common/assert_utils';
35import {Distance} from 'common/geometry/distance';
36import {PersistentStore} from 'common/persistent_store';
37import {UrlUtils} from 'common/url_utils';
38import {Analytics} from 'logging/analytics';
39import {TRACE_INFO} from 'trace/trace_info';
40import {TraceType} from 'trace/trace_type';
41import {DisplayIdentifier} from 'viewers/common/display_identifier';
42import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
43import {UserOptions} from 'viewers/common/user_options';
44import {RectDblClickDetail, ViewerEvents} from 'viewers/common/viewer_events';
45import {UiRect} from 'viewers/components/rects/ui_rect';
46import {iconDividerStyle} from 'viewers/components/styles/icon_divider.styles';
47import {multlineTooltip} from 'viewers/components/styles/tooltip.styles';
48import {viewerCardInnerStyle} from 'viewers/components/styles/viewer_card.styles';
49import {Canvas} from './canvas';
50import {Mapper3D} from './mapper3d';
51import {ShadingMode} from './shading_mode';
52
53@Component({
54  selector: 'rects-view',
55  template: `
56    <div class="view-header">
57      <div class="title-section">
58        <collapsible-section-title
59          [title]="title"
60          (collapseButtonClicked)="collapseButtonClicked.emit()"></collapsible-section-title>
61        <div class="right-btn-container">
62          <button
63            color="accent"
64            class="shading-mode"
65            (mouseenter)="onInteractionStart([shadingModeButton])"
66            (mouseleave)="onInteractionEnd([shadingModeButton])"
67            mat-icon-button
68            [matTooltip]="getShadingMode()"
69            [disabled]="shadingModes.length < 2"
70            (click)="onShadingModeButtonClicked()" #shadingModeButton>
71            <mat-icon *ngIf="largeRectsMapper3d.isWireFrame()" class="material-symbols-outlined" aria-hidden="true"> deployed_code </mat-icon>
72            <mat-icon *ngIf="largeRectsMapper3d.isShadedByGradient()" svgIcon="cube_partial_shade"></mat-icon>
73            <mat-icon *ngIf="largeRectsMapper3d.isShadedByOpacity()" svgIcon="cube_full_shade"></mat-icon>
74          </button>
75
76          <div class="icon-divider"></div>
77
78          <div class="slider-container">
79            <mat-icon
80              color="accent"
81              matTooltip="Rotation"
82              class="slider-icon"
83              (mouseenter)="onInteractionStart([rotationSlider, rotationSliderIcon])"
84              (mouseleave)="onInteractionEnd([rotationSlider, rotationSliderIcon])" #rotationSliderIcon> rotate_90_degrees_ccw </mat-icon>
85            <mat-slider
86              class="slider-rotation"
87              step="0.02"
88              min="0"
89              max="1"
90              aria-label="units"
91              [value]="largeRectsMapper3d.getCameraRotationFactor()"
92              (input)="onRotationSliderChange($event.value)"
93              (focus)="$event.target.blur()"
94              color="accent"
95              (mousedown)="onInteractionStart([rotationSlider, rotationSliderIcon])"
96              (mouseup)="onInteractionEnd([rotationSlider, rotationSliderIcon])" #rotationSlider></mat-slider>
97            <mat-icon
98              color="accent"
99              matTooltip="Spacing"
100              class="slider-icon material-symbols-outlined"
101              (mouseenter)="onInteractionStart([spacingSlider, spacingSliderIcon])"
102              (mouseleave)="onInteractionEnd([spacingSlider, spacingSliderIcon])" #spacingSliderIcon> format_letter_spacing </mat-icon>
103            <mat-slider
104              class="slider-spacing"
105              step="0.02"
106              min="0.02"
107              max="1"
108              aria-label="units"
109              [value]="getZSpacingFactor()"
110              (input)="onSeparationSliderChange($event.value)"
111              (focus)="$event.target.blur()"
112              color="accent"
113              (mousedown)="onInteractionStart([spacingSlider, spacingSliderIcon])"
114              (mouseup)="onInteractionEnd([spacingSlider, spacingSliderIcon])" #spacingSlider></mat-slider>
115          </div>
116
117          <div class="icon-divider"></div>
118
119          <button
120            color="accent"
121            (mouseenter)="onInteractionStart([zoomInButton])"
122            (mouseleave)="onInteractionEnd([zoomInButton])"
123            mat-icon-button
124            class="zoom-in-button"
125            (click)="onZoomInClick()" #zoomInButton>
126            <mat-icon aria-hidden="true"> zoom_in </mat-icon>
127          </button>
128          <button
129            color="accent"
130            (mouseenter)="onInteractionStart([zoomOutButton])"
131            (mouseleave)="onInteractionEnd([zoomOutButton])"
132            mat-icon-button
133            class="zoom-out-button"
134            (click)="onZoomOutClick()" #zoomOutButton>
135            <mat-icon aria-hidden="true"> zoom_out </mat-icon>
136          </button>
137
138          <div class="icon-divider"></div>
139
140          <button
141            color="accent"
142            (mouseenter)="onInteractionStart([resetZoomButton])"
143            (mouseleave)="onInteractionEnd([resetZoomButton])"
144            mat-icon-button
145            matTooltip="Restore camera settings"
146            class="reset-button"
147            (click)="resetCamera()" #resetZoomButton>
148            <mat-icon aria-hidden="true"> restore </mat-icon>
149          </button>
150        </div>
151      </div>
152      <div class="filter-controls view-controls">
153        <user-options
154          class="block-filter-controls"
155          [userOptions]="userOptions"
156          [eventType]="ViewerEvents.RectsUserOptionsChange"
157          [traceType]="dependencies[0]"
158          [logCallback]="Analytics.Navigation.logRectSettingsChanged">
159        </user-options>
160
161        <div class="displays-section">
162          <span class="mat-body-1"> {{groupLabel}}: </span>
163          <mat-form-field appearance="none" class="displays-select">
164            <mat-select
165              #displaySelect
166              disableOptionCentering
167              (selectionChange)="onDisplaySelectChange($event)"
168              [value]="currentDisplays"
169              [disabled]="internalDisplays.length === 1"
170              multiple>
171              <mat-select-trigger>
172                <span>
173                  {{ getSelectTriggerValue() }}
174                </span>
175              </mat-select-trigger>
176              <mat-option
177                *ngFor="let display of internalDisplays"
178                [value]="display"
179                [matTooltip]="'Display Id: ' + display.displayId"
180                matTooltipPosition="right">
181                <div class="option-label">
182                  <button
183                    mat-flat-button
184                    class="option-only-button"
185                    (click)="onOnlyButtonClick($event, display)"> Only </button>
186                  <span class="option-label-text"> {{ display.name }} </span>
187                </div>
188              </mat-option>
189            </mat-select>
190          </mat-form-field>
191        </div>
192      </div>
193    </div>
194    <mat-divider></mat-divider>
195    <span class="mat-body-1 placeholder-text" *ngIf="rects.length===0"> No rects found. </span>
196    <span class="mat-body-1 placeholder-text" *ngIf="currentDisplays.length===0"> No displays selected. </span>
197    <div class="rects-content">
198      <div class="canvas-container">
199        <canvas
200          class="large-rects-canvas"
201          (click)="onRectClick($event)"
202          (dblclick)="onRectDblClick($event)"
203          oncontextmenu="return false"></canvas>
204        <div class="large-rects-labels"></div>
205        <canvas
206          class="mini-rects-canvas"
207          (dblclick)="onMiniRectDblClick($event)"
208          oncontextmenu="return false"></canvas>
209      </div>
210    </div>
211  `,
212  styles: [
213    `
214      .view-header {
215        display: flex;
216        flex-direction: column;
217      }
218      .right-btn-container {
219        display: flex;
220        align-items: center;
221        padding: 2px 0px;
222      }
223      .right-btn-container .mat-slider-horizontal {
224        min-width: 64px !important;
225      }
226      .icon-divider {
227        height: 50%;
228      }
229      .slider-container {
230        padding: 0 5px;
231        display: flex;
232        align-items: center;
233      }
234      .slider-icon {
235        min-width: 18px;
236        width: 18px;
237        height: 18px;
238        line-height: 18px;
239        font-size: 18px;
240      }
241      .filter-controls {
242        justify-content: space-between;
243      }
244      .block-filter-controls {
245        display: flex;
246        flex-direction: row;
247        align-items: baseline;
248      }
249      .displays-section {
250        display: flex;
251        flex-direction: row;
252        align-items: center;
253        width: fit-content;
254        flex-wrap: nowrap;
255      }
256      .displays-select {
257        font-size: 14px;
258        background-color: var(--disabled-color);
259        border-radius: 4px;
260        height: 24px;
261        margin-left: 5px;
262      }
263      .rects-content {
264        height: 100%;
265        display: flex;
266        flex-direction: column;
267        padding: 0px 12px;
268      }
269      .canvas-container {
270        height: 100%;
271        width: 100%;
272        position: relative;
273      }
274      .large-rects-canvas {
275        position: absolute;
276        top: 0;
277        left: 0;
278        width: 100%;
279        height: 100%;
280        cursor: pointer;
281      }
282      .large-rects-labels {
283        position: absolute;
284        top: 0;
285        left: 0;
286        width: 100%;
287        height: 100%;
288        pointer-events: none;
289      }
290      .mini-rects-canvas {
291        cursor: pointer;
292        width: 30%;
293        height: 30%;
294        top: 16px;
295        display: block;
296        position: absolute;
297        z-index: 1000;
298      }
299
300      .option-label {
301        display: flex;
302        align-items: center;
303        justify-content: space-between;
304      }
305
306      .option-only-button {
307        padding: 0 10px;
308        border-radius: 10px;
309        background-color: var(--disabled-color) !important;
310        color: var(--default-text-color);
311        min-width: fit-content;
312        height: 18px;
313        align-items: center;
314        display: flex;
315      }
316
317      .option-label-text {
318        overflow: hidden;
319        text-overflow: ellipsis;
320      }
321    `,
322    multlineTooltip,
323    iconDividerStyle,
324    viewerCardInnerStyle,
325  ],
326})
327export class RectsComponent implements OnInit, OnDestroy {
328  Analytics = Analytics;
329  ViewerEvents = ViewerEvents;
330
331  @Input() title = 'title';
332  @Input() zoomFactor = 1;
333  @Input() store?: PersistentStore;
334  @Input() rects: UiRect[] = [];
335  @Input() miniRects: UiRect[] | undefined;
336  @Input() displays: DisplayIdentifier[] = [];
337  @Input() highlightedItem = '';
338  @Input() groupLabel = 'Displays';
339  @Input() isStackBased = false;
340  @Input() shadingModes: ShadingMode[] = [ShadingMode.GRADIENT];
341  @Input() userOptions: UserOptions = {};
342  @Input() dependencies: TraceType[] = [];
343  @Input() pinnedItems: UiHierarchyTreeNode[] = [];
344  @Input() isDarkMode = false;
345
346  @Output() collapseButtonClicked = new EventEmitter();
347
348  private internalRects: UiRect[] = [];
349  private internalMiniRects?: UiRect[];
350  private storeKeyZSpacingFactor = '';
351  private storeKeyShadingMode = '';
352  private storeKeySelectedDisplays = '';
353  private internalDisplays: DisplayIdentifier[] = [];
354  private internalHighlightedItem = '';
355  private currentDisplays: DisplayIdentifier[] = [];
356  private largeRectsMapper3d = new Mapper3D();
357  private miniRectsMapper3d = new Mapper3D();
358  private largeRectsCanvas?: Canvas;
359  private miniRectsCanvas?: Canvas;
360  private resizeObserver = new ResizeObserver((entries) => {
361    this.updateLargeRectsPosition();
362  });
363  private largeRectsCanvasElement?: HTMLCanvasElement;
364  private miniRectsCanvasElement?: HTMLCanvasElement;
365  private largeRectsLabelsElement?: HTMLElement;
366  private mouseMoveListener = (event: MouseEvent) => this.onMouseMove(event);
367  private mouseUpListener = (event: MouseEvent) => this.onMouseUp(event);
368  private panning = false;
369
370  private static readonly ZOOM_SCROLL_RATIO = 0.3;
371
372  constructor(
373    @Inject(ElementRef) private elementRef: ElementRef<HTMLElement>,
374    @Inject(MatIconRegistry) private matIconRegistry: MatIconRegistry,
375    @Inject(DomSanitizer) private domSanitizer: DomSanitizer,
376  ) {
377    this.matIconRegistry.addSvgIcon(
378      'cube_full_shade',
379      this.domSanitizer.bypassSecurityTrustResourceUrl(
380        UrlUtils.getRootUrl() + 'cube_full_shade.svg',
381      ),
382    );
383    this.matIconRegistry.addSvgIcon(
384      'cube_partial_shade',
385      this.domSanitizer.bypassSecurityTrustResourceUrl(
386        UrlUtils.getRootUrl() + 'cube_partial_shade.svg',
387      ),
388    );
389  }
390
391  ngOnInit() {
392    this.largeRectsMapper3d.setAllowedShadingModes(this.shadingModes);
393
394    const canvasContainer = assertDefined(
395      this.elementRef.nativeElement.querySelector<HTMLElement>(
396        '.canvas-container',
397      ),
398    );
399    this.resizeObserver.observe(canvasContainer);
400
401    this.largeRectsCanvasElement = canvasContainer.querySelector(
402      '.large-rects-canvas',
403    )! as HTMLCanvasElement;
404    this.largeRectsLabelsElement = assertDefined(
405      canvasContainer.querySelector('.large-rects-labels'),
406    ) as HTMLElement;
407    this.largeRectsCanvas = new Canvas(
408      this.largeRectsCanvasElement,
409      this.largeRectsLabelsElement,
410      () => this.isDarkMode,
411    );
412    this.largeRectsCanvasElement.addEventListener('mousedown', (event) =>
413      this.onCanvasMouseDown(event),
414    );
415
416    this.largeRectsMapper3d.increaseZoomFactor(this.zoomFactor - 1);
417
418    if (this.store) {
419      this.updateControlsFromStore();
420    }
421
422    this.redrawLargeRectsAndLabels();
423
424    this.miniRectsCanvasElement = canvasContainer.querySelector(
425      '.mini-rects-canvas',
426    )! as HTMLCanvasElement;
427    this.miniRectsCanvas = new Canvas(
428      this.miniRectsCanvasElement,
429      undefined,
430      () => this.isDarkMode,
431    );
432    this.miniRectsMapper3d.setShadingMode(ShadingMode.GRADIENT);
433    this.miniRectsMapper3d.resetToOrthogonalState();
434    if (this.miniRects && this.miniRects.length > 0) {
435      this.internalMiniRects = this.miniRects;
436      this.drawMiniRects();
437    }
438  }
439
440  ngOnChanges(simpleChanges: SimpleChanges) {
441    this.handleLargeRectChanges(simpleChanges);
442    if (
443      simpleChanges['miniRects'] ||
444      (this.miniRects && simpleChanges['isDarkMode'])
445    ) {
446      this.internalMiniRects = this.miniRects;
447      this.drawMiniRects();
448    }
449  }
450
451  private handleLargeRectChanges(simpleChanges: SimpleChanges) {
452    let displayChange = false;
453    if (simpleChanges['displays']) {
454      const curr: DisplayIdentifier[] = simpleChanges['displays'].currentValue;
455      const prev: DisplayIdentifier[] =
456        simpleChanges['displays'].previousValue ?? [];
457      displayChange =
458        curr.length !== prev.length ||
459        (curr.length > 0 &&
460          !curr.every((d, index) => d.displayId === prev[index].displayId));
461    }
462
463    let redrawRects = false;
464    let recolorRects = false;
465    let recolorLabels = false;
466    if (simpleChanges['pinnedItems']) {
467      this.largeRectsMapper3d.setPinnedItems(this.pinnedItems);
468      recolorRects = true;
469    }
470    if (simpleChanges['highlightedItem']) {
471      this.internalHighlightedItem =
472        simpleChanges['highlightedItem'].currentValue;
473      this.largeRectsMapper3d.setHighlightedRectId(
474        this.internalHighlightedItem,
475      );
476      recolorRects = true;
477      recolorLabels = true;
478    }
479    if (simpleChanges['isDarkMode']) {
480      recolorRects = true;
481      recolorLabels = true;
482    }
483    if (simpleChanges['rects']) {
484      this.internalRects = simpleChanges['rects'].currentValue;
485      redrawRects = true;
486    }
487
488    if (displayChange) {
489      this.onDisplaysChange(simpleChanges['displays']);
490    } else if (redrawRects) {
491      this.redrawLargeRectsAndLabels();
492    } else if (recolorRects && recolorLabels) {
493      this.updateLargeRectsAndLabelsColors();
494    } else if (recolorRects) {
495      this.updateLargeRectsColors();
496    }
497  }
498
499  ngOnDestroy() {
500    this.resizeObserver?.disconnect();
501  }
502
503  onDisplaysChange(change: SimpleChange) {
504    const displays = change.currentValue;
505    this.internalDisplays = displays;
506    const activeDisplay = this.getActiveDisplay(this.internalDisplays);
507
508    if (displays.length === 0) {
509      this.updateCurrentDisplays([], false);
510      return;
511    }
512
513    if (change.firstChange) {
514      this.updateCurrentDisplays([activeDisplay], false);
515      return;
516    }
517
518    const curr = this.internalDisplays.filter((display) =>
519      this.currentDisplays.some((curr) => curr.displayId === display.displayId),
520    );
521    if (curr.length > 0) {
522      this.updateCurrentDisplays(curr);
523      return;
524    }
525
526    const currGroupIds = this.largeRectsMapper3d.getCurrentGroupIds();
527    const displaysWithCurrentGroupId = this.internalDisplays.filter((display) =>
528      currGroupIds.some((curr) => curr === display.groupId),
529    );
530    if (displaysWithCurrentGroupId.length === 0) {
531      this.updateCurrentDisplays([activeDisplay]);
532      return;
533    }
534
535    this.updateCurrentDisplays([
536      this.getActiveDisplay(displaysWithCurrentGroupId),
537    ]);
538    return;
539  }
540
541  updateControlsFromStore() {
542    this.storeKeyZSpacingFactor = `rectsView.${this.title}.zSpacingFactor`;
543    this.storeKeyShadingMode = `rectsView.${this.title}.shadingMode`;
544    this.storeKeySelectedDisplays = `rectsView.${this.title}.selectedDisplayId`;
545
546    const storedZSpacingFactor = assertDefined(this.store).get(
547      this.storeKeyZSpacingFactor,
548    );
549    if (storedZSpacingFactor !== undefined) {
550      this.largeRectsMapper3d.setZSpacingFactor(Number(storedZSpacingFactor));
551    }
552
553    const storedShadingMode = assertDefined(this.store).get(
554      this.storeKeyShadingMode,
555    );
556    if (
557      storedShadingMode !== undefined &&
558      this.shadingModes.includes(storedShadingMode as ShadingMode)
559    ) {
560      this.largeRectsMapper3d.setShadingMode(storedShadingMode as ShadingMode);
561    }
562
563    const storedSelectedDisplays = assertDefined(this.store).get(
564      this.storeKeySelectedDisplays,
565    );
566    if (storedSelectedDisplays !== undefined) {
567      const storedIds: Array<number | string> = JSON.parse(
568        storedSelectedDisplays,
569      );
570      const displays = this.internalDisplays.filter((display) => {
571        return storedIds.some((id) => display.displayId === id);
572      });
573      if (displays.length > 0) {
574        this.currentDisplays = displays;
575        this.largeRectsMapper3d.setCurrentGroupIds(
576          displays.map((d) => d.groupId),
577        );
578      }
579    }
580  }
581
582  onSeparationSliderChange(factor: number) {
583    Analytics.Navigation.logRectSettingsChanged(
584      'z spacing',
585      factor,
586      TRACE_INFO[this.dependencies[0]].name,
587    );
588    this.store?.add(this.storeKeyZSpacingFactor, `${factor}`);
589    this.largeRectsMapper3d.setZSpacingFactor(factor);
590    this.redrawLargeRectsAndLabels();
591  }
592
593  onRotationSliderChange(factor: number) {
594    this.largeRectsMapper3d.setCameraRotationFactor(factor);
595    this.updateLargeRectsPositionAndLabels();
596  }
597
598  resetCamera() {
599    Analytics.Navigation.logZoom('reset', 'rects');
600    this.largeRectsMapper3d.resetCamera();
601    this.redrawLargeRectsAndLabels(true);
602  }
603
604  @HostListener('wheel', ['$event'])
605  onScroll(event: WheelEvent) {
606    if ((event.target as HTMLElement).className === 'large-rects-canvas') {
607      if (event.deltaY > 0) {
608        Analytics.Navigation.logZoom('scroll', 'rects', 'out');
609        this.doZoomOut(RectsComponent.ZOOM_SCROLL_RATIO);
610      } else {
611        Analytics.Navigation.logZoom('scroll', 'rects', 'in');
612        this.doZoomIn(RectsComponent.ZOOM_SCROLL_RATIO);
613      }
614    }
615  }
616
617  onCanvasMouseDown(event: MouseEvent) {
618    document.addEventListener('mousemove', this.mouseMoveListener);
619    document.addEventListener('mouseup', this.mouseUpListener);
620  }
621
622  onMouseMove(event: MouseEvent) {
623    this.panning = true;
624    const distance = new Distance(event.movementX, event.movementY);
625    this.largeRectsMapper3d.addPanScreenDistance(distance);
626    this.updateLargeRectsPosition();
627  }
628
629  onMouseUp(event: MouseEvent) {
630    document.removeEventListener('mousemove', this.mouseMoveListener);
631    document.removeEventListener('mouseup', this.mouseUpListener);
632  }
633
634  onZoomInClick() {
635    Analytics.Navigation.logZoom('button', 'rects', 'in');
636    this.doZoomIn();
637  }
638
639  onZoomOutClick() {
640    Analytics.Navigation.logZoom('button', 'rects', 'out');
641    this.doZoomOut();
642  }
643
644  onDisplaySelectChange(event: MatSelectChange) {
645    const selectedDisplays: DisplayIdentifier[] = event.value;
646    this.updateCurrentDisplays(selectedDisplays);
647  }
648
649  getSelectTriggerValue(): string {
650    return this.currentDisplays.map((d) => d.name).join(', ');
651  }
652
653  onOnlyButtonClick(event: MouseEvent, selected: DisplayIdentifier) {
654    event.preventDefault();
655    event.stopPropagation();
656    this.updateCurrentDisplays([selected]);
657  }
658
659  onRectClick(event: MouseEvent) {
660    if (this.panning) {
661      this.panning = false;
662      return;
663    }
664    event.preventDefault();
665
666    const id = this.findClickedRectId(event);
667    if (id !== undefined) {
668      this.notifyHighlightedItem(id);
669    }
670  }
671
672  onRectDblClick(event: MouseEvent) {
673    event.preventDefault();
674
675    const clickedRectId = this.findClickedRectId(event);
676    if (clickedRectId === undefined) {
677      return;
678    }
679
680    this.elementRef.nativeElement.dispatchEvent(
681      new CustomEvent(ViewerEvents.RectsDblClick, {
682        bubbles: true,
683        detail: new RectDblClickDetail(clickedRectId),
684      }),
685    );
686  }
687
688  onMiniRectDblClick(event: MouseEvent) {
689    event.preventDefault();
690
691    this.elementRef.nativeElement.dispatchEvent(
692      new CustomEvent(ViewerEvents.MiniRectsDblClick, {bubbles: true}),
693    );
694  }
695
696  getZSpacingFactor(): number {
697    return this.largeRectsMapper3d.getZSpacingFactor();
698  }
699
700  getShadingMode(): ShadingMode {
701    return this.largeRectsMapper3d.getShadingMode();
702  }
703
704  onShadingModeButtonClicked() {
705    this.largeRectsMapper3d.updateShadingMode();
706    const newMode = this.largeRectsMapper3d.getShadingMode();
707    Analytics.Navigation.logRectSettingsChanged(
708      'shading mode',
709      newMode,
710      TRACE_INFO[this.dependencies[0]].name,
711    );
712    this.store?.add(this.storeKeyShadingMode, newMode);
713    this.updateLargeRectsColors();
714  }
715
716  onInteractionStart(components: CanColor[]) {
717    components.forEach((c) => (c.color = 'primary'));
718  }
719
720  onInteractionEnd(components: CanColor[]) {
721    components.forEach((c) => (c.color = 'accent'));
722  }
723
724  private getActiveDisplay(displays: DisplayIdentifier[]): DisplayIdentifier {
725    const displaysWithRects = displays.filter((display) =>
726      this.internalRects.some(
727        (rect) => !rect.isDisplay && rect.groupId === display.groupId,
728      ),
729    );
730    return (
731      displaysWithRects.find((display) => display.isActive) ??
732      displaysWithRects.at(0) ?? // fallback if no active displays
733      displays[0]
734    );
735  }
736
737  private updateCurrentDisplays(
738    displays: DisplayIdentifier[],
739    storeChange = true,
740  ) {
741    if (storeChange) {
742      this.store?.add(
743        this.storeKeySelectedDisplays,
744        JSON.stringify(displays.map((d) => d.displayId)),
745      );
746    }
747    this.currentDisplays = displays;
748    this.largeRectsMapper3d.setCurrentGroupIds(displays.map((d) => d.groupId));
749    this.redrawLargeRectsAndLabels(true);
750  }
751
752  private findClickedRectId(event: MouseEvent): string | undefined {
753    const canvas = event.target as Element;
754    const canvasOffset = canvas.getBoundingClientRect();
755
756    const x =
757      ((event.clientX - canvasOffset.left) / canvas.clientWidth) * 2 - 1;
758    const y =
759      -((event.clientY - canvasOffset.top) / canvas.clientHeight) * 2 + 1;
760    const z = 0;
761
762    return this.largeRectsCanvas?.getClickedRectId(x, y, z);
763  }
764
765  private doZoomIn(ratio = 1) {
766    this.largeRectsMapper3d.increaseZoomFactor(ratio);
767    this.updateLargeRectsPositionAndLabels();
768  }
769
770  private doZoomOut(ratio = 1) {
771    this.largeRectsMapper3d.decreaseZoomFactor(ratio);
772    this.updateLargeRectsPositionAndLabels();
773  }
774
775  private redrawLargeRectsAndLabels(updateBoundingBox = false) {
776    this.largeRectsMapper3d.setRects(this.internalRects);
777    const scene = this.largeRectsMapper3d.computeScene(updateBoundingBox);
778    this.largeRectsCanvas?.updateViewPosition(
779      scene.camera,
780      scene.boundingBox,
781      scene.zDepth,
782    );
783    this.largeRectsCanvas?.updateRects(scene.rects);
784    this.largeRectsCanvas?.updateLabels(scene.labels);
785    this.largeRectsCanvas?.renderView();
786  }
787
788  private updateLargeRectsPosition() {
789    const scene = this.largeRectsMapper3d.computeScene(false);
790    this.largeRectsCanvas?.updateViewPosition(
791      scene.camera,
792      scene.boundingBox,
793      scene.zDepth,
794    );
795    this.largeRectsCanvas?.renderView();
796  }
797
798  private updateLargeRectsPositionAndLabels() {
799    const scene = this.largeRectsMapper3d.computeScene(false);
800    this.largeRectsCanvas?.updateViewPosition(
801      scene.camera,
802      scene.boundingBox,
803      scene.zDepth,
804    );
805    this.largeRectsCanvas?.updateLabels(scene.labels);
806    this.largeRectsCanvas?.renderView();
807  }
808
809  private updateLargeRectsColors() {
810    const scene = this.largeRectsMapper3d.computeScene(false);
811    this.largeRectsCanvas?.updateRects(scene.rects);
812    this.largeRectsCanvas?.renderView();
813  }
814
815  private updateLargeRectsAndLabelsColors() {
816    const scene = this.largeRectsMapper3d.computeScene(false);
817    this.largeRectsCanvas?.updateRects(scene.rects);
818    this.largeRectsCanvas?.updateLabels(scene.labels);
819    this.largeRectsCanvas?.renderView();
820  }
821
822  private drawMiniRects() {
823    if (this.internalMiniRects && this.miniRectsCanvas) {
824      this.miniRectsMapper3d.setShadingMode(ShadingMode.GRADIENT);
825      this.miniRectsMapper3d.setCurrentGroupIds([
826        this.internalMiniRects[0]?.groupId,
827      ]);
828      this.miniRectsMapper3d.resetToOrthogonalState();
829      this.miniRectsMapper3d.setRects(this.internalMiniRects);
830
831      const scene = this.miniRectsMapper3d.computeScene(true);
832      this.miniRectsCanvas.updateViewPosition(
833        scene.camera,
834        scene.boundingBox,
835        scene.zDepth,
836      );
837      this.miniRectsCanvas.updateRects(scene.rects);
838      this.miniRectsCanvas.updateLabels(scene.labels);
839      this.miniRectsCanvas.renderView();
840
841      // Canvas internally sets these values to 100%. They need to be reset afterwards
842      if (this.miniRectsCanvasElement) {
843        this.miniRectsCanvasElement.style.width = '30%';
844        this.miniRectsCanvasElement.style.height = '30%';
845      }
846    }
847  }
848
849  private notifyHighlightedItem(id: string) {
850    const event: CustomEvent = new CustomEvent(
851      ViewerEvents.HighlightedIdChange,
852      {
853        bubbles: true,
854        detail: {id},
855      },
856    );
857    this.elementRef.nativeElement.dispatchEvent(event);
858  }
859}
860